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>
107 lines
3.2 KiB
C#
107 lines
3.2 KiB
C#
using System.Text.Json;
|
|
using ChatAgent.Api.Plugins;
|
|
using ChatAgent.Shared.Models;
|
|
|
|
namespace ChatAgent.Api.Tests;
|
|
|
|
public class ExtractionPluginTests
|
|
{
|
|
private readonly ExtractionPlugin _plugin = new();
|
|
|
|
[Fact]
|
|
public void ValidateExtractedFields_AllRequiredPresent_ReturnsValid()
|
|
{
|
|
var fields = new ExtractedFields
|
|
{
|
|
Client = "Acme Corp",
|
|
Project = "Phase 2",
|
|
Hours = 3,
|
|
Rate = 150,
|
|
Currency = "USD",
|
|
Date = "2026-04-01"
|
|
};
|
|
|
|
var resultJson = _plugin.ValidateExtractedFields(JsonSerializer.Serialize(fields));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsValid);
|
|
Assert.Empty(result.Errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateExtractedFields_MissingRequired_ReturnsErrors()
|
|
{
|
|
// Missing Client and Hours
|
|
var fields = new ExtractedFields
|
|
{
|
|
Project = "Phase 2",
|
|
Rate = 150,
|
|
Currency = "USD",
|
|
Date = "2026-04-01"
|
|
};
|
|
|
|
var resultJson = _plugin.ValidateExtractedFields(JsonSerializer.Serialize(fields));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Client"));
|
|
Assert.Contains(result.Errors, e => e.Contains("Hours"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateExtractedFields_InvalidJson_ReturnsError()
|
|
{
|
|
var resultJson = _plugin.ValidateExtractedFields("not valid json");
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Invalid JSON"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateExtractedFields_ZeroHours_ReturnsError()
|
|
{
|
|
var fields = new ExtractedFields
|
|
{
|
|
Client = "Acme Corp",
|
|
Project = "Phase 2",
|
|
Hours = 0,
|
|
Rate = 150,
|
|
Currency = "USD",
|
|
Date = "2026-04-01"
|
|
};
|
|
|
|
var resultJson = _plugin.ValidateExtractedFields(JsonSerializer.Serialize(fields));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains(result.Errors, e => e.Contains("Hours"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidateExtractedFields_OptionalFieldsMissing_StillValid()
|
|
{
|
|
// Description and PoNumber are optional
|
|
var fields = new ExtractedFields
|
|
{
|
|
Client = "Acme Corp",
|
|
Project = "Phase 2",
|
|
Hours = 3,
|
|
Rate = 150,
|
|
Currency = "USD",
|
|
Date = "2026-04-01"
|
|
// Description and PoNumber intentionally omitted
|
|
};
|
|
|
|
var resultJson = _plugin.ValidateExtractedFields(JsonSerializer.Serialize(fields));
|
|
var result = JsonSerializer.Deserialize<ValidationResult>(resultJson);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsValid);
|
|
}
|
|
}
|