Two classes over the same backend showed as two adapters on the status page. One GiteaRepository, one HttpClient, one health entry; the host lists an adapter once even when it serves several ports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
111 lines
4.5 KiB
C#
111 lines
4.5 KiB
C#
using Microsoft.AspNetCore.Authentication;
|
|
using MudBlazor.Services;
|
|
using Wiki.Adapters.FileIndex;
|
|
using Wiki.Adapters.Gitea;
|
|
using Wiki.Adapters.LlamaCpp;
|
|
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<WikiOptions>() ?? new WikiOptions();
|
|
builder.Services.AddSingleton(wiki);
|
|
builder.Services.AddSingleton(TimeProvider.System);
|
|
|
|
// ---- adapters, selected by configuration ------------------------------------------------
|
|
builder.Services.AddGiteaAdapters(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<IReadOnlyList<IEmbeddingModel>>(sp =>
|
|
wiki.Embeddings.Models.Keys.Select(sp.GetRequiredKeyedService<IEmbeddingModel>).ToList());
|
|
builder.Services.AddSingleton<IEmbeddingModel>(sp => sp.GetRequiredKeyedService<IEmbeddingModel>(wiki.Embeddings.Active));
|
|
// One adapter may serve several ports (Gitea is note store and fact sink); list it once.
|
|
builder.Services.AddSingleton<IReadOnlyList<IAdapterHealth>>(sp =>
|
|
{
|
|
List<IAdapterHealth> all =
|
|
[
|
|
sp.GetRequiredService<INoteStore>(),
|
|
sp.GetRequiredService<IFactSink>(),
|
|
sp.GetRequiredService<IVectorIndex>(),
|
|
sp.GetRequiredService<IDocumentCatalog>(),
|
|
.. sp.GetRequiredService<IReadOnlyList<IEmbeddingModel>>(),
|
|
];
|
|
return all.Distinct().ToList();
|
|
});
|
|
|
|
// ---- use cases ---------------------------------------------------------------------------
|
|
builder.Services.AddSingleton(new SyncOptions());
|
|
builder.Services.AddSingleton<SyncUseCase>();
|
|
builder.Services.AddSingleton<SyncCoordinator>();
|
|
builder.Services.AddSingleton<RankUseCase>();
|
|
builder.Services.AddSingleton<StatusQuery>();
|
|
builder.Services.AddSingleton<SyncBackgroundService>();
|
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<SyncBackgroundService>());
|
|
|
|
// ---- auth: humans arrive through the Authentik outpost, machines with an API key ---------
|
|
builder.Services.AddAuthentication(ProxyHeaderAuthenticationHandler.SchemeName)
|
|
.AddScheme<AuthenticationSchemeOptions, ProxyHeaderAuthenticationHandler>(ProxyHeaderAuthenticationHandler.SchemeName, null)
|
|
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(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<App>().AddInteractiveServerRenderMode();
|
|
|
|
app.Run();
|
|
|
|
public partial class Program;
|