Files
AgenticCode/src/ChatAgent.Api/Program.cs
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

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 { }