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:
@@ -10,4 +10,9 @@
|
||||
<ProjectReference Include="..\ChatAgent.Shared\ChatAgent.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.74.0" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.74.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,44 +1,50 @@
|
||||
// ChatController.cs -- Proxies chat requests to the Responses API with streaming.
|
||||
// ChatController.cs -- Handles chat requests using Semantic Kernel for AI completion.
|
||||
//
|
||||
// This controller receives messages from the WASM client, forwards them to the
|
||||
// local Responses API (OpenAI-compatible) at a configurable URL, and streams
|
||||
// the response tokens back as Server-Sent Events (SSE).
|
||||
// This controller receives messages from the WASM client, processes them through
|
||||
// Semantic Kernel's chat completion service (pointed at a local CLIProxyAPI proxy),
|
||||
// and streams the response tokens back as Server-Sent Events (SSE).
|
||||
//
|
||||
// Key concepts demonstrated:
|
||||
// - IHttpClientFactory named client injection for external API calls
|
||||
// - IConfiguration for reading appsettings.json values
|
||||
// - SSE streaming response from ASP.NET Core (text/event-stream)
|
||||
// - Parsing upstream SSE events and re-emitting simplified events to the client
|
||||
// - Semantic Kernel injection and usage in an ASP.NET Core controller
|
||||
// - IChatCompletionService for streaming chat completions
|
||||
// - OpenAIPromptExecutionSettings for configuring tool calling behavior
|
||||
// - FunctionChoiceBehavior.Auto() for automatic tool invocation
|
||||
// - Streaming SK responses as SSE to maintain the existing client contract
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ChatAgent.Api.Plugins;
|
||||
using ChatAgent.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace ChatAgent.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxies chat requests to the Responses API and streams tokens back to the client.
|
||||
/// The Responses API URL and model are configured in appsettings.json under "ResponsesApi".
|
||||
/// Processes chat requests through Semantic Kernel and streams tokens back to the client.
|
||||
/// The Kernel is configured in Program.cs with an OpenAI connector pointed at CLIProxyAPI.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChatController : ControllerBase
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
// Kernel is the central Semantic Kernel object. It holds the AI service
|
||||
// (chat completion) and any registered plugins (tools). We inject it via DI
|
||||
// rather than creating it manually, following ASP.NET Core conventions.
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
public ChatController(IHttpClientFactory httpClientFactory, IConfiguration configuration)
|
||||
public ChatController(Kernel kernel)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_kernel = kernel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST /api/chat -- Accepts a ChatRequest with messages, forwards to the Responses API
|
||||
/// with streaming enabled, and re-emits text deltas as simplified SSE events.
|
||||
/// POST /api/chat -- Accepts a ChatRequest with messages, processes them through
|
||||
/// Semantic Kernel's chat completion with tool calling enabled, and streams
|
||||
/// text tokens back as SSE events.
|
||||
///
|
||||
/// Client SSE format:
|
||||
/// Client SSE format (unchanged from before migration):
|
||||
/// data: {"text":"token here"}\n\n -- for each text delta
|
||||
/// data: [DONE]\n\n -- when streaming completes
|
||||
/// data: {"error":"message"}\n\n -- if an error occurs
|
||||
@@ -47,113 +53,80 @@ namespace ChatAgent.Api.Controllers
|
||||
public async Task Post([FromBody] ChatRequest request)
|
||||
{
|
||||
// Set the response content type to SSE so the client knows to read it as a stream.
|
||||
// "text/event-stream" is the standard MIME type for Server-Sent Events.
|
||||
Response.ContentType = "text/event-stream";
|
||||
Response.Headers["Cache-Control"] = "no-cache";
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("ResponsesApi");
|
||||
var model = _configuration["ResponsesApi:Model"] ?? "claude-sonnet-4-6";
|
||||
// IChatCompletionService is the SK abstraction for chat-based AI models.
|
||||
// GetRequiredService<T>() retrieves it from the kernel's service collection.
|
||||
// This is the service registered via AddOpenAIChatCompletion() in Program.cs.
|
||||
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
||||
|
||||
// Build the Responses API request payload.
|
||||
// The Responses API expects "input" (array of role/content objects) and "model".
|
||||
// "stream": true enables SSE streaming of token deltas.
|
||||
var inputMessages = request.Messages.Select(m => new
|
||||
// ChatHistory is SK's representation of a conversation. It maps directly
|
||||
// to the messages array in OpenAI's API format. We convert our ChatMessage
|
||||
// DTOs into SK's format.
|
||||
var chatHistory = new ChatHistory();
|
||||
foreach (var msg in request.Messages)
|
||||
{
|
||||
role = m.Role,
|
||||
content = m.Content
|
||||
}).ToArray();
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model,
|
||||
input = inputMessages,
|
||||
stream = true
|
||||
};
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
|
||||
// Use HttpCompletionOption.ResponseHeadersRead so we start reading the stream
|
||||
// as soon as headers arrive, rather than waiting for the full response body.
|
||||
using var upstreamRequest = new HttpRequestMessage(HttpMethod.Post, "/v1/responses")
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
using var upstreamResponse = await client.SendAsync(
|
||||
upstreamRequest,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
HttpContext.RequestAborted);
|
||||
|
||||
if (!upstreamResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await upstreamResponse.Content.ReadAsStringAsync();
|
||||
await WriteSSEAsync($"{{\"error\":\"Responses API returned {upstreamResponse.StatusCode}: {EscapeJson(errorBody)}\"}}");
|
||||
await WriteSSEAsync("[DONE]");
|
||||
return;
|
||||
if (msg.Role == "user")
|
||||
chatHistory.AddUserMessage(msg.Content);
|
||||
else if (msg.Role == "assistant")
|
||||
chatHistory.AddAssistantMessage(msg.Content);
|
||||
}
|
||||
|
||||
// Read the upstream SSE stream line by line, extract text deltas,
|
||||
// and re-emit them as simplified SSE events to the client.
|
||||
using var stream = await upstreamResponse.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
// Import the ExtractionPlugin so its [KernelFunction] methods are available
|
||||
// as tools for this request. We import from the DI-registered instance.
|
||||
// This makes validate_extracted_fields() visible to the LLM.
|
||||
var extractionPlugin = HttpContext.RequestServices.GetRequiredService<ExtractionPlugin>();
|
||||
_kernel.ImportPluginFromObject(extractionPlugin, "Extraction");
|
||||
|
||||
// Use ReadLineAsync and check for null instead of reader.EndOfStream,
|
||||
// because EndOfStream performs a synchronous read which is not supported
|
||||
// in ASP.NET Core's async pipeline.
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
// OpenAIPromptExecutionSettings controls how the LLM processes the request.
|
||||
//
|
||||
// FunctionChoiceBehavior.Auto() enables automatic function calling:
|
||||
// - The LLM sees all registered plugin functions as available tools
|
||||
// - When the LLM decides to call a tool, SK automatically executes it
|
||||
// - The tool result is fed back to the LLM so it can reason about it
|
||||
// - This creates the agentic loop: extract → validate → fix → retry
|
||||
//
|
||||
// The Auto() behavior allows the LLM to make tool call round-trips
|
||||
// autonomously. SK's built-in safeguard limits the number of auto-invoke
|
||||
// attempts to prevent runaway loops. If the agent exhausts retries,
|
||||
// it responds with a clarification request to the user.
|
||||
var executionSettings = new OpenAIPromptExecutionSettings
|
||||
{
|
||||
// SSE format: "data: {json}" lines, separated by blank lines.
|
||||
// We only care about lines starting with "data: ".
|
||||
if (!line.StartsWith("data: "))
|
||||
continue;
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
|
||||
var data = line.Substring(6); // strip "data: " prefix
|
||||
|
||||
// Parse the JSON to find response.output_text.delta events.
|
||||
// These carry the actual text tokens in the "delta" field.
|
||||
try
|
||||
// GetStreamingChatMessageContentsAsync returns an IAsyncEnumerable that yields
|
||||
// content chunks as they arrive from the LLM. Each chunk may contain:
|
||||
// - Text content (the actual response tokens)
|
||||
// - Tool call requests (which SK handles automatically via auto-invoke)
|
||||
//
|
||||
// We iterate the stream and forward text chunks as SSE events,
|
||||
// preserving the exact format the Blazor client expects.
|
||||
await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync(
|
||||
chatHistory,
|
||||
executionSettings,
|
||||
_kernel,
|
||||
HttpContext.RequestAborted))
|
||||
{
|
||||
// Only emit chunks that contain text content.
|
||||
// Tool call chunks are handled internally by SK and don't produce
|
||||
// visible output -- the LLM will emit text after processing tool results.
|
||||
if (!string.IsNullOrEmpty(chunk.Content))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("type", out var typeElement))
|
||||
{
|
||||
var eventType = typeElement.GetString();
|
||||
|
||||
if (eventType == "response.output_text.delta")
|
||||
{
|
||||
// Extract the text delta and send it to the client
|
||||
if (root.TryGetProperty("delta", out var deltaElement))
|
||||
{
|
||||
var delta = deltaElement.GetString() ?? "";
|
||||
await WriteSSEAsync($"{{\"text\":{JsonSerializer.Serialize(delta)}}}");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
else if (eventType == "response.completed")
|
||||
{
|
||||
// Stream is done
|
||||
await WriteSSEAsync("[DONE]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Skip malformed JSON lines
|
||||
await WriteSSEAsync($"{{\"text\":{JsonSerializer.Serialize(chunk.Content)}}}");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// If we exit the loop without seeing response.completed, still signal done
|
||||
// Signal stream completion to the client
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await WriteSSEAsync($"{{\"error\":{JsonSerializer.Serialize($"Failed to reach Responses API: {ex.Message}")}}}");
|
||||
await WriteSSEAsync($"{{\"error\":{JsonSerializer.Serialize($"Failed to reach LLM service: {ex.Message}")}}}");
|
||||
await WriteSSEAsync("[DONE]");
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
@@ -171,13 +144,5 @@ namespace ChatAgent.Api.Controllers
|
||||
await Response.WriteAsync($"data: {data}\n\n");
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a string for embedding in JSON (handles quotes and backslashes).
|
||||
/// </summary>
|
||||
private static string EscapeJson(string s)
|
||||
{
|
||||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
103
src/ChatAgent.Api/Plugins/ExtractionPlugin.cs
Normal file
103
src/ChatAgent.Api/Plugins/ExtractionPlugin.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
// ExtractionPlugin.cs -- Semantic Kernel plugin for validating extracted fields.
|
||||
//
|
||||
// In Semantic Kernel, a "plugin" is a class whose methods are exposed to the LLM
|
||||
// as callable tools (functions). The LLM can decide to invoke these functions during
|
||||
// a conversation when it determines they are relevant to the task.
|
||||
//
|
||||
// Key SK concepts demonstrated here:
|
||||
//
|
||||
// [KernelFunction] -- Marks a method as a function the LLM can call. SK discovers
|
||||
// these at startup and includes them in the tool list sent with each LLM request.
|
||||
//
|
||||
// [Description] -- Tells the LLM what the function does. The LLM reads this text
|
||||
// to decide whether and when to call the function. Good descriptions are critical
|
||||
// for reliable tool use.
|
||||
//
|
||||
// Auto-invocation -- When configured with FunctionChoiceBehavior.Auto(), SK
|
||||
// automatically executes tool calls the LLM makes and feeds the results back,
|
||||
// allowing the LLM to reason about the output and decide next steps (retry, fix,
|
||||
// or respond to the user). This creates the agentic loop.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ChatAgent.Shared.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace ChatAgent.Api.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin that validates extracted key-value fields against the predefined schema.
|
||||
/// The LLM calls this after extracting fields from natural language to check
|
||||
/// whether all required fields are present and correctly typed.
|
||||
/// </summary>
|
||||
public class ExtractionPlugin
|
||||
{
|
||||
// The required fields that must be non-null and non-empty for validation to pass.
|
||||
// These match the required properties on ExtractedFields.
|
||||
private static readonly string[] RequiredFields =
|
||||
{ "Client", "Project", "Hours", "Rate", "Currency", "Date" };
|
||||
|
||||
/// <summary>
|
||||
/// Validates extracted fields against the predefined schema.
|
||||
/// Returns a JSON object indicating whether the extraction is valid
|
||||
/// and listing any errors found.
|
||||
/// </summary>
|
||||
/// <param name="fieldsJson">
|
||||
/// JSON string representing the extracted fields. Expected shape:
|
||||
/// { "Client": "...", "Project": "...", "Hours": 3, ... }
|
||||
/// </param>
|
||||
/// <returns>JSON string with { "IsValid": bool, "Errors": [...] }</returns>
|
||||
[KernelFunction("validate_extracted_fields")]
|
||||
[Description("Validates extracted key-value fields against the required schema. " +
|
||||
"Call this after extracting fields from natural language text to check " +
|
||||
"that all required fields (Client, Project, Hours, Rate, Currency, Date) " +
|
||||
"are present and correctly typed. Returns validation result with any errors.")]
|
||||
public string ValidateExtractedFields(
|
||||
[Description("JSON string of extracted fields")] string fieldsJson)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
ExtractedFields? fields;
|
||||
try
|
||||
{
|
||||
fields = JsonSerializer.Deserialize<ExtractedFields>(fieldsJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add($"Invalid JSON: {ex.Message}");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
if (fields == null)
|
||||
{
|
||||
result.IsValid = false;
|
||||
result.Errors.Add("Deserialized fields object is null");
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
// Check each required field for presence and non-empty value
|
||||
if (string.IsNullOrWhiteSpace(fields.Client))
|
||||
result.Errors.Add("Missing required field: Client");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Project))
|
||||
result.Errors.Add("Missing required field: Project");
|
||||
|
||||
if (fields.Hours == null || fields.Hours <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Hours (must be a positive number)");
|
||||
|
||||
if (fields.Rate == null || fields.Rate <= 0)
|
||||
result.Errors.Add("Missing or invalid required field: Rate (must be a positive number)");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Currency))
|
||||
result.Errors.Add("Missing required field: Currency");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fields.Date))
|
||||
result.Errors.Add("Missing required field: Date");
|
||||
|
||||
result.IsValid = result.Errors.Count == 0;
|
||||
return JsonSerializer.Serialize(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
// Program.cs -- ASP.NET Core Web API entry point for ChatAgent.
|
||||
//
|
||||
// These using directives bring in the Semantic Kernel extension methods for DI registration.
|
||||
// Without them, the AddOpenAIChatCompletion() and AddKernel() methods won't be found.
|
||||
using Microsoft.SemanticKernel;
|
||||
//
|
||||
// This is the backend server. In Phase 1, it only serves a health check endpoint.
|
||||
// In later phases, it will proxy OpenAI API calls (keeping the API key server-side)
|
||||
// and manage JSON file storage for conversation persistence.
|
||||
@@ -16,14 +20,43 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
// for explicit structure -- each controller is a separate file with clear routing (D-05).
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// Register a named HttpClient for proxying requests to the Responses API.
|
||||
// The base URL comes from appsettings.json (server-side config, not exposed to the browser).
|
||||
// IHttpClientFactory manages the underlying HttpMessageHandler lifetime.
|
||||
builder.Services.AddHttpClient("ResponsesApi", client =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["ResponsesApi:BaseUrl"] ?? "http://localhost:8317";
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
// --- Semantic Kernel Setup ---
|
||||
//
|
||||
// Semantic Kernel (SK) is an AI orchestration framework from Microsoft. It provides:
|
||||
// - Chat completion connectors (OpenAI, Azure OpenAI, etc.)
|
||||
// - Plugin system for exposing C# methods as tools the LLM can call
|
||||
// - Automatic function calling (the LLM decides when to invoke tools)
|
||||
// - Streaming support for token-by-token delivery
|
||||
//
|
||||
// The "Kernel" is the central object: it holds the AI service, plugins, and configuration.
|
||||
// We register it in DI so controllers can inject it.
|
||||
|
||||
// Read the CLIProxyAPI proxy URL and model from appsettings.json.
|
||||
// The OpenAI connector works with any OpenAI-compatible API endpoint,
|
||||
// so we point it at our local CLIProxyAPI proxy rather than OpenAI directly.
|
||||
// IMPORTANT: The base URL must include "/v1" because the OpenAI SDK appends
|
||||
// "chat/completions" directly to the base URL. Without "/v1", requests would
|
||||
// hit "/chat/completions" instead of "/v1/chat/completions" and get a 404.
|
||||
var responsesApiBaseUrl = builder.Configuration["ResponsesApi:BaseUrl"] ?? "http://localhost:8317/v1";
|
||||
var model = builder.Configuration["ResponsesApi:Model"] ?? "claude-sonnet-4-6";
|
||||
|
||||
// AddOpenAIChatCompletion registers an IChatCompletionService in DI.
|
||||
// The "endpoint" parameter lets us target any OpenAI-compatible API (here: CLIProxyAPI).
|
||||
// The "apiKey" is required by the connector but CLIProxyAPI may not check it,
|
||||
// so we use a placeholder. In production, this would be a real API key.
|
||||
builder.Services.AddOpenAIChatCompletion(
|
||||
modelId: model,
|
||||
endpoint: new Uri(responsesApiBaseUrl),
|
||||
apiKey: builder.Configuration["ResponsesApi:ApiKey"] ?? "not-needed");
|
||||
|
||||
// AddKernel() registers the Kernel class itself in DI. It automatically picks up
|
||||
// any AI services (like the chat completion above) that are already registered.
|
||||
builder.Services.AddKernel();
|
||||
|
||||
// Register the ExtractionPlugin so the Kernel can expose its [KernelFunction] methods
|
||||
// as tools. When the LLM sees these tools, it can decide to call them during a conversation
|
||||
// to validate extracted data. The plugin is registered as a singleton via DI.
|
||||
builder.Services.AddSingleton<ChatAgent.Api.Plugins.ExtractionPlugin>();
|
||||
|
||||
// AddCors() registers Cross-Origin Resource Sharing services.
|
||||
// CORS is REQUIRED because the Blazor WASM client runs on a different origin
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ResponsesApi": {
|
||||
"BaseUrl": "http://localhost:8317",
|
||||
"BaseUrl": "http://localhost:8317/v1",
|
||||
"Model": "claude-sonnet-4-6"
|
||||
}
|
||||
}
|
||||
|
||||
41
src/ChatAgent.Shared/Models/ExtractedFields.cs
Normal file
41
src/ChatAgent.Shared/Models/ExtractedFields.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// ExtractedFields.cs -- Strongly-typed schema for structured data extraction.
|
||||
//
|
||||
// This class defines the predefined set of key-value fields that the AI agent
|
||||
// extracts from natural language input (e.g., email text). All fields are known
|
||||
// at compile time. Required fields must be non-null for validation to pass.
|
||||
//
|
||||
// Placeholder fields are used until the real schema is provided.
|
||||
|
||||
namespace ChatAgent.Shared.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// The fixed set of fields the agent extracts from natural language input.
|
||||
/// Required fields are marked with comments; optional fields may be null.
|
||||
/// </summary>
|
||||
public class ExtractedFields
|
||||
{
|
||||
/// <summary>Client or company name (required).</summary>
|
||||
public string? Client { get; set; }
|
||||
|
||||
/// <summary>Project or engagement name (required).</summary>
|
||||
public string? Project { get; set; }
|
||||
|
||||
/// <summary>Number of hours worked (required).</summary>
|
||||
public decimal? Hours { get; set; }
|
||||
|
||||
/// <summary>Hourly rate (required).</summary>
|
||||
public decimal? Rate { get; set; }
|
||||
|
||||
/// <summary>Currency code, e.g. "USD", "GBP" (required).</summary>
|
||||
public string? Currency { get; set; }
|
||||
|
||||
/// <summary>Date of work or service (required). ISO 8601 format preferred.</summary>
|
||||
public string? Date { get; set; }
|
||||
|
||||
/// <summary>Description of work performed (optional).</summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>Purchase order number (optional).</summary>
|
||||
public string? PoNumber { get; set; }
|
||||
}
|
||||
}
|
||||
24
src/ChatAgent.Shared/Models/ValidationResult.cs
Normal file
24
src/ChatAgent.Shared/Models/ValidationResult.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// ValidationResult.cs -- Result of validating extracted fields.
|
||||
//
|
||||
// Returned by the ExtractionPlugin's validation function so the AI agent
|
||||
// can see which fields are missing or malformed and decide whether to
|
||||
// retry extraction or escalate to the user.
|
||||
|
||||
namespace ChatAgent.Shared.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes whether extracted fields passed validation, and if not,
|
||||
/// which specific errors were found.
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>True if all required fields are present and correctly typed.</summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of validation error messages (e.g., "Missing required field: Client").
|
||||
/// Empty when IsValid is true.
|
||||
/// </summary>
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user