using Microsoft.AspNetCore.Authentication; using MudBlazor.Services; using Wiki.Adapters.FileIndex; using Wiki.Adapters.Gitea; using Wiki.Adapters.LlamaCpp; using Wiki.Adapters.TypeSafe; using Wiki.Adapters.Anthropic; using Wiki.Application.Retrieval; using Wiki.Application.Status; using Wiki.Application.Sync; using Wiki.Core.Ports; using Wiki.Host.Api; using Wiki.Host.Auth; using Wiki.Host.Components; using Wiki.Host.Composition; var builder = WebApplication.CreateBuilder(args); var wiki = builder.Configuration.GetSection("Wiki").Get() ?? new WikiOptions(); builder.Services.AddSingleton(wiki); builder.Services.AddSingleton(TimeProvider.System); // ---- adapters, selected by configuration ------------------------------------------------ builder.Services.AddGiteaNoteStore(o => { o.BaseUrl = wiki.NoteStore.Gitea.BaseUrl; o.Repository = wiki.NoteStore.Gitea.Repository; o.Branch = wiki.NoteStore.Gitea.Branch; o.Token = builder.Configuration["Wiki:NoteStore:Gitea:Token"] ?? string.Empty; }); builder.Services.AddFileIndexAdapters(o => o.Directory = wiki.Index.File.Directory); foreach (var (name, model) in wiki.Embeddings.Models) { switch (model.Adapter) { case "LlamaCpp": builder.Services.AddLlamaCppEmbeddings(name, o => { o.Url = model.Url; o.Model = model.Model; o.ModelId = name; o.Dimension = model.Dimension; o.BasicAuth = builder.Configuration[$"Wiki:Embeddings:Models:{name}:BasicAuth"]; }); break; default: throw new InvalidOperationException($"unknown embeddings adapter '{model.Adapter}' for model '{name}'"); } } builder.Services.AddSingleton>(sp => wiki.Embeddings.Models.Keys.Select(sp.GetRequiredKeyedService).ToList()); builder.Services.AddSingleton(sp => sp.GetRequiredKeyedService(wiki.Embeddings.Active)); foreach (var (name, judge) in wiki.Judges.Models) { switch (judge.Adapter) { case "TypeSafe": builder.Services.AddTypeSafeJudge(name, o => { o.Url = judge.Url; o.Model = judge.Model; o.ModelId = name; o.ApiKey = builder.Configuration[$"Wiki:Judges:Models:{name}:ApiKey"] ?? string.Empty; }); break; default: throw new InvalidOperationException($"unknown judge adapter '{judge.Adapter}' for '{name}'"); } } builder.Services.AddSingleton>(sp => wiki.Judges.Models.Keys.Select(sp.GetRequiredKeyedService).ToList()); builder.Services.AddSingleton(sp => sp.GetRequiredKeyedService(wiki.Judges.Active)); foreach (var (name, answer) in wiki.AnswerModels.Models) { switch (answer.Adapter) { case "Anthropic": builder.Services.AddAnthropicAnswerModel(name, o => { o.BaseUrl = answer.Url; o.Model = answer.Model; o.ModelId = name; o.MaxTokens = answer.MaxTokens; o.ApiKey = builder.Configuration[$"Wiki:AnswerModels:Models:{name}:ApiKey"] ?? string.Empty; }); break; default: throw new InvalidOperationException($"unknown answer-model adapter '{answer.Adapter}' for '{name}'"); } } builder.Services.AddSingleton>(sp => wiki.AnswerModels.Models.Keys.Select(sp.GetRequiredKeyedService).ToList()); builder.Services.AddSingleton(sp => sp.GetRequiredKeyedService(wiki.AnswerModels.Active)); // One adapter may serve several ports; list it once. builder.Services.AddSingleton>(sp => { List all = [ sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), .. sp.GetRequiredService>(), .. sp.GetRequiredService>(), .. sp.GetRequiredService>(), ]; return all.Distinct().ToList(); }); // ---- use cases --------------------------------------------------------------------------- builder.Services.AddSingleton(new SyncOptions()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // ---- auth: humans arrive through the Authentik outpost, machines with an API key --------- builder.Services.AddAuthentication(ProxyHeaderAuthenticationHandler.SchemeName) .AddScheme(ProxyHeaderAuthenticationHandler.SchemeName, null) .AddScheme(ApiKeyAuthenticationHandler.SchemeName, null); builder.Services.AddAuthorization(options => { options.AddPolicy(Policies.Write, p => p.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName).RequireAuthenticatedUser()); }); // ---- UI ---------------------------------------------------------------------------------- builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.AddMudServices(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); } // The not-found page is for the UI only: an API 401/404 must stay a JSON status, not be // re-executed as a POST against a Blazor page (which fails antiforgery with an empty 400). app.UseWhen( ctx => HttpMethods.IsGet(ctx.Request.Method) && !ctx.Request.Path.StartsWithSegments("/api"), branch => branch.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true)); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); app.MapHealthEndpoints(); app.MapIndexEndpoints(); app.MapApiV1(); app.MapStaticAssets(); app.MapRazorComponents().AddInteractiveServerRenderMode(); app.Run(); public partial class Program;