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>
147 lines
7.0 KiB
C#
147 lines
7.0 KiB
C#
// 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.
|
|
//
|
|
// ASP.NET Core uses a "builder pattern": first configure services (DI container),
|
|
// then build the app, configure middleware pipeline, and run.
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// --- Service Registration (Dependency Injection container) ---
|
|
|
|
// AddControllers() registers MVC controller services so ASP.NET Core discovers
|
|
// classes decorated with [ApiController]. We use Controllers (not Minimal API)
|
|
// for explicit structure -- each controller is a separate file with clear routing (D-05).
|
|
builder.Services.AddControllers();
|
|
|
|
// --- 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();
|
|
|
|
// --- External API Typed HttpClients ---
|
|
//
|
|
// Each extraction tool wraps an external API call. We register typed HttpClients
|
|
// so that each service gets its own HttpClient with a pre-configured base URL.
|
|
// AddHttpClient<T>() uses IHttpClientFactory under the hood, which manages
|
|
// HttpClient lifetimes and avoids socket exhaustion.
|
|
|
|
builder.Services.AddHttpClient<ChatAgent.Api.Services.CounterpartyApiClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(
|
|
builder.Configuration["ExternalApis:CounterpartyBaseUrl"] ?? "http://localhost:5000/api/counterparty");
|
|
});
|
|
|
|
builder.Services.AddHttpClient<ChatAgent.Api.Services.TradeApiClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(
|
|
builder.Configuration["ExternalApis:TradeBaseUrl"] ?? "http://localhost:5000/api/trade");
|
|
});
|
|
|
|
builder.Services.AddHttpClient<ChatAgent.Api.Services.CurrencyApiClient>(client =>
|
|
{
|
|
client.BaseAddress = new Uri(
|
|
builder.Configuration["ExternalApis:CurrencyBaseUrl"] ?? "http://localhost:5000/api/currency");
|
|
});
|
|
|
|
// --- Few-Shot Prompting Service ---
|
|
//
|
|
// FewShotService loads the extraction instruction template and few-shot examples
|
|
// from disk at startup. It pre-assembles a ChatHistory prefix that is cloned
|
|
// for each extraction request. Registered as a singleton since examples don't
|
|
// change at runtime.
|
|
// Resolve the examples path relative to the content root so it works correctly
|
|
// both when running the app normally and in WebApplicationFactory test contexts.
|
|
var examplesRelativePath = builder.Configuration["Examples:FewShotPath"] ?? "examples/extraction";
|
|
var examplesAbsolutePath = Path.IsPathRooted(examplesRelativePath)
|
|
? examplesRelativePath
|
|
: Path.Combine(builder.Environment.ContentRootPath, examplesRelativePath);
|
|
builder.Services.AddSingleton(new ChatAgent.Api.Services.FewShotService(examplesAbsolutePath));
|
|
|
|
// Register the ExtractionPlugin with its typed HttpClient dependencies.
|
|
// The plugin exposes [KernelFunction] methods as tools the LLM can call
|
|
// to look up counterparties, validate trades, currencies, and the extraction schema.
|
|
// Scoped lifetime because it depends on typed HttpClients (which are transient).
|
|
builder.Services.AddScoped<ChatAgent.Api.Plugins.ExtractionPlugin>();
|
|
|
|
// AddCors() registers Cross-Origin Resource Sharing services.
|
|
// CORS is REQUIRED because the Blazor WASM client runs on a different origin
|
|
// (https://localhost:5200) than this API (https://localhost:7100).
|
|
// Browsers block cross-origin HTTP requests by default as a security measure.
|
|
// Without this policy, the client's fetch() calls to the API would be rejected.
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("AllowBlazorClient", policy =>
|
|
{
|
|
policy
|
|
// Only allow requests from the Blazor WASM client origin
|
|
.WithOrigins("https://localhost:5200")
|
|
// Allow any HTTP header (Content-Type, Accept, etc.)
|
|
.AllowAnyHeader()
|
|
// Allow any HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
.AllowAnyMethod();
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
// --- Middleware Pipeline ---
|
|
// Middleware order matters in ASP.NET Core -- each middleware runs in the order
|
|
// it is registered. CORS must be applied before routing and authorization
|
|
// so that preflight (OPTIONS) requests are handled correctly.
|
|
|
|
// Apply the CORS policy globally -- every response includes the correct
|
|
// Access-Control-Allow-Origin header for the Blazor client's origin.
|
|
app.UseCors("AllowBlazorClient");
|
|
|
|
// UseAuthorization() enables the authorization middleware. Even though we have
|
|
// no auth in Phase 1, it is included because ASP.NET Core expects it in the
|
|
// pipeline when controllers are used. It is a no-op without [Authorize] attributes.
|
|
app.UseAuthorization();
|
|
|
|
// MapControllers() scans the assembly for all classes with [ApiController]
|
|
// and maps their routes. This is what connects HealthController's
|
|
// [Route("api/[controller]")] to the URL path /api/health.
|
|
app.MapControllers();
|
|
|
|
app.Run();
|
|
|
|
// This partial class declaration makes the auto-generated Program class public,
|
|
// which is required by WebApplicationFactory<Program> in integration tests.
|
|
// Without this, the test project cannot reference the entry point type.
|
|
public partial class Program { }
|