// 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() uses IHttpClientFactory under the hood, which manages // HttpClient lifetimes and avoids socket exhaustion. builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri( builder.Configuration["ExternalApis:CounterpartyBaseUrl"] ?? "http://localhost:5000/api/counterparty"); }); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri( builder.Configuration["ExternalApis:TradeBaseUrl"] ?? "http://localhost:5000/api/trade"); }); builder.Services.AddHttpClient(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(); // 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 in integration tests. // Without this, the test project cannot reference the entry point type. public partial class Program { }