feat: Ingestion Pipeline Stabilization and WASM Service Proxies #42
@@ -21,4 +21,18 @@ public interface ISyncBroadcaster
|
||||
DateTime timestamp,
|
||||
string? excludedConnectionId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts real-time ingestion status updates to a specific user.
|
||||
/// This is used by background workers to provide feedback during AI-intensive processing.
|
||||
/// </summary>
|
||||
/// <param name="userId">The ID of the user who owns the ingestion request.</param>
|
||||
/// <param name="message">A human-readable status message (e.g., "Parsing chapters...").</param>
|
||||
/// <param name="progress">Progress percentage (0.0 to 1.0).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task BroadcastIngestionProgressAsync(
|
||||
string userId,
|
||||
string message,
|
||||
double progress,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ public class Ebook
|
||||
public string? LastChapter { get; set; }
|
||||
|
||||
public int LastChapterIndex { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the ebook has been processed by the AI ingestion engine
|
||||
/// and is ready for reading (Knowledge Units generated).
|
||||
/// </summary>
|
||||
public bool IsReadyForReading { get; set; } = false;
|
||||
|
mjasin marked this conversation as resolved
|
||||
|
||||
// Relationship to NexusUser
|
||||
[Required]
|
||||
|
||||
@@ -31,7 +31,8 @@ public static class DependencyInjection
|
||||
if (!string.IsNullOrEmpty(pgConnectionString))
|
||||
{
|
||||
services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseNpgsql(pgConnectionString, x => x.UseVector()));
|
||||
options.UseNpgsql(pgConnectionString, x => x.UseVector()),
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
// Also register a scoped DbContext for repositories that need it
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
@@ -41,7 +42,8 @@ public static class DependencyInjection
|
||||
{
|
||||
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
|
||||
services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseSqlite(sqliteConnectionString));
|
||||
options.UseSqlite(sqliteConnectionString),
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseSqlite(sqliteConnectionString));
|
||||
|
||||
@@ -21,9 +21,14 @@ internal sealed class EbookRepository : IEbookRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EF.Functions.ILike is PostgreSQL-specific; fall back to ToLower for SQLite.
|
||||
// The Author table is expected to be small, so the full scan is acceptable.
|
||||
// TODO: Add a case-insensitive collation to the Author.Name column in a future migration.
|
||||
// Use PostgreSQL ILike for case-insensitive searching if on Npgsql provider,
|
||||
// otherwise fallback to string comparison.
|
||||
if (_context.Database.IsNpgsql())
|
||||
{
|
||||
return await _context.Authors
|
||||
.FirstOrDefaultAsync(a => EF.Functions.ILike(a.Name, name), cancellationToken);
|
||||
}
|
||||
|
||||
return await _context.Authors
|
||||
.FirstOrDefaultAsync(
|
||||
a => a.Name.ToLower() == name.ToLower(),
|
||||
@@ -34,7 +39,12 @@ internal sealed class EbookRepository : IEbookRepository
|
||||
public void AddAuthor(Author author) => _context.Authors.Add(author);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddEbook(Ebook ebook) => _context.Ebooks.Add(ebook);
|
||||
public void AddEbook(Ebook ebook)
|
||||
{
|
||||
// Explicitly set the readiness flag to false upon addition
|
||||
ebook.IsReadyForReading = false;
|
||||
_context.Ebooks.Add(ebook);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -25,19 +25,37 @@ internal sealed class SignalRSyncBroadcaster : ISyncBroadcaster
|
||||
string? excludedConnectionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var groupName = $"User_{userId}";
|
||||
|
||||
// Using Clients.User(userId) targeted broadcasting.
|
||||
// This pushes to all of a user's connected devices across all sessions.
|
||||
if (!string.IsNullOrEmpty(excludedConnectionId))
|
||||
{
|
||||
await _hubContext.Clients
|
||||
.GroupExcept(groupName, excludedConnectionId)
|
||||
.User(userId)
|
||||
.SendAsync("ProgressUpdated", pageId, timestamp, cancellationToken: cancellationToken);
|
||||
|
||||
// Note: SignalR HubContext doesn't easily support 'Except' when using .User(id)
|
||||
// from outside the Hub itself without custom IUserIdProvider.
|
||||
// If strict exclusion is needed, we'd use groups, but requirements mandate .User(userId).
|
||||
}
|
||||
else
|
||||
{
|
||||
await _hubContext.Clients
|
||||
.Group(groupName)
|
||||
.User(userId)
|
||||
.SendAsync("ProgressUpdated", pageId, timestamp, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task BroadcastIngestionProgressAsync(
|
||||
string userId,
|
||||
string message,
|
||||
double progress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Pushes ingestion status (e.g., "Parsing chapters...") and progress (0.0-1.0)
|
||||
// directly to the user's active session components (like BookIngestionModal).
|
||||
await _hubContext.Clients
|
||||
.User(userId)
|
||||
.SendAsync("IngestionProgress", message, progress, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ using NexusReader.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using NexusReader.Data.Persistence;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
using NexusReader.Domain.Entities;
|
||||
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
@@ -42,10 +45,12 @@ builder.Services.AddHttpClient("NexusAPI", client =>
|
||||
|
||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
||||
|
||||
// Dummy registrations for server-only handlers to satisfy DI validation
|
||||
// Real WASM implementations for application abstractions
|
||||
builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory());
|
||||
builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(new ThrowingEmbeddingGenerator());
|
||||
builder.Services.AddSingleton<IBookStorageService>(new ThrowingBookStorageService());
|
||||
builder.Services.AddScoped<IEmbeddingGenerator<string, Embedding<float>>, WasmEmbeddingGenerator>();
|
||||
builder.Services.AddScoped<IBookStorageService, WasmBookStorageService>();
|
||||
builder.Services.AddScoped<IEbookRepository, WasmEbookRepository>();
|
||||
|
mjasin marked this conversation as resolved
Outdated
Antigravity
commented
Architectural Violation (Clean Architecture / CQRS): Building 'Wasm' proxies for Infrastructure interfaces ( Architectural Violation (Clean Architecture / CQRS): Building 'Wasm' proxies for Infrastructure interfaces (`IEbookRepository`, `ISyncBroadcaster`, etc.) in order to satisfy DI for MediatR handlers executing locally on the WASM client is an anti-pattern. MediatR handlers that depend on server-side infrastructure must not be executed directly from client environments. The client should send an HTTP API request (or SignalR message), and the MediatR handler should be executed exclusively on the server.
|
||||
builder.Services.AddScoped<ISyncBroadcaster, WasmSyncBroadcaster>();
|
||||
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddScoped<IEpubReader, WasmEpubReader>();
|
||||
@@ -55,23 +60,5 @@ await builder.Build().RunAsync();
|
||||
|
||||
public class ThrowingDbContextFactory : IDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client.");
|
||||
}
|
||||
|
||||
public class ThrowingEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
||||
{
|
||||
public void Dispose() { }
|
||||
public Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<string> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Embedding generation cannot be used in WASM client.");
|
||||
public object? GetService(Type serviceType, object? serviceKey = null) => null;
|
||||
}
|
||||
|
||||
public class ThrowingBookStorageService : IBookStorageService
|
||||
{
|
||||
private const string ErrorMessage = "File storage operations are not supported in the WASM client. Use the API endpoint for ingestion.";
|
||||
|
||||
public Task<string> SaveEbookAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<string> SaveEbookAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<string?> SaveCoverAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<string?> SaveCoverAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client. Use API proxies for data access.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Net.Http.Json;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmBookStorageService : IBookStorageService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmBookStorageService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<string> SaveEbookAsync(byte[] data, string fileName)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/storage/save/ebook", new { data, fileName });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<StorageResponse>();
|
||||
return result?.Path ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task<string> SaveEbookAsync(Stream data, string fileName)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await data.CopyToAsync(ms);
|
||||
return await SaveEbookAsync(ms.ToArray(), fileName);
|
||||
}
|
||||
|
||||
public async Task<string?> SaveCoverAsync(byte[] data, string fileName)
|
||||
{
|
||||
if (data == null || data.Length == 0) return null;
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/storage/save/cover", new { data, fileName });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<StorageResponse>();
|
||||
return result?.Path;
|
||||
}
|
||||
|
||||
public async Task<string?> SaveCoverAsync(Stream data, string fileName)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await data.CopyToAsync(ms);
|
||||
return await SaveCoverAsync(ms.ToArray(), fileName);
|
||||
}
|
||||
|
||||
private record StorageResponse(string Path);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Net.Http.Json;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Domain.Entities;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmEbookRepository : IEbookRepository
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmEbookRepository(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/repository/author/find", new { name }, cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<Author>(cancellationToken: cancellationToken);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddAuthor(Author author)
|
||||
{
|
||||
// For a repository in WASM, we can't easily do 'void' fire-and-forget Add without a local state.
|
||||
// However, we can either queue it or just do nothing if the caller expects SaveChangesAsync to handle it.
|
||||
// But the common pattern for this app seems to be calling the API.
|
||||
// For now, we'll assume the entity will be sent during SaveChanges or a separate command.
|
||||
// Given the constraints, we'll mark it for later serialization or just throw if not supported.
|
||||
// Better yet: we'll implement a 'Real' enough version that tracks changes locally.
|
||||
_stagedAuthors.Add(author);
|
||||
}
|
||||
|
||||
public void AddEbook(Ebook ebook)
|
||||
{
|
||||
_stagedEbooks.Add(ebook);
|
||||
}
|
||||
|
||||
private readonly List<Author> _stagedAuthors = new();
|
||||
private readonly List<Ebook> _stagedEbooks = new();
|
||||
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
|
mjasin marked this conversation as resolved
Outdated
Antigravity
commented
Transactional Integrity Flaw: Staging entities locally and then saving them across multiple Minimal API endpoints ( Transactional Integrity Flaw: Staging entities locally and then saving them across multiple Minimal API endpoints (`/api/repository/author/add`, `/api/repository/ebook/add`, and `/api/repository/save`) completely destroys Unit of Work and transaction scope. Each API call runs in a separate DI scope, meaning `SaveChangesAsync` on the server will have no tracked entities and will do nothing. The entire operation should be encapsulated in a single backend MediatR command.
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var author in _stagedAuthors)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/repository/author/add", author, cancellationToken);
|
||||
count++;
|
||||
}
|
||||
foreach (var ebook in _stagedEbooks)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/repository/ebook/add", ebook, cancellationToken);
|
||||
count++;
|
||||
}
|
||||
|
||||
_stagedAuthors.Clear();
|
||||
_stagedEbooks.Clear();
|
||||
|
||||
await _httpClient.PostAsync("/api/repository/save", null, cancellationToken);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmEmbeddingGenerator(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
|
||||
IEnumerable<string> values,
|
||||
EmbeddingGenerationOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/ai/embeddings", new { values, options }, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<GeneratedEmbeddings<Embedding<float>>>(cancellationToken: cancellationToken);
|
||||
return result ?? new GeneratedEmbeddings<Embedding<float>>();
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
if (serviceType == typeof(IEmbeddingGenerator<string, Embedding<float>>)) return this;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Net.Http.Json;
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmSyncBroadcaster : ISyncBroadcaster
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmSyncBroadcaster(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task BroadcastProgressAsync(
|
||||
|
mjasin marked this conversation as resolved
Outdated
Antigravity
commented
Redundant Implementation: The WASM client does not and should not broadcast sync progress via HTTP. Redundant Implementation: The WASM client does not and should not broadcast sync progress via HTTP. `SyncService.cs` already correctly uses `HubConnection.SendAsync` to communicate with the `SyncHub` on the server, which then dispatches the `UpdateReadingProgressCommand`. This proxy serves no architectural purpose and is misleading.
|
||||
string userId,
|
||||
string pageId,
|
||||
DateTime timestamp,
|
||||
string? excludedConnectionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/broadcaster/progress", new
|
||||
{
|
||||
userId,
|
||||
pageId,
|
||||
timestamp,
|
||||
excludedConnectionId
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task BroadcastIngestionProgressAsync(
|
||||
string userId,
|
||||
string message,
|
||||
double progress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/broadcaster/ingestion-progress", new
|
||||
{
|
||||
userId,
|
||||
message,
|
||||
progress
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using System.Security.Claims;
|
||||
using NexusReader.Infrastructure.Services;
|
||||
using Stripe;
|
||||
using Microsoft.Extensions.AI;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
|
||||
@@ -250,6 +253,13 @@ app.MapGet("/api/epub/{ebookId:guid}/{index:int}", async (Guid ebookId, int inde
|
||||
return Results.BadRequest(errorMsg);
|
||||
}).RequireAuthorization();
|
||||
|
||||
// Proxy API for AI services (Embeddings)
|
||||
app.MapPost("/api/ai/embeddings", async (EmbeddingsRequest request, IEmbeddingGenerator<string, Embedding<float>> generator) =>
|
||||
{
|
||||
var result = await generator.GenerateAsync(request.Values, request.Options);
|
||||
return Results.Ok(result);
|
||||
}).RequireAuthorization();
|
||||
|
||||
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
||||
|
||||
knowledgeApi.MapPost("/", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
@@ -293,6 +303,63 @@ knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
||||
return Results.BadRequest(errorMsg);
|
||||
});
|
||||
|
||||
// Proxy API for WASM Repository calls
|
||||
var repoApi = app.MapGroup("/api/repository").RequireAuthorization();
|
||||
|
||||
repoApi.MapPost("/author/find", async (AuthorFindRequest request, IEbookRepository repo) =>
|
||||
{
|
||||
var author = await repo.FindAuthorByNameAsync(request.Name);
|
||||
return author != null ? Results.Ok(author) : Results.NotFound();
|
||||
});
|
||||
|
||||
repoApi.MapPost("/author/add", (Author author, IEbookRepository repo) =>
|
||||
{
|
||||
repo.AddAuthor(author);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
repoApi.MapPost("/ebook/add", (Ebook ebook, IEbookRepository repo) =>
|
||||
|
mjasin marked this conversation as resolved
Outdated
Antigravity
commented
Domain Leakage: Exposing raw Domain Entities ( Domain Leakage: Exposing raw Domain Entities (`Ebook`, `Author`) directly as parameters in REST APIs is unsafe and prone to JSON serialization failures (e.g., navigation properties, circular references). Furthermore, these endpoints expose repository CRUD operations instead of adhering to the CQRS pattern via Application Commands.
|
||||
{
|
||||
repo.AddEbook(ebook);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
repoApi.MapPost("/save", async (IEbookRepository repo) =>
|
||||
{
|
||||
await repo.SaveChangesAsync();
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// Proxy API for WASM Broadcaster calls
|
||||
var broadcasterApi = app.MapGroup("/api/broadcaster").RequireAuthorization();
|
||||
|
||||
broadcasterApi.MapPost("/progress", async (BroadcastProgressRequest request, ISyncBroadcaster broadcaster) =>
|
||||
{
|
||||
await broadcaster.BroadcastProgressAsync(request.UserId, request.PageId, request.Timestamp, request.ExcludedConnectionId);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
broadcasterApi.MapPost("/ingestion-progress", async (BroadcastIngestionProgressRequest request, ISyncBroadcaster broadcaster) =>
|
||||
{
|
||||
await broadcaster.BroadcastIngestionProgressAsync(request.UserId, request.Message, request.Progress);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// Proxy API for WASM Storage calls
|
||||
var storageApi = app.MapGroup("/api/storage").RequireAuthorization();
|
||||
|
||||
storageApi.MapPost("/save/ebook", async (StorageRequest request, IBookStorageService storage) =>
|
||||
{
|
||||
var path = await storage.SaveEbookAsync(request.Data, request.FileName);
|
||||
return Results.Ok(new { Path = path });
|
||||
});
|
||||
|
||||
storageApi.MapPost("/save/cover", async (StorageRequest request, IBookStorageService storage) =>
|
||||
{
|
||||
var path = await storage.SaveCoverAsync(request.Data, request.FileName);
|
||||
return Results.Ok(new { Path = path });
|
||||
});
|
||||
|
||||
app.MapPost("/api/library/ingest", async ([FromBody] IngestEbookRequest request, ClaimsPrincipal user, IMediator mediator) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
@@ -518,3 +585,8 @@ app.Run();
|
||||
|
||||
public record KnowledgeRequest(string Text);
|
||||
public record GroundednessRequest(string Answer, string Context);
|
||||
public record AuthorFindRequest(string Name);
|
||||
public record BroadcastProgressRequest(string UserId, string PageId, DateTime Timestamp, string? ExcludedConnectionId);
|
||||
public record BroadcastIngestionProgressRequest(string UserId, string Message, double Progress);
|
||||
public record StorageRequest(byte[] Data, string FileName);
|
||||
public record EmbeddingsRequest(IEnumerable<string> Values, EmbeddingGenerationOptions? Options);
|
||||
|
||||
Missing EF Core Migration: A new property
IsReadyForReadingwas added to theEbookdomain entity, but no corresponding EF Core migration was generated. According to project standards, every change to a Domain entity must be accompanied by a new migration.