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": Action anthropic = 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; }; builder.Services.AddAnthropicAnswerModel(name, anthropic); builder.Services.AddAnthropicFactDrafter(name, anthropic); 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)); // Fact drafters ride with the answer models: same entries, same keys. builder.Services.AddSingleton>(sp => wiki.AnswerModels.Models.Keys.Select(sp.GetRequiredKeyedService).ToList()); builder.Services.AddSingleton(sp => sp.GetRequiredKeyedService(wiki.AnswerModels.Active)); // ---- encyclopedia: the text model and the run exist only when enabled -------------------- var encyclopedia = wiki.Encyclopedia; builder.Services.AddSingleton(encyclopedia); string? encyclopediaConfigWarning = null; if (encyclopedia.Enabled) { var answerEntry = wiki.AnswerModels.Models.GetValueOrDefault(encyclopedia.AnswerModel); encyclopediaConfigWarning = answerEntry is null ? $"Encyclopedia.AnswerModel '{encyclopedia.AnswerModel}' has no entry in Wiki:AnswerModels:Models; using the default API URL" : string.IsNullOrEmpty(builder.Configuration[$"Wiki:AnswerModels:Models:{encyclopedia.AnswerModel}:ApiKey"]) ? $"No API key at Wiki:AnswerModels:Models:{encyclopedia.AnswerModel}:ApiKey; encyclopedia runs will fail until it is set" : null; answerEntry ??= new AnswerModelOptions(); builder.Services.AddAnthropicTextModel(o => { o.BaseUrl = answerEntry.Url; o.Model = encyclopedia.Model; o.ApiKey = builder.Configuration[$"Wiki:AnswerModels:Models:{encyclopedia.AnswerModel}:ApiKey"] ?? string.Empty; }); builder.Services.AddSingleton(new Wiki.Application.Encyclopedia.EncyclopediaSettings( encyclopedia.PlanMaxTokens, encyclopedia.ArticleMaxTokens, encyclopedia.MaxArticlesPerRun, encyclopedia.MaxSourceCharsPerArticle, encyclopedia.PilotSlugs, encyclopedia.MaxArticles)); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); } // 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>(), .. sp.GetRequiredService>(), .. sp.GetServices(), ]; 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(new HouseMediaCache()); 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 sign in with OIDC against Authentik, machines with an API key ---------- builder.Services.AddWikiAuthentication(builder.Configuration); // ---- UI ---------------------------------------------------------------------------------- builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddMudServices(); var app = builder.Build(); if (encyclopediaConfigWarning is not null) app.Logger.LogWarning("{Warning}", encyclopediaConfigWarning); // Outermost, so the headers also reach a response the exception handler rewrites. app.UseHouseHeaders(); 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). // House 404s keep their own response: /not-found needs a login, which would turn them into a redirect. app.UseWhen( ctx => HttpMethods.IsGet(ctx.Request.Method) && !ctx.Request.Path.StartsWithSegments("/api") && !ctx.Request.Path.StartsWithSegments("/house"), branch => branch.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true)); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); app.MapHealthEndpoints(); app.MapIndexEndpoints(); app.MapApiV1(); app.MapHouseEndpoints(); app.MapStaticAssets().AllowAnonymous(); app.MapRazorComponents().AddInteractiveServerRenderMode(); app.Run(); public partial class Program;