feat: implement multi-tenancy support across knowledge services and normalize TenantId to string type.
This commit is contained in:
@@ -5,10 +5,13 @@ namespace NexusReader.Application.Abstractions.Services;
|
||||
|
||||
public interface IKnowledgeService
|
||||
{
|
||||
Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetGraphDataAsync(string text, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetKnowledgeMapAsync(string text, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetSummaryAndQuizAsync(string text, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetGraphDataAsync(string text, string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetKnowledgeMapAsync(string text, string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Result<KnowledgePacket>> GetSummaryAndQuizAsync(string text, string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Result<List<RelevantContext>>> GetRelevantContextAsync(string query, string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Result<GroundednessResult>> VerifyGroundednessAsync(string answer, string context, string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Result> ClearCacheAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record GroundednessResult(float Score, string Rationale, bool IsGrounded);
|
||||
|
||||
@@ -1,51 +1,22 @@
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.AI;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
|
||||
namespace NexusReader.Application.Commands.AI;
|
||||
|
||||
public record VerifyGroundednessCommand(string Answer, string Context) : IRequest<Result<GroundednessResult>>;
|
||||
|
||||
public record GroundednessResult(float Score, string Rationale, bool IsGrounded);
|
||||
public record VerifyGroundednessCommand(string Answer, string Context, string TenantId) : IRequest<Result<GroundednessResult>>;
|
||||
|
||||
public class VerifyGroundednessCommandHandler : IRequestHandler<VerifyGroundednessCommand, Result<GroundednessResult>>
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
private readonly IKnowledgeService _knowledgeService;
|
||||
|
||||
public VerifyGroundednessCommandHandler(IChatClient chatClient)
|
||||
public VerifyGroundednessCommandHandler(IKnowledgeService knowledgeService)
|
||||
{
|
||||
_chatClient = chatClient;
|
||||
_knowledgeService = knowledgeService;
|
||||
}
|
||||
|
||||
public async Task<Result<GroundednessResult>> Handle(VerifyGroundednessCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var systemPrompt = @"
|
||||
You are a Fact-Checking AI. Evaluate if the 'Answer' is supported by the 'Context'.
|
||||
Rate the groundedness from 0.0 to 1.0.
|
||||
Return ONLY a JSON object: { ""score"": 0.9, ""rationale"": ""string"", ""isGrounded"": true }
|
||||
";
|
||||
|
||||
var userPrompt = $"Context: {request.Context}\n\nAnswer: {request.Answer}";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _chatClient.GetResponseAsync(new List<ChatMessage>
|
||||
{
|
||||
new ChatMessage(ChatRole.System, systemPrompt),
|
||||
new ChatMessage(ChatRole.User, userPrompt)
|
||||
}, cancellationToken: cancellationToken);
|
||||
|
||||
var rawJson = response.Text?.Trim() ?? "{}";
|
||||
// Simple cleanup if needed
|
||||
rawJson = rawJson.Replace("```json", "").Replace("```", "").Trim();
|
||||
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<GroundednessResult>(rawJson, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return result != null ? Result.Ok(result) : Result.Fail("Failed to parse groundedness result");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error("Failed to verify groundedness").CausedBy(ex));
|
||||
}
|
||||
return await _knowledgeService.VerifyGroundednessAsync(request.Answer, request.Context, request.TenantId, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,8 @@ public static class DependencyInjection
|
||||
{
|
||||
services.AddMapsterConfiguration();
|
||||
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static System.Reflection.Assembly Assembly => typeof(DependencyInjection).Assembly;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ using NexusReader.Application.Abstractions.Messaging;
|
||||
namespace NexusReader.Application.Queries.Graph;
|
||||
|
||||
/// <param name="Text">Chapter or page content to extract the graph from.</param>
|
||||
public record GetKnowledgeGraphQuery(string Text) : IQuery<GraphDataDto>;
|
||||
/// <param name="TenantId">Tenant scope for knowledge extraction and caching.</param>
|
||||
public record GetKnowledgeGraphQuery(string Text, string TenantId) : IQuery<GraphDataDto>;
|
||||
|
||||
|
||||
@@ -18,20 +18,13 @@ internal sealed class GetKnowledgeGraphQueryHandler : IQueryHandler<GetKnowledge
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
return Result.Ok(new GraphDataDto());
|
||||
|
||||
var result = await _knowledgeService.GetGraphDataAsync(request.Text, cancellationToken);
|
||||
var result = await _knowledgeService.GetGraphDataAsync(request.Text, request.TenantId, cancellationToken);
|
||||
|
||||
if (result.IsFailed)
|
||||
return Result.Fail<GraphDataDto>(result.Errors);
|
||||
|
||||
var graph = result.Value.Graph;
|
||||
|
||||
if (graph is null)
|
||||
return Result.Ok(new GraphDataDto());
|
||||
|
||||
if (graph is null)
|
||||
return Result.Ok(new GraphDataDto());
|
||||
|
||||
return Result.Ok(graph);
|
||||
return graph is null ? Result.Ok(new GraphDataDto()) : Result.Ok(graph);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace NexusReader.Domain.Entities;
|
||||
|
||||
@@ -20,7 +21,9 @@ public class NexusUser : IdentityUser
|
||||
/// <summary>
|
||||
/// Unique identifier for the tenant (SaaS multi-tenancy support).
|
||||
/// </summary>
|
||||
public Guid TenantId { get; set; }
|
||||
[Required]
|
||||
[MaxLength(128)]
|
||||
public string TenantId { get; set; } = "global";
|
||||
|
||||
/// <summary>
|
||||
/// Current subscription plan (e.g., "Free", "Pro", "Enterprise").
|
||||
|
||||
@@ -74,11 +74,13 @@ public static class DependencyInjection
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, ProUserHandler>();
|
||||
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
|
||||
});
|
||||
services.AddScoped<IInfrastructureMarker, InfrastructureMarker>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static System.Reflection.Assembly Assembly => typeof(DependencyInjection).Assembly;
|
||||
}
|
||||
|
||||
public interface IInfrastructureMarker { }
|
||||
internal class InfrastructureMarker : IInfrastructureMarker { }
|
||||
|
||||
@@ -44,7 +44,7 @@ public static class DbInitializer
|
||||
EmailConfirmed = true,
|
||||
CurrentPlan = "Enterprise",
|
||||
AITokenLimit = 1000000,
|
||||
TenantId = Guid.NewGuid()
|
||||
TenantId = Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
var createPowerUser = await userManager.CreateAsync(adminUser, "Admin123!");
|
||||
|
||||
@@ -43,27 +43,27 @@ public class KnowledgeService : IKnowledgeService
|
||||
_tokenizer = TiktokenTokenizer.CreateForModel("gpt-4");
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetKnowledgeInternalAsync(text, PromptRegistry.KnowledgeExtractionSystemPrompt, "full", cancellationToken);
|
||||
return await GetKnowledgeInternalAsync(text, tenantId, PromptRegistry.KnowledgeExtractionSystemPrompt, "full", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetGraphDataAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetGraphDataAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetKnowledgeInternalAsync(text, PromptRegistry.GraphExtractionPrompt, "graph", cancellationToken);
|
||||
return await GetKnowledgeInternalAsync(text, tenantId, PromptRegistry.GraphExtractionPrompt, "graph", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetSummaryAndQuizAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetSummaryAndQuizAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetKnowledgeInternalAsync(text, PromptRegistry.SummaryAndQuizPrompt, "summary_quiz", cancellationToken);
|
||||
return await GetKnowledgeInternalAsync(text, tenantId, PromptRegistry.SummaryAndQuizPrompt, "summary_quiz", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeMapAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeMapAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetKnowledgeInternalAsync(text, PromptRegistry.KM_ExtractionPrompt, "km_map", cancellationToken);
|
||||
return await GetKnowledgeInternalAsync(text, tenantId, PromptRegistry.KM_ExtractionPrompt, "km_map", cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result<KnowledgePacket>> GetKnowledgeInternalAsync(string text, string systemPrompt, string cacheSuffix, CancellationToken cancellationToken)
|
||||
private async Task<Result<KnowledgePacket>> GetKnowledgeInternalAsync(string text, string tenantId, string systemPrompt, string cacheSuffix, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
@@ -84,7 +84,7 @@ public class KnowledgeService : IKnowledgeService
|
||||
|
||||
// 1. Check Cache
|
||||
var cached = await _dbContext.SemanticKnowledgeCache
|
||||
.FirstOrDefaultAsync(c => c.ContentHash == hash && c.PromptVersion == PromptVersion, cancellationToken);
|
||||
.FirstOrDefaultAsync(c => c.ContentHash == hash && c.TenantId == tenantId && c.PromptVersion == PromptVersion, cancellationToken);
|
||||
|
||||
if (cached != null)
|
||||
{
|
||||
@@ -146,7 +146,7 @@ public class KnowledgeService : IKnowledgeService
|
||||
OriginalText = normalizedText,
|
||||
ModelId = _settings.Model,
|
||||
PromptVersion = PromptVersion,
|
||||
TenantId = "global", // Default for shared cache, should be overridden by caller context if possible
|
||||
TenantId = tenantId,
|
||||
Vector = vector,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
@@ -163,7 +163,7 @@ public class KnowledgeService : IKnowledgeService
|
||||
// 5. Process KM-RAG Units and Links if present
|
||||
if (knowledgePacket.Units.Any())
|
||||
{
|
||||
await ProcessKnowledgeUnitsAsync(knowledgePacket, "global", cancellationToken);
|
||||
await ProcessKnowledgeUnitsAsync(knowledgePacket, tenantId, cancellationToken);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
@@ -191,7 +191,7 @@ public class KnowledgeService : IKnowledgeService
|
||||
var unit = existing ?? new KnowledgeUnit { Id = unitId, TenantId = tenantId };
|
||||
unit.Type = Enum.TryParse<NexusReader.Domain.Enums.KnowledgeUnitType>(unitDto.Type, true, out var type) ? type : NexusReader.Domain.Enums.KnowledgeUnitType.Snippet;
|
||||
unit.Content = unitDto.Content;
|
||||
unit.SourceId = "extracted"; // Should be passed from context
|
||||
unit.SourceId = "extracted";
|
||||
unit.MetadataJson = JsonSerializer.Serialize(unitDto.Metadata);
|
||||
|
||||
// Generate unit-specific embedding for granular retrieval
|
||||
@@ -217,6 +217,44 @@ public class KnowledgeService : IKnowledgeService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<GroundednessResult>> VerifyGroundednessAsync(string answer, string context, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var systemPrompt = @"
|
||||
You are a Fact-Checking AI. Evaluate if the 'Answer' is supported by the 'Context'.
|
||||
Rate the groundedness from 0.0 to 1.0.
|
||||
Return ONLY a JSON object: { ""score"": 0.9, ""rationale"": ""string"", ""isGrounded"": true }
|
||||
";
|
||||
|
||||
var userPrompt = $"Context: {context}\n\nAnswer: {answer}";
|
||||
|
||||
try
|
||||
{
|
||||
var options = new ChatOptions
|
||||
{
|
||||
Temperature = 0.0f, // Low temperature for factual checks
|
||||
MaxOutputTokens = 500
|
||||
};
|
||||
|
||||
var response = await _retryPipeline.ExecuteAsync(async ct =>
|
||||
await _chatClient.GetResponseAsync(new List<ChatMessage>
|
||||
{
|
||||
new ChatMessage(ChatRole.System, systemPrompt),
|
||||
new ChatMessage(ChatRole.User, userPrompt)
|
||||
}, options, cancellationToken: ct), cancellationToken);
|
||||
|
||||
var rawJson = response.Text?.Trim() ?? "{}";
|
||||
rawJson = rawJson.Replace("```json", "").Replace("```", "").Trim();
|
||||
|
||||
var result = JsonSerializer.Deserialize<GroundednessResult>(rawJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return result != null ? Result.Ok(result) : Result.Fail("Failed to parse groundedness result");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error("Failed to verify groundedness").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<RelevantContext>>> GetRelevantContextAsync(string query, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return Result.Fail("Query is empty.");
|
||||
|
||||
@@ -49,6 +49,10 @@ public static class MauiProgram
|
||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
||||
|
||||
builder.Services.AddApplication();
|
||||
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
|
||||
NexusReader.Application.DependencyInjection.Assembly
|
||||
));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
@using MediatR
|
||||
@using NexusReader.Application.Commands.AI
|
||||
@using NexusReader.Application.Abstractions.Services
|
||||
@using NexusReader.UI.Shared.Components.Atoms
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inject IMediator Mediator
|
||||
@inject AuthenticationStateProvider AuthProvider
|
||||
|
||||
<div class="groundedness-badge @GetStatusClass()" title="@_result?.Rationale">
|
||||
@if (_isChecking)
|
||||
@@ -24,16 +27,29 @@
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.groundedness-badge.status-high { color: var(--nexus-neon); border-color: var(--nexus-neon); }
|
||||
.groundedness-badge.status-medium { color: #ffaa00; border-color: #ffaa00; }
|
||||
.groundedness-badge.status-low { color: #ff4444; border-color: #ff4444; }
|
||||
.groundedness-badge.status-high {
|
||||
color: var(--nexus-neon);
|
||||
border-color: var(--nexus-neon);
|
||||
}
|
||||
|
||||
.shimmer { opacity: 0.6; }
|
||||
.groundedness-badge.status-medium {
|
||||
color: #ffaa00;
|
||||
border-color: #ffaa00;
|
||||
}
|
||||
|
||||
.groundedness-badge.status-low {
|
||||
color: #ff4444;
|
||||
border-color: #ff4444;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
@@ -56,7 +72,10 @@
|
||||
_isChecking = true;
|
||||
StateHasChanged();
|
||||
|
||||
var res = await Mediator.Send(new VerifyGroundednessCommand(Answer, Context));
|
||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
||||
var tenantId = authState.User.FindFirst("TenantId")?.Value ?? "global";
|
||||
|
||||
var res = await Mediator.Send(new VerifyGroundednessCommand(Answer, Context, tenantId));
|
||||
if (res.IsSuccess)
|
||||
{
|
||||
_result = res.Value;
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class KnowledgeCoordinator : IDisposable
|
||||
_interactionService.RequestHighlightBlock(nodeId);
|
||||
}
|
||||
|
||||
public async Task ProcessFullPageAsync(string fullContent)
|
||||
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fullContent)) return;
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class KnowledgeCoordinator : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _knowledgeService.GetGraphDataAsync(fullContent);
|
||||
var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var packet = result.Value;
|
||||
@@ -73,12 +73,12 @@ public sealed class KnowledgeCoordinator : IDisposable
|
||||
_graphService.SetActiveNode(blockId);
|
||||
}
|
||||
|
||||
public async Task<KnowledgePacket?> RequestSummaryAndQuizAsync(string content)
|
||||
public async Task<KnowledgePacket?> RequestSummaryAndQuizAsync(string content, string tenantId = "global")
|
||||
{
|
||||
_quizService.SetHydrating(true);
|
||||
try
|
||||
{
|
||||
var result = await _knowledgeService.GetSummaryAndQuizAsync(content);
|
||||
var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var packet = result.Value;
|
||||
|
||||
@@ -14,22 +14,22 @@ public class WasmKnowledgeService : IKnowledgeService
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await CallKnowledgeApiAsync("/api/knowledge", text, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetGraphDataAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetGraphDataAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await CallKnowledgeApiAsync("/api/knowledge/graph", text, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeMapAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetKnowledgeMapAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await CallKnowledgeApiAsync("/api/knowledge/map", text, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result<KnowledgePacket>> GetSummaryAndQuizAsync(string text, CancellationToken cancellationToken = default)
|
||||
public async Task<Result<KnowledgePacket>> GetSummaryAndQuizAsync(string text, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await CallKnowledgeApiAsync("/api/knowledge/summary", text, cancellationToken);
|
||||
}
|
||||
@@ -53,6 +53,25 @@ public class WasmKnowledgeService : IKnowledgeService
|
||||
return Result.Fail(new Error($"Network error: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
public async Task<Result<GroundednessResult>> VerifyGroundednessAsync(string answer, string context, string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/knowledge/verify-groundedness", new { answer, context, tenantId }, cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<GroundednessResult>(cancellationToken: cancellationToken);
|
||||
return result != null ? Result.Ok(result) : Result.Fail("Failed to deserialize groundedness result.");
|
||||
}
|
||||
|
||||
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return Result.Fail($"Server error ({response.StatusCode}): {errorBody}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error($"Network error: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Result<KnowledgePacket>> CallKnowledgeApiAsync(string endpoint, string text, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,11 @@ builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
|
||||
NexusReader.Application.DependencyInjection.Assembly,
|
||||
NexusReader.Infrastructure.DependencyInjection.Assembly
|
||||
));
|
||||
|
||||
// Authorization Policies
|
||||
builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
@@ -114,6 +119,16 @@ builder.Services.Configure<IdentityOptions>(options =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Startup Validation
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var marker = scope.ServiceProvider.GetService<IInfrastructureMarker>();
|
||||
if (marker == null)
|
||||
{
|
||||
throw new InvalidOperationException("CRITICAL: Infrastructure layer was not registered. Ensure AddInfrastructure() is called in Program.cs.");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Database is initialized and seeded
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
@@ -198,27 +213,40 @@ app.MapGet("/api/epub/{index}", async (int index, IEpubService epubService) =>
|
||||
|
||||
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
||||
|
||||
knowledgeApi.MapPost("/", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
||||
knowledgeApi.MapPost("/", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var result = await knowledgeService.GetKnowledgeAsync(request.Text);
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.GetKnowledgeAsync(request.Text, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapPost("/graph", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
||||
knowledgeApi.MapPost("/graph", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var result = await knowledgeService.GetGraphDataAsync(request.Text);
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.GetGraphDataAsync(request.Text, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapPost("/summary", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
||||
knowledgeApi.MapPost("/summary", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text);
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapPost("/verify-groundedness", async (GroundednessRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.VerifyGroundednessAsync(request.Answer, request.Context, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
|
||||
|
||||
knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var result = await knowledgeService.ClearCacheAsync();
|
||||
@@ -377,3 +405,4 @@ app.MapRazorComponents<App>()
|
||||
app.Run();
|
||||
|
||||
public record KnowledgeRequest(string Text);
|
||||
public record GroundednessRequest(string Answer, string Context);
|
||||
|
||||
Reference in New Issue
Block a user