From 89b242f97e87b3da29f8ac1c225897c3dd5e9d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=B4=E5=BB=B7=E6=9D=B0?= <2200150071@qq.com> Date: Tue, 5 Aug 2025 09:16:01 +0800 Subject: [PATCH 01/31] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/Program.cs | 44 ++++++++++++++++++ .../RAG.Api/Properties/launchSettings.json | 41 +++++++++++++++++ backend/RAG.Api/RAG.Api.csproj | 27 +++++++++++ backend/RAG.Api/RAG.Api.http | 6 +++ backend/RAG.Api/appsettings.json | 9 ++++ backend/RAG.Application/Class1.cs | 6 +++ .../RAG.Application/RAG.Application.csproj | 19 ++++++++ backend/RAG.Domain/Class1.cs | 6 +++ backend/RAG.Domain/RAG.Domain.csproj | 9 ++++ backend/RAG.Infrastructure/Class1.cs | 6 +++ .../RAG.Infrastructure.csproj | 22 +++++++++ backend/RAG.Tests/RAG.Tests.csproj | 29 ++++++++++++ backend/RAG.Tests/UnitTest1.cs | 10 ++++ backend/RAG.sln | 46 +++++++++++++++++++ backend/txt.txt | 0 frontend/txt.txt | 0 16 files changed, 280 insertions(+) create mode 100644 backend/RAG.Api/Program.cs create mode 100644 backend/RAG.Api/Properties/launchSettings.json create mode 100644 backend/RAG.Api/RAG.Api.csproj create mode 100644 backend/RAG.Api/RAG.Api.http create mode 100644 backend/RAG.Api/appsettings.json create mode 100644 backend/RAG.Application/Class1.cs create mode 100644 backend/RAG.Application/RAG.Application.csproj create mode 100644 backend/RAG.Domain/Class1.cs create mode 100644 backend/RAG.Domain/RAG.Domain.csproj create mode 100644 backend/RAG.Infrastructure/Class1.cs create mode 100644 backend/RAG.Infrastructure/RAG.Infrastructure.csproj create mode 100644 backend/RAG.Tests/RAG.Tests.csproj create mode 100644 backend/RAG.Tests/UnitTest1.cs create mode 100644 backend/RAG.sln delete mode 100644 backend/txt.txt delete mode 100644 frontend/txt.txt diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs new file mode 100644 index 0000000..00ff539 --- /dev/null +++ b/backend/RAG.Api/Program.cs @@ -0,0 +1,44 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast") +.WithOpenApi(); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/backend/RAG.Api/Properties/launchSettings.json b/backend/RAG.Api/Properties/launchSettings.json new file mode 100644 index 0000000..e02ba24 --- /dev/null +++ b/backend/RAG.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:27975", + "sslPort": 44389 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7158;http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/RAG.Api/RAG.Api.csproj b/backend/RAG.Api/RAG.Api.csproj new file mode 100644 index 0000000..2db79c4 --- /dev/null +++ b/backend/RAG.Api/RAG.Api.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http new file mode 100644 index 0000000..28f4958 --- /dev/null +++ b/backend/RAG.Api/RAG.Api.http @@ -0,0 +1,6 @@ +@RAG.Api_HostAddress = http://localhost:5128 + +GET {{RAG.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/backend/RAG.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/backend/RAG.Application/Class1.cs b/backend/RAG.Application/Class1.cs new file mode 100644 index 0000000..973f93a --- /dev/null +++ b/backend/RAG.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace RAG.Application; + +public class Class1 +{ + +} diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj new file mode 100644 index 0000000..7d7b78b --- /dev/null +++ b/backend/RAG.Application/RAG.Application.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/backend/RAG.Domain/Class1.cs b/backend/RAG.Domain/Class1.cs new file mode 100644 index 0000000..43b53cf --- /dev/null +++ b/backend/RAG.Domain/Class1.cs @@ -0,0 +1,6 @@ +namespace RAG.Domain; + +public class Class1 +{ + +} diff --git a/backend/RAG.Domain/RAG.Domain.csproj b/backend/RAG.Domain/RAG.Domain.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/backend/RAG.Domain/RAG.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/RAG.Infrastructure/Class1.cs b/backend/RAG.Infrastructure/Class1.cs new file mode 100644 index 0000000..cfa7d06 --- /dev/null +++ b/backend/RAG.Infrastructure/Class1.cs @@ -0,0 +1,6 @@ +namespace RAG.Infrastructure; + +public class Class1 +{ + +} diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj new file mode 100644 index 0000000..bcf0485 --- /dev/null +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/backend/RAG.Tests/RAG.Tests.csproj b/backend/RAG.Tests/RAG.Tests.csproj new file mode 100644 index 0000000..5d9e2d3 --- /dev/null +++ b/backend/RAG.Tests/RAG.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/backend/RAG.Tests/UnitTest1.cs b/backend/RAG.Tests/UnitTest1.cs new file mode 100644 index 0000000..0c054e8 --- /dev/null +++ b/backend/RAG.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace RAG.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/backend/RAG.sln b/backend/RAG.sln new file mode 100644 index 0000000..f53c424 --- /dev/null +++ b/backend/RAG.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Api", "RAG.Api\RAG.Api.csproj", "{3DAC1A57-A784-43CD-B665-68896913F082}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Application", "RAG.Application\RAG.Application.csproj", "{FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Domain", "RAG.Domain\RAG.Domain.csproj", "{2937A929-EB24-43B6-8BD9-EEA32B895C9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Infrastructure", "RAG.Infrastructure\RAG.Infrastructure.csproj", "{46C0164B-CF85-4CA1-95A6-16DDC05476CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Tests", "RAG.Tests\RAG.Tests.csproj", "{994D11A9-A40A-4FC2-A7FB-BB366380EEF2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3DAC1A57-A784-43CD-B665-68896913F082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DAC1A57-A784-43CD-B665-68896913F082}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DAC1A57-A784-43CD-B665-68896913F082}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DAC1A57-A784-43CD-B665-68896913F082}.Release|Any CPU.Build.0 = Release|Any CPU + {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Release|Any CPU.Build.0 = Release|Any CPU + {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Release|Any CPU.Build.0 = Release|Any CPU + {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Release|Any CPU.Build.0 = Release|Any CPU + {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/backend/txt.txt b/backend/txt.txt deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/txt.txt b/frontend/txt.txt deleted file mode 100644 index e69de29..0000000 -- Gitee From c3468a958a004568ddecc3f9e201fae7ad0d43de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Tue, 5 Aug 2025 09:45:14 +0800 Subject: [PATCH 02/31] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=EF=BC=8C=E5=B9=B6=E5=AE=8C=E6=88=90=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E4=B9=8B=E9=97=B4=E7=9A=84=E4=BE=9D=E8=B5=96=EF=BC=8C?= =?UTF-8?q?=E5=B7=B2=E5=AE=8C=E6=88=90=E5=AE=9E=E4=BD=93=E7=B1=BB=E5=88=9B?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/Controllers/ChatController.cs | 89 +++++++++++ .../RAG.Api/Controllers/DocumentController.cs | 63 ++++++++ backend/RAG.Api/Program.cs | 11 +- .../RAG.Api/Properties/launchSettings.json | 28 +--- backend/RAG.Api/RAG.Api.csproj | 13 +- backend/RAG.Api/RAG.Api.http | 2 +- .../RAG.Application/RAG.Application.csproj | 8 +- backend/RAG.Domain/Entities/ChatHistory.cs | 37 +++++ backend/RAG.Domain/Entities/Document.cs | 45 ++++++ backend/RAG.Domain/Entities/User.cs | 19 +++ backend/RAG.Domain/RAG.Domain.csproj | 2 +- .../RAG.Domain/Repositories/IVectorStore.cs | 20 +++ .../Services/IDocumentProcessingService.cs | 13 ++ .../Data/ApplicationDbContext.cs | 26 ++++ .../RAG.Infrastructure.csproj | 10 +- .../Services/DocumentProcessingService.cs | 143 ++++++++++++++++++ .../VectorStore/PgVectorStore.cs | 109 +++++++++++++ backend/RAG.Tests/RAG.Tests.csproj | 14 +- backend/RAG.Tests/UnitTest1.cs | 4 +- backend/RAG.sln | 98 ++++++++---- backend/txt.txt | 0 21 files changed, 658 insertions(+), 96 deletions(-) create mode 100644 backend/RAG.Api/Controllers/ChatController.cs create mode 100644 backend/RAG.Api/Controllers/DocumentController.cs create mode 100644 backend/RAG.Domain/Entities/ChatHistory.cs create mode 100644 backend/RAG.Domain/Entities/Document.cs create mode 100644 backend/RAG.Domain/Entities/User.cs create mode 100644 backend/RAG.Domain/Repositories/IVectorStore.cs create mode 100644 backend/RAG.Domain/Services/IDocumentProcessingService.cs create mode 100644 backend/RAG.Infrastructure/Data/ApplicationDbContext.cs create mode 100644 backend/RAG.Infrastructure/Services/DocumentProcessingService.cs create mode 100644 backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs create mode 100644 backend/txt.txt diff --git a/backend/RAG.Api/Controllers/ChatController.cs b/backend/RAG.Api/Controllers/ChatController.cs new file mode 100644 index 0000000..1723df4 --- /dev/null +++ b/backend/RAG.Api/Controllers/ChatController.cs @@ -0,0 +1,89 @@ +// using Microsoft.AspNetCore.Mvc; +// using System.Threading.Tasks; +// using RAG.Domain.Repositories; +// using RAG.Domain.Entities; +// using Microsoft.Extensions.Logging; +// using System.Collections.Generic; +// using System.Linq; + +// namespace RAG.Api.Controllers +// { +// [ApiController] +// [Route("api/[controller]")] +// public class ChatController : ControllerBase +// { +// private readonly IVectorStore _vectorStore; +// private readonly ILogger _logger; + +// public ChatController( +// IVectorStore vectorStore, +// ILogger logger) +// { +// _vectorStore = vectorStore; +// _logger = logger; +// } + +// [HttpPost("query")] +// public async Task Query([FromBody] ChatQuery query) +// { +// try +// { +// if (string.IsNullOrEmpty(query.Message)) +// { +// return BadRequest("Message cannot be empty."); +// } + +// _logger.LogInformation("Processing chat query: {Message}", query.Message); + +// // In a real implementation, you would generate an embedding for the query +// // var queryEmbedding = await GenerateEmbeddingAsync(query.Message); + +// // For demonstration purposes, create a dummy embedding +// var queryEmbedding = new float[1536]; // Match the vector dimension used in PgVectorStore + +// // Search for similar vectors +// var results = await _vectorStore.SearchVectorsAsync(queryEmbedding, query.TopK ?? 5); + +// // In a real implementation, you would use these results to generate a response +// // using a language model +// var response = new ChatResponse +// { +// Query = query.Message, +// Response = "This is a placeholder response generated based on retrieved context.", +// References = results.Select(r => new Reference +// { +// Content = r.Content.Substring(0, Math.Min(100, r.Content.Length)) + "...", +// Similarity = r.Similarity +// }).ToList() +// }; + +// return Ok(response); +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Error processing chat query"); +// return StatusCode(500, "Internal server error"); +// } +// } +// } + +// public class ChatQuery +// { +// public string Message { get; set; } +// public int? TopK { get; set; } +// public string SessionId { get; set; } +// } + +// public class ChatResponse +// { +// public string Query { get; set; } +// public string Response { get; set; } +// public List References { get; set; } +// } + +// public class Reference +// { +// public string Content { get; set; } +// public double Similarity { get; set; } +// } +// } \ No newline at end of file diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs new file mode 100644 index 0000000..1e4540c --- /dev/null +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -0,0 +1,63 @@ +// using Microsoft.AspNetCore.Mvc; +// using Microsoft.AspNetCore.Http; +// using System.Threading.Tasks; +// using RAG.Domain.Services; +// using RAG.Domain.Entities; +// using Microsoft.Extensions.Logging; + +// namespace RAG.Api.Controllers +// { +// [ApiController] +// [Route("api/[controller]")] +// public class DocumentController : ControllerBase +// { +// private readonly IDocumentProcessingService _documentProcessingService; +// private readonly ILogger _logger; + +// public DocumentController( +// IDocumentProcessingService documentProcessingService, +// ILogger logger) +// { +// _documentProcessingService = documentProcessingService; +// _logger = logger; +// } + +// [HttpPost("upload")] +// public async Task UploadDocument(IFormFile file) +// { +// try +// { +// if (file == null || file.Length == 0) +// { +// return BadRequest("No file uploaded."); +// } + +// // In a real application, you would get the user ID from authentication +// var userId = 1; // Placeholder + +// using (var stream = file.OpenReadStream()) +// { +// var document = await _documentProcessingService.ProcessDocumentAsync( +// stream, +// file.FileName, +// file.ContentType, +// userId); + +// return Ok(new { document.Id, document.Title, document.FileType, document.CreatedAt }); +// } +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Error uploading document"); +// return StatusCode(500, "Internal server error"); +// } +// } + +// [HttpGet("supported-types")] +// public IActionResult GetSupportedFileTypes() +// { +// var supportedTypes = new[] { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; +// return Ok(supportedTypes); +// } +// } +// } \ No newline at end of file diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs index 00ff539..ee9d65d 100644 --- a/backend/RAG.Api/Program.cs +++ b/backend/RAG.Api/Program.cs @@ -1,17 +1,15 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.MapOpenApi(); } app.UseHttpsRedirection(); @@ -33,8 +31,7 @@ app.MapGet("/weatherforecast", () => .ToArray(); return forecast; }) -.WithName("GetWeatherForecast") -.WithOpenApi(); +.WithName("GetWeatherForecast"); app.Run(); diff --git a/backend/RAG.Api/Properties/launchSettings.json b/backend/RAG.Api/Properties/launchSettings.json index e02ba24..8749fc5 100644 --- a/backend/RAG.Api/Properties/launchSettings.json +++ b/backend/RAG.Api/Properties/launchSettings.json @@ -1,20 +1,11 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:27975", - "sslPort": 44389 - } - }, + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5128", + "launchBrowser": false, + "applicationUrl": "http://localhost:5097", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,17 +13,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7158;http://localhost:5128", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", + "launchBrowser": false, + "applicationUrl": "https://localhost:7117;http://localhost:5097", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/backend/RAG.Api/RAG.Api.csproj b/backend/RAG.Api/RAG.Api.csproj index 2db79c4..b4c5283 100644 --- a/backend/RAG.Api/RAG.Api.csproj +++ b/backend/RAG.Api/RAG.Api.csproj @@ -1,22 +1,13 @@ - net8.0 + net9.0 enable enable - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - + diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 28f4958..df87c00 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -1,4 +1,4 @@ -@RAG.Api_HostAddress = http://localhost:5128 +@RAG.Api_HostAddress = http://localhost:5097 GET {{RAG.Api_HostAddress}}/weatherforecast/ Accept: application/json diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj index 7d7b78b..8c0b9f3 100644 --- a/backend/RAG.Application/RAG.Application.csproj +++ b/backend/RAG.Application/RAG.Application.csproj @@ -4,14 +4,8 @@ - - - - - - - net8.0 + net9.0 enable enable diff --git a/backend/RAG.Domain/Entities/ChatHistory.cs b/backend/RAG.Domain/Entities/ChatHistory.cs new file mode 100644 index 0000000..5c7c5bd --- /dev/null +++ b/backend/RAG.Domain/Entities/ChatHistory.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace RAG.Domain.Entities +{ + public class ChatHistory + { + public int Id { get; set; } + public string SessionId { get; set; } + public int UserId { get; set; } + public User User { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public ICollection Messages { get; set; } = new List(); + } + + public class ChatMessage + { + public int Id { get; set; } + public int ChatHistoryId { get; set; } + public ChatHistory ChatHistory { get; set; } + public string Content { get; set; } + public bool IsUserMessage { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public ICollection References { get; set; } = new List(); + } + + public class MessageReference + { + public int Id { get; set; } + public int ChatMessageId { get; set; } + public ChatMessage ChatMessage { get; set; } + public int DocumentChunkId { get; set; } + public DocumentChunk DocumentChunk { get; set; } + public double RelevanceScore { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs new file mode 100644 index 0000000..771f36f --- /dev/null +++ b/backend/RAG.Domain/Entities/Document.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace RAG.Domain.Entities +{ + public class Document + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public string FilePath { get; set; } + public string FileName { get; set; } + public string FileType { get; set; } + public long FileSize { get; set; } + public string Description { get; set; } + public string Tags { get; set; } + public string AccessLevel { get; set; } = "internal"; // 'internal' or 'public' + public string Status { get; set; } = "pending"; // 'pending', 'processing', 'completed', 'failed' + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public int UserId { get; set; } + public User User { get; set; } + public ICollection Chunks { get; set; } = new List(); + } + + public class DocumentChunk + { + public int Id { get; set; } + public int DocumentId { get; set; } + public Document Document { get; set; } + public string Content { get; set; } + public int Position { get; set; } + public int TokenCount { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } + + public class Vector + { + public int Id { get; set; } + public int ChunkId { get; set; } + public DocumentChunk Chunk { get; set; } + public float[] Embedding { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/User.cs b/backend/RAG.Domain/Entities/User.cs new file mode 100644 index 0000000..b7787a3 --- /dev/null +++ b/backend/RAG.Domain/Entities/User.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace RAG.Domain.Entities +{ + public class User + { + public int Id { get; set; } + public string Username { get; set; } + public string Email { get; set; } + public string PasswordHash { get; set; } + public string FullName { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public bool IsActive { get; set; } = true; + public ICollection Documents { get; set; } = new List(); + public ICollection ChatHistories { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/RAG.Domain.csproj b/backend/RAG.Domain/RAG.Domain.csproj index fa71b7a..125f4c9 100644 --- a/backend/RAG.Domain/RAG.Domain.csproj +++ b/backend/RAG.Domain/RAG.Domain.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/backend/RAG.Domain/Repositories/IVectorStore.cs b/backend/RAG.Domain/Repositories/IVectorStore.cs new file mode 100644 index 0000000..3dfb6b4 --- /dev/null +++ b/backend/RAG.Domain/Repositories/IVectorStore.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RAG.Domain.Repositories +{ + public interface IVectorStore + { + Task InitializeAsync(); + Task StoreVectorAsync(string content, float[] embedding, object metadata = null); + Task> SearchVectorsAsync(float[] queryVector, int topK = 5); + } + + public class VectorResult + { + public int Id { get; set; } + public string Content { get; set; } + public Dictionary Metadata { get; set; } + public double Similarity { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Services/IDocumentProcessingService.cs b/backend/RAG.Domain/Services/IDocumentProcessingService.cs new file mode 100644 index 0000000..5ffcc6d --- /dev/null +++ b/backend/RAG.Domain/Services/IDocumentProcessingService.cs @@ -0,0 +1,13 @@ +using System.IO; +using System.Threading.Tasks; +using RAG.Domain.Entities; + +namespace RAG.Domain.Services +{ + public interface IDocumentProcessingService + { + Task ProcessDocumentAsync(Stream fileStream, string fileName, string contentType, int userId); + bool SupportsFileType(string fileExtension); + Task ExtractTextFromDocumentAsync(Stream fileStream, string fileExtension); + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..5f21a8b --- /dev/null +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -0,0 +1,26 @@ +// using Microsoft.EntityFrameworkCore; +// using RAG.Domain.Entities; + +// namespace RAG.Infrastructure.Data +// { +// public class ApplicationDbContext : DbContext +// { +// public ApplicationDbContext(DbContextOptions options) +// : base(options) +// {} + +// // DbSets for your entities +// public DbSet Documents { get; set; } +// public DbSet ChatHistories { get; set; } +// public DbSet Users { get; set; } + +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// base.OnModelCreating(modelBuilder); + +// // Configure entity mappings if needed +// modelBuilder.Entity() +// .HasIndex(d => d.Title); +// } +// } +// } \ No newline at end of file diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index bcf0485..2b89d02 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -5,16 +5,8 @@ - - - - - - - - - net8.0 + net9.0 enable enable diff --git a/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs new file mode 100644 index 0000000..5ab2d03 --- /dev/null +++ b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs @@ -0,0 +1,143 @@ +// using System; +// using System.Collections.Generic; +// using System.IO; +// using System.Linq; +// using System.Threading.Tasks; +// using iTextSharp.text.pdf; +// using iTextSharp.text.pdf.parser; +// using Microsoft.Extensions.Logging; +// using Microsoft.Extensions.Configuration; +// using RAG.Domain.Entities; +// using RAG.Domain.Repositories; +// using RAG.Domain.Services; + +// namespace RAG.Infrastructure.Services +// { +// public class DocumentProcessingService : IDocumentProcessingService +// { +// private readonly ILogger _logger; +// private readonly IVectorStore _vectorStore; +// private readonly IConfiguration _configuration; + +// public DocumentProcessingService( +// ILogger logger, +// IVectorStore vectorStore, +// IConfiguration configuration) +// { +// _logger = logger; +// _vectorStore = vectorStore; +// _configuration = configuration; +// } + +// public async Task ProcessDocumentAsync(Stream fileStream, string fileName, string contentType, int userId) +// { +// try +// { +// _logger.LogInformation("Processing document: {FileName}", fileName); + +// // Extract file extension +// var fileExtension = Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; +// if (string.IsNullOrEmpty(fileExtension)) +// { +// throw new ArgumentException("File extension is required."); +// } + +// // Check if file type is supported +// if (!SupportsFileType(fileExtension)) +// { +// throw new NotSupportedException($"File type {fileExtension} is not supported."); +// } + +// // Extract text from document +// var textChunks = await ExtractTextFromDocumentAsync(fileStream, fileExtension); + +// // Create document entity +// var document = new Document +// { +// Title = Path.GetFileNameWithoutExtension(fileName), +// FilePath = fileName, +// FileType = fileExtension, +// UserId = userId, +// Content = string.Join("\n", textChunks) +// }; + +// // Process each chunk for vector storage +// for (int i = 0; i < textChunks.Length; i++) +// { +// var chunk = textChunks[i]; +// document.Chunks.Add(new DocumentChunk +// { +// Content = chunk, +// Position = i +// }); + +// // In a real implementation, you would generate embeddings here +// // For example using OpenAI API or other embedding service +// // var embedding = await GenerateEmbeddingAsync(chunk); +// // await _vectorStore.StoreVectorAsync(chunk, embedding, new { documentId = document.Id, chunkId = i }); +// } + +// _logger.LogInformation("Document processed successfully: {FileName}", fileName); +// return document; +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Error processing document: {FileName}", fileName); +// throw; +// } +// } + +// public bool SupportsFileType(string fileExtension) +// { +// var supportedTypes = new List { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; +// return supportedTypes.Contains(fileExtension); +// } + +// public async Task ExtractTextFromDocumentAsync(Stream fileStream, string fileExtension) +// { +// switch (fileExtension) +// { +// case ".pdf": +// return await ExtractTextFromPdfAsync(fileStream); +// case ".txt": +// return await ExtractTextFromTextAsync(fileStream); +// // Add cases for other file types as needed +// default: +// throw new NotSupportedException($"File type {fileExtension} is not supported for text extraction."); +// } +// } + +// private async Task ExtractTextFromPdfAsync(Stream fileStream) +// { +// var textChunks = new List(); + +// using (var reader = new PdfReader(fileStream)) +// { +// for (int i = 1; i <= reader.NumberOfPages; i++) +// { +// var text = PdfTextExtractor.GetTextFromPage(reader, i); +// textChunks.Add(text); +// } +// } + +// return textChunks.ToArray(); +// } + +// private async Task ExtractTextFromTextAsync(Stream fileStream) +// { +// using (var reader = new StreamReader(fileStream)) +// { +// var text = await reader.ReadToEndAsync(); +// // Split text into chunks (simple implementation) +// return new[] { text }; +// } +// } + +// // In a real implementation, you would implement this method +// // private async Task GenerateEmbeddingAsync(string text) +// // { +// // // Call OpenAI API or other embedding service +// // // return embedding; +// // } +// } +// } \ No newline at end of file diff --git a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs new file mode 100644 index 0000000..926bfa4 --- /dev/null +++ b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs @@ -0,0 +1,109 @@ +// using System.Data; +// using Microsoft.Extensions.Configuration; +// using Npgsql; +// using RAG.Domain.Repositories; +// using RAG.Domain.Entities; + +// namespace RAG.Infrastructure.VectorStore +// { +// public class PgVectorStore : IVectorStore +// { +// private readonly string _connectionString; +// private readonly string _tableName; + +// public PgVectorStore(IConfiguration configuration) +// { +// _connectionString = configuration.GetSection("VectorStore:ConnectionString").Value; +// _tableName = configuration.GetSection("VectorStore:TableName").Value ?? "vectors"; +// } + +// public async Task InitializeAsync() +// { +// // Create extension if not exists +// await ExecuteNonQueryAsync("CREATE EXTENSION IF NOT EXISTS vector"); + +// // Create table if not exists +// await ExecuteNonQueryAsync($@"CREATE TABLE IF NOT EXISTS {_tableName} ( +// id SERIAL PRIMARY KEY, +// content TEXT NOT NULL, +// metadata JSONB, +// embedding VECTOR(1536) NOT NULL +// )"); + +// // Create index if not exists +// await ExecuteNonQueryAsync($@"CREATE INDEX IF NOT EXISTS {_tableName}_embedding_idx ON {_tableName} USING ivfflat (embedding vector_cosine_ops)"); +// } + +// public async Task StoreVectorAsync(string content, float[] embedding, object metadata = null) +// { +// var metadataJson = metadata != null ? System.Text.Json.JsonSerializer.Serialize(metadata) : null; + +// await ExecuteNonQueryAsync($@"INSERT INTO {_tableName} (content, metadata, embedding) +// VALUES (@content, @metadata, @embedding)", +// new NpgsqlParameter("@content", content), +// new NpgsqlParameter("@metadata", metadataJson ?? (object)DBNull.Value), +// new NpgsqlParameter("@embedding", NpgsqlTypes.NpgsqlDbType.Vector) { Value = embedding }); +// } + +// public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5) +// { +// var results = new List(); + +// using (var connection = new NpgsqlConnection(_connectionString)) +// { +// await connection.OpenAsync(); + +// using (var command = new NpgsqlCommand($@"SELECT id, content, metadata, embedding <=> @queryVector AS similarity +// FROM {_tableName} +// ORDER BY similarity +// LIMIT @topK", connection)) +// { +// command.Parameters.AddWithValue("@queryVector", NpgsqlTypes.NpgsqlDbType.Vector, queryVector); +// command.Parameters.AddWithValue("@topK", topK); + +// using (var reader = await command.ExecuteReaderAsync()) +// { +// while (await reader.ReadAsync()) +// { +// results.Add(new VectorResult +// { +// Id = reader.GetInt32(0), +// Content = reader.GetString(1), +// Metadata = reader.IsDBNull(2) ? null : System.Text.Json.JsonSerializer.Deserialize>(reader.GetString(2)), +// Similarity = reader.GetDouble(3) +// }); +// } +// } +// } +// } + +// return results; +// } + +// private async Task ExecuteNonQueryAsync(string sql, params NpgsqlParameter[] parameters) +// { +// using (var connection = new NpgsqlConnection(_connectionString)) +// { +// await connection.OpenAsync(); + +// using (var command = new NpgsqlCommand(sql, connection)) +// { +// if (parameters != null) +// { +// command.Parameters.AddRange(parameters); +// } + +// await command.ExecuteNonQueryAsync(); +// } +// } +// } +// } + +// public class VectorResult +// { +// public int Id { get; set; } +// public string Content { get; set; } +// public Dictionary Metadata { get; set; } +// public double Similarity { get; set; } +// } +// } \ No newline at end of file diff --git a/backend/RAG.Tests/RAG.Tests.csproj b/backend/RAG.Tests/RAG.Tests.csproj index 5d9e2d3..66603ab 100644 --- a/backend/RAG.Tests/RAG.Tests.csproj +++ b/backend/RAG.Tests/RAG.Tests.csproj @@ -1,19 +1,17 @@ - + - net8.0 + net9.0 enable enable - false - true - - - - + + + + diff --git a/backend/RAG.Tests/UnitTest1.cs b/backend/RAG.Tests/UnitTest1.cs index 0c054e8..3326ee9 100644 --- a/backend/RAG.Tests/UnitTest1.cs +++ b/backend/RAG.Tests/UnitTest1.cs @@ -1,4 +1,4 @@ -namespace RAG.Tests; +namespace RAG.Tests; public class UnitTest1 { @@ -7,4 +7,4 @@ public class UnitTest1 { } -} \ No newline at end of file +} diff --git a/backend/RAG.sln b/backend/RAG.sln index f53c424..70304a8 100644 --- a/backend/RAG.sln +++ b/backend/RAG.sln @@ -3,44 +3,88 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Api", "RAG.Api\RAG.Api.csproj", "{3DAC1A57-A784-43CD-B665-68896913F082}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Api", "RAG.Api\RAG.Api.csproj", "{7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Application", "RAG.Application\RAG.Application.csproj", "{FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Application", "RAG.Application\RAG.Application.csproj", "{3BFEF6F7-44BA-470F-8265-A35E061D4465}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Domain", "RAG.Domain\RAG.Domain.csproj", "{2937A929-EB24-43B6-8BD9-EEA32B895C9C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Domain", "RAG.Domain\RAG.Domain.csproj", "{09BBBB92-0E93-4195-8830-ED2546A17771}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Infrastructure", "RAG.Infrastructure\RAG.Infrastructure.csproj", "{46C0164B-CF85-4CA1-95A6-16DDC05476CB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Infrastructure", "RAG.Infrastructure\RAG.Infrastructure.csproj", "{4461B67F-4DA9-419E-9BAC-452DEFC585ED}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Tests", "RAG.Tests\RAG.Tests.csproj", "{994D11A9-A40A-4FC2-A7FB-BB366380EEF2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RAG.Tests", "RAG.Tests\RAG.Tests.csproj", "{0EC59D41-9602-49F9-B60C-97A249767630}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Debug|x64.Build.0 = Debug|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Debug|x86.Build.0 = Debug|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Release|Any CPU.Build.0 = Release|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Release|x64.ActiveCfg = Release|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Release|x64.Build.0 = Release|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Release|x86.ActiveCfg = Release|Any CPU + {7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}.Release|x86.Build.0 = Release|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Debug|x64.ActiveCfg = Debug|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Debug|x64.Build.0 = Debug|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Debug|x86.ActiveCfg = Debug|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Debug|x86.Build.0 = Debug|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Release|Any CPU.Build.0 = Release|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Release|x64.ActiveCfg = Release|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Release|x64.Build.0 = Release|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Release|x86.ActiveCfg = Release|Any CPU + {3BFEF6F7-44BA-470F-8265-A35E061D4465}.Release|x86.Build.0 = Release|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Debug|x64.ActiveCfg = Debug|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Debug|x64.Build.0 = Debug|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Debug|x86.ActiveCfg = Debug|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Debug|x86.Build.0 = Debug|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Release|Any CPU.Build.0 = Release|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Release|x64.ActiveCfg = Release|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Release|x64.Build.0 = Release|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Release|x86.ActiveCfg = Release|Any CPU + {09BBBB92-0E93-4195-8830-ED2546A17771}.Release|x86.Build.0 = Release|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Debug|x64.Build.0 = Debug|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Debug|x86.Build.0 = Debug|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Release|Any CPU.Build.0 = Release|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Release|x64.ActiveCfg = Release|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Release|x64.Build.0 = Release|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Release|x86.ActiveCfg = Release|Any CPU + {4461B67F-4DA9-419E-9BAC-452DEFC585ED}.Release|x86.Build.0 = Release|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Debug|x64.Build.0 = Debug|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Debug|x86.ActiveCfg = Debug|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Debug|x86.Build.0 = Debug|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Release|Any CPU.Build.0 = Release|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Release|x64.ActiveCfg = Release|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Release|x64.Build.0 = Release|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Release|x86.ActiveCfg = Release|Any CPU + {0EC59D41-9602-49F9-B60C-97A249767630}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3DAC1A57-A784-43CD-B665-68896913F082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3DAC1A57-A784-43CD-B665-68896913F082}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3DAC1A57-A784-43CD-B665-68896913F082}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3DAC1A57-A784-43CD-B665-68896913F082}.Release|Any CPU.Build.0 = Release|Any CPU - {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB90A32A-968E-4FDB-BA02-25C8A3F0D05A}.Release|Any CPU.Build.0 = Release|Any CPU - {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2937A929-EB24-43B6-8BD9-EEA32B895C9C}.Release|Any CPU.Build.0 = Release|Any CPU - {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {46C0164B-CF85-4CA1-95A6-16DDC05476CB}.Release|Any CPU.Build.0 = Release|Any CPU - {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {994D11A9-A40A-4FC2-A7FB-BB366380EEF2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection EndGlobal diff --git a/backend/txt.txt b/backend/txt.txt new file mode 100644 index 0000000..e69de29 -- Gitee From b497aa40456eb528fd5c4acb1d688f3d6dd71934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Tue, 5 Aug 2025 10:33:16 +0800 Subject: [PATCH 03/31] =?UTF-8?q?=E5=B7=B2=E4=B8=8B=E8=BD=BD=E5=A5=BD?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=8C=85=EF=BC=8C=E4=B8=94=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=BA=86=E5=AE=9E=E4=BD=93=E7=B1=BB=EF=BC=8C=E4=BB=93=E5=82=A8?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BD=86=E6=9C=89=E8=AE=B8=E5=A4=9A?= =?UTF-8?q?=E6=8A=A5=E9=94=99=EF=BC=8C=E8=BF=98=E5=9C=A8=E5=A4=84=E7=90=86?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/Controllers/ChatController.cs | 154 +++---- .../RAG.Api/Controllers/DocumentController.cs | 107 +++-- backend/RAG.Api/Program.cs | 70 +++- backend/RAG.Api/RAG.Api.csproj | 9 + backend/RAG.Api/appsettings.json | 9 +- .../Commands/UploadDocumentCommand.cs | 8 + .../Handlers/UploadDocumentHandler.cs | 66 +++ .../RAG.Application/RAG.Application.csproj | 6 + backend/RAG.Domain/Entities/BaseEntity.cs | 27 ++ backend/RAG.Domain/Entities/Document.cs | 92 ++++- .../RAG.Domain/Repositories/IRepository.cs | 20 + .../RAG.Domain/Repositories/IUnitOfWork.cs | 10 + .../RAG.Domain/ValueObjects/AccessLevel.cs | 27 ++ .../RAG.Domain/ValueObjects/DocumentTitle.cs | 25 ++ backend/RAG.Domain/ValueObjects/FileName.cs | 25 ++ backend/RAG.Domain/ValueObjects/FilePath.cs | 22 + backend/RAG.Domain/ValueObjects/FileSize.cs | 22 + backend/RAG.Domain/ValueObjects/FileType.cs | 22 + .../Data/ApplicationDbContext.cs | 44 +- .../RAG.Infrastructure.csproj | 9 + .../Repositories/ApplicationUnitOfWork.cs | 34 ++ .../Repositories/Repository.cs | 68 +++ .../Services/DocumentProcessingService.cs | 387 +++++++++++------- .../VectorStore/PgVectorStore.cs | 220 +++++----- 24 files changed, 1041 insertions(+), 442 deletions(-) create mode 100644 backend/RAG.Application/Commands/UploadDocumentCommand.cs create mode 100644 backend/RAG.Application/Handlers/UploadDocumentHandler.cs create mode 100644 backend/RAG.Domain/Entities/BaseEntity.cs create mode 100644 backend/RAG.Domain/Repositories/IRepository.cs create mode 100644 backend/RAG.Domain/Repositories/IUnitOfWork.cs create mode 100644 backend/RAG.Domain/ValueObjects/AccessLevel.cs create mode 100644 backend/RAG.Domain/ValueObjects/DocumentTitle.cs create mode 100644 backend/RAG.Domain/ValueObjects/FileName.cs create mode 100644 backend/RAG.Domain/ValueObjects/FilePath.cs create mode 100644 backend/RAG.Domain/ValueObjects/FileSize.cs create mode 100644 backend/RAG.Domain/ValueObjects/FileType.cs create mode 100644 backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs create mode 100644 backend/RAG.Infrastructure/Repositories/Repository.cs diff --git a/backend/RAG.Api/Controllers/ChatController.cs b/backend/RAG.Api/Controllers/ChatController.cs index 1723df4..febaf3c 100644 --- a/backend/RAG.Api/Controllers/ChatController.cs +++ b/backend/RAG.Api/Controllers/ChatController.cs @@ -1,89 +1,89 @@ -// using Microsoft.AspNetCore.Mvc; -// using System.Threading.Tasks; -// using RAG.Domain.Repositories; -// using RAG.Domain.Entities; -// using Microsoft.Extensions.Logging; -// using System.Collections.Generic; -// using System.Linq; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using RAG.Domain.Repositories; +using RAG.Domain.Entities; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; -// namespace RAG.Api.Controllers -// { -// [ApiController] -// [Route("api/[controller]")] -// public class ChatController : ControllerBase -// { -// private readonly IVectorStore _vectorStore; -// private readonly ILogger _logger; +namespace RAG.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ChatController : ControllerBase + { + private readonly IVectorStore _vectorStore; + private readonly ILogger _logger; -// public ChatController( -// IVectorStore vectorStore, -// ILogger logger) -// { -// _vectorStore = vectorStore; -// _logger = logger; -// } + public ChatController( + IVectorStore vectorStore, + ILogger logger) + { + _vectorStore = vectorStore; + _logger = logger; + } -// [HttpPost("query")] -// public async Task Query([FromBody] ChatQuery query) -// { -// try -// { -// if (string.IsNullOrEmpty(query.Message)) -// { -// return BadRequest("Message cannot be empty."); -// } + [HttpPost("query")] + public async Task Query([FromBody] ChatQuery query) + { + try + { + if (string.IsNullOrEmpty(query.Message)) + { + return BadRequest("Message cannot be empty."); + } -// _logger.LogInformation("Processing chat query: {Message}", query.Message); + _logger.LogInformation("Processing chat query: {Message}", query.Message); -// // In a real implementation, you would generate an embedding for the query -// // var queryEmbedding = await GenerateEmbeddingAsync(query.Message); + // In a real implementation, you would generate an embedding for the query + // var queryEmbedding = await GenerateEmbeddingAsync(query.Message); -// // For demonstration purposes, create a dummy embedding -// var queryEmbedding = new float[1536]; // Match the vector dimension used in PgVectorStore + // For demonstration purposes, create a dummy embedding + var queryEmbedding = new float[1536]; // Match the vector dimension used in PgVectorStore -// // Search for similar vectors -// var results = await _vectorStore.SearchVectorsAsync(queryEmbedding, query.TopK ?? 5); + // Search for similar vectors + var results = await _vectorStore.SearchVectorsAsync(queryEmbedding, query.TopK ?? 5); -// // In a real implementation, you would use these results to generate a response -// // using a language model -// var response = new ChatResponse -// { -// Query = query.Message, -// Response = "This is a placeholder response generated based on retrieved context.", -// References = results.Select(r => new Reference -// { -// Content = r.Content.Substring(0, Math.Min(100, r.Content.Length)) + "...", -// Similarity = r.Similarity -// }).ToList() -// }; + // In a real implementation, you would use these results to generate a response + // using a language model + var response = new ChatResponse + { + Query = query.Message, + Response = "This is a placeholder response generated based on retrieved context.", + References = results.Select(r => new Reference + { + Content = r.Content.Substring(0, Math.Min(100, r.Content.Length)) + "...", + Similarity = r.Similarity + }).ToList() + }; -// return Ok(response); -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, "Error processing chat query"); -// return StatusCode(500, "Internal server error"); -// } -// } -// } + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing chat query"); + return StatusCode(500, "Internal server error"); + } + } + } -// public class ChatQuery -// { -// public string Message { get; set; } -// public int? TopK { get; set; } -// public string SessionId { get; set; } -// } + public class ChatQuery + { + public string Message { get; set; } + public int? TopK { get; set; } + public string SessionId { get; set; } + } -// public class ChatResponse -// { -// public string Query { get; set; } -// public string Response { get; set; } -// public List References { get; set; } -// } + public class ChatResponse + { + public string Query { get; set; } + public string Response { get; set; } + public List References { get; set; } + } -// public class Reference -// { -// public string Content { get; set; } -// public double Similarity { get; set; } -// } -// } \ No newline at end of file + public class Reference + { + public string Content { get; set; } + public double Similarity { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index 1e4540c..766cee6 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -1,63 +1,58 @@ -// using Microsoft.AspNetCore.Mvc; -// using Microsoft.AspNetCore.Http; -// using System.Threading.Tasks; -// using RAG.Domain.Services; -// using RAG.Domain.Entities; -// using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using MediatR; +using RAG.Application.Commands; +using RAG.Domain.ValueObjects; +using Microsoft.Extensions.Logging; -// namespace RAG.Api.Controllers -// { -// [ApiController] -// [Route("api/[controller]")] -// public class DocumentController : ControllerBase -// { -// private readonly IDocumentProcessingService _documentProcessingService; -// private readonly ILogger _logger; +namespace RAG.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class DocumentController : ControllerBase + { + private readonly IMediator _mediator; + private readonly ILogger _logger; -// public DocumentController( -// IDocumentProcessingService documentProcessingService, -// ILogger logger) -// { -// _documentProcessingService = documentProcessingService; -// _logger = logger; -// } + public DocumentController( + IMediator mediator, + ILogger logger) + { + _mediator = mediator; + _logger = logger; + } -// [HttpPost("upload")] -// public async Task UploadDocument(IFormFile file) -// { -// try -// { -// if (file == null || file.Length == 0) -// { -// return BadRequest("No file uploaded."); -// } + [HttpPost("upload")] + public async Task UploadDocument(IFormFile file, [FromForm] string accessLevel = "internal") + { + try + { + if (file == null || file.Length == 0) + { + return BadRequest("No file uploaded."); + } -// // In a real application, you would get the user ID from authentication -// var userId = 1; // Placeholder + // 创建访问级别值对象 + var accessLevelObj = AccessLevel.Create(accessLevel); -// using (var stream = file.OpenReadStream()) -// { -// var document = await _documentProcessingService.ProcessDocumentAsync( -// stream, -// file.FileName, -// file.ContentType, -// userId); + // 发送上传文档命令 + var documentId = await _mediator.Send(new UploadDocumentCommand(file, accessLevelObj)); -// return Ok(new { document.Id, document.Title, document.FileType, document.CreatedAt }); -// } -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, "Error uploading document"); -// return StatusCode(500, "Internal server error"); -// } -// } + return Ok(new { DocumentId = documentId }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error uploading document"); + return StatusCode(500, "Internal server error"); + } + } -// [HttpGet("supported-types")] -// public IActionResult GetSupportedFileTypes() -// { -// var supportedTypes = new[] { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; -// return Ok(supportedTypes); -// } -// } -// } \ No newline at end of file + [HttpGet("supported-types")] + public IActionResult GetSupportedFileTypes() + { + var supportedTypes = new[] { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; + return Ok(supportedTypes); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs index ee9d65d..b21db8c 100644 --- a/backend/RAG.Api/Program.cs +++ b/backend/RAG.Api/Program.cs @@ -1,9 +1,55 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using RAG.Application.Commands; +using RAG.Application.Handlers; +using RAG.Domain.Repositories; +using RAG.Domain.Services; +using RAG.Infrastructure.Data; +using RAG.Infrastructure.Repositories; +using RAG.Infrastructure.Services; +using RAG.Infrastructure.VectorStore; +using MediatR; +using System.Reflection; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. + +builder.Services.AddControllers(); + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +// Add MediatR +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(UploadDocumentCommand).Assembly)); + +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(UploadDocumentHandler).Assembly)); + +// Add DbContext +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Add Repositories and Unit of Work +builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); +builder.Services.AddScoped(); + +// Add Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Add CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -14,28 +60,10 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; +app.UseCors("AllowAll"); -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast"); +app.MapControllers(); app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} +public partial class Program {} diff --git a/backend/RAG.Api/RAG.Api.csproj b/backend/RAG.Api/RAG.Api.csproj index b4c5283..e7b12a3 100644 --- a/backend/RAG.Api/RAG.Api.csproj +++ b/backend/RAG.Api/RAG.Api.csproj @@ -7,7 +7,16 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json index 10f68b8..147f30e 100644 --- a/backend/RAG.Api/appsettings.json +++ b/backend/RAG.Api/appsettings.json @@ -5,5 +5,12 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=rag_db;Username=postgres;Password=your_password_here" + }, + "VectorStore": { + "Type": "Postgres", + "ConnectionString": "Host=localhost;Port=5432;Database=rag_db;Username=postgres;Password=your_password_here" + } } diff --git a/backend/RAG.Application/Commands/UploadDocumentCommand.cs b/backend/RAG.Application/Commands/UploadDocumentCommand.cs new file mode 100644 index 0000000..f9cb20f --- /dev/null +++ b/backend/RAG.Application/Commands/UploadDocumentCommand.cs @@ -0,0 +1,8 @@ +using MediatR; +using Microsoft.AspNetCore.Http; +using RAG.Domain.ValueObjects; + +namespace RAG.Application.Commands +{ + public record UploadDocumentCommand(IFormFile File, AccessLevel AccessLevel) : IRequest; +} \ No newline at end of file diff --git a/backend/RAG.Application/Handlers/UploadDocumentHandler.cs b/backend/RAG.Application/Handlers/UploadDocumentHandler.cs new file mode 100644 index 0000000..e0c7ef4 --- /dev/null +++ b/backend/RAG.Application/Handlers/UploadDocumentHandler.cs @@ -0,0 +1,66 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using RAG.Application.Commands; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Domain.Services; +using RAG.Domain.ValueObjects; + +namespace RAG.Application.Handlers +{ + public class UploadDocumentHandler : IRequestHandler + { + private readonly IDocumentProcessingService _documentProcessingService; + private readonly IRepository _documentRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public UploadDocumentHandler( + IDocumentProcessingService documentProcessingService, + IRepository documentRepository, + IUnitOfWork unitOfWork, + ILogger logger) + { + _documentProcessingService = documentProcessingService; + _documentRepository = documentRepository; + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task Handle(UploadDocumentCommand request, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Handling document upload request"); + + // 获取当前用户ID(实际应用中应从身份验证上下文中获取) + var userId = 1; // 占位符 + + // 使用文档处理服务处理上传的文件 + using (var stream = request.File.OpenReadStream()) + { + var document = await _documentProcessingService.ProcessDocumentAsync( + stream, + request.File.FileName, + request.File.ContentType, + userId); + + // 设置文档访问级别 + document.AccessLevel = request.AccessLevel.Value; + + // 保存文档到数据库 + await _documentRepository.AddAsync(document); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation("Document uploaded successfully with ID: {DocumentId}", document.Id); + return document.Id; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling document upload request"); + throw; + } + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj index 8c0b9f3..748d7dd 100644 --- a/backend/RAG.Application/RAG.Application.csproj +++ b/backend/RAG.Application/RAG.Application.csproj @@ -4,6 +4,12 @@ + + + + + + net9.0 enable diff --git a/backend/RAG.Domain/Entities/BaseEntity.cs b/backend/RAG.Domain/Entities/BaseEntity.cs new file mode 100644 index 0000000..092db55 --- /dev/null +++ b/backend/RAG.Domain/Entities/BaseEntity.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace RAG.Domain.Entities +{ + public abstract class BaseEntity + { + private readonly List _domainEvents = new(); + + public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); + + protected void AddDomainEvent(DomainEvent domainEvent) + { + _domainEvents.Add(domainEvent); + } + + public void ClearDomainEvents() + { + _domainEvents.Clear(); + } + } + + public abstract class DomainEvent + { + public DateTime OccurredOn { get; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs index 771f36f..1cb36ed 100644 --- a/backend/RAG.Domain/Entities/Document.cs +++ b/backend/RAG.Domain/Entities/Document.cs @@ -1,26 +1,92 @@ using System; using System.Collections.Generic; +using RAG.Domain.ValueObjects; +using RAG.Domain.Events; namespace RAG.Domain.Entities { - public class Document + public class Document : BaseEntity { - public int Id { get; set; } - public string Title { get; set; } - public string Content { get; set; } - public string FilePath { get; set; } - public string FileName { get; set; } - public string FileType { get; set; } - public long FileSize { get; set; } + private readonly List _chunks = new(); + + // 私有构造函数,确保通过工厂方法创建 + private Document() { } + + // 工厂方法 + public static Document Create( + DocumentTitle title, + FileName fileName, + FilePath filePath, + FileType fileType, + FileSize fileSize, + AccessLevel accessLevel) + { + var document = new Document + { + Title = title, + FileName = fileName, + FilePath = filePath, + FileType = fileType, + FileSize = fileSize, + AccessLevel = accessLevel, + Status = "pending", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + // 发布领域事件 + document.AddDomainEvent(new DocumentCreatedEvent(document.Id)); + + return document; + } + + public int Id { get; private set; } + public DocumentTitle Title { get; private set; } + public FileName FileName { get; private set; } + public FilePath FilePath { get; private set; } + public FileType FileType { get; private set; } + public FileSize FileSize { get; private set; } public string Description { get; set; } public string Tags { get; set; } - public string AccessLevel { get; set; } = "internal"; // 'internal' or 'public' - public string Status { get; set; } = "pending"; // 'pending', 'processing', 'completed', 'failed' - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public AccessLevel AccessLevel { get; private set; } + public string Status { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime UpdatedAt { get; private set; } public int UserId { get; set; } public User User { get; set; } - public ICollection Chunks { get; set; } = new List(); + + public IReadOnlyList Chunks => _chunks.AsReadOnly(); + + // 业务方法 + public void UpdateProcessingStatus() + { + if (Status != "pending") + throw new InvalidOperationException("Document must be in pending status"); + + Status = "processing"; + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new DocumentProcessingStartedEvent(Id)); + } + + public void CompleteProcessing() + { + if (Status != "processing") + throw new InvalidOperationException("Document must be in processing status"); + + Status = "completed"; + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new DocumentProcessingCompletedEvent(Id)); + } + + public void AddChunk(DocumentChunk chunk) + { + if (chunk.DocumentId != Id) + throw new ArgumentException("Chunk must belong to this document"); + + _chunks.Add(chunk); + } } public class DocumentChunk diff --git a/backend/RAG.Domain/Repositories/IRepository.cs b/backend/RAG.Domain/Repositories/IRepository.cs new file mode 100644 index 0000000..11e1774 --- /dev/null +++ b/backend/RAG.Domain/Repositories/IRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace RAG.Domain.Repositories +{ + public interface IRepository where TEntity : class + { + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task> FindAsync(Expression> predicate); + Task SingleOrDefaultAsync(Expression> predicate); + Task AddAsync(TEntity entity); + Task AddRangeAsync(IEnumerable entities); + void Remove(TEntity entity); + void RemoveRange(IEnumerable entities); + Task CountAsync(Expression> predicate = null); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IUnitOfWork.cs b/backend/RAG.Domain/Repositories/IUnitOfWork.cs new file mode 100644 index 0000000..20429ac --- /dev/null +++ b/backend/RAG.Domain/Repositories/IUnitOfWork.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace RAG.Domain.Repositories +{ + public interface IUnitOfWork + { + IRepository GetRepository() where TEntity : class; + Task SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/AccessLevel.cs b/backend/RAG.Domain/ValueObjects/AccessLevel.cs new file mode 100644 index 0000000..633f3a0 --- /dev/null +++ b/backend/RAG.Domain/ValueObjects/AccessLevel.cs @@ -0,0 +1,27 @@ +namespace RAG.Domain.ValueObjects +{ + public record AccessLevel + { + public string Value { get; init; } + + private AccessLevel(string value) + { + Value = value; + } + + public static AccessLevel Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Access level cannot be empty"); + + value = value.ToLowerInvariant(); + + if (value != "internal" && value != "public") + throw new ArgumentException("Access level must be 'internal' or 'public'"); + + return new AccessLevel(value); + } + + public static implicit operator string(AccessLevel accessLevel) => accessLevel.Value; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/DocumentTitle.cs b/backend/RAG.Domain/ValueObjects/DocumentTitle.cs new file mode 100644 index 0000000..50ddd1f --- /dev/null +++ b/backend/RAG.Domain/ValueObjects/DocumentTitle.cs @@ -0,0 +1,25 @@ +namespace RAG.Domain.ValueObjects +{ + public record DocumentTitle + { + public string Value { get; init; } + + private DocumentTitle(string value) + { + Value = value; + } + + public static DocumentTitle Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Document title cannot be empty"); + + if (value.Length > 255) + throw new ArgumentException("Document title cannot exceed 255 characters"); + + return new DocumentTitle(value); + } + + public static implicit operator string(DocumentTitle title) => title.Value; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FileName.cs b/backend/RAG.Domain/ValueObjects/FileName.cs new file mode 100644 index 0000000..22aa95a --- /dev/null +++ b/backend/RAG.Domain/ValueObjects/FileName.cs @@ -0,0 +1,25 @@ +namespace RAG.Domain.ValueObjects +{ + public record FileName + { + public string Value { get; init; } + + private FileName(string value) + { + Value = value; + } + + public static FileName Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("File name cannot be empty"); + + if (value.Length > 255) + throw new ArgumentException("File name cannot exceed 255 characters"); + + return new FileName(value); + } + + public static implicit operator string(FileName fileName) => fileName.Value; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FilePath.cs b/backend/RAG.Domain/ValueObjects/FilePath.cs new file mode 100644 index 0000000..7263ee4 --- /dev/null +++ b/backend/RAG.Domain/ValueObjects/FilePath.cs @@ -0,0 +1,22 @@ +namespace RAG.Domain.ValueObjects +{ + public record FilePath + { + public string Value { get; init; } + + private FilePath(string value) + { + Value = value; + } + + public static FilePath Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("File path cannot be empty"); + + return new FilePath(value); + } + + public static implicit operator string(FilePath filePath) => filePath.Value; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FileSize.cs b/backend/RAG.Domain/ValueObjects/FileSize.cs new file mode 100644 index 0000000..63f5288 --- /dev/null +++ b/backend/RAG.Domain/ValueObjects/FileSize.cs @@ -0,0 +1,22 @@ +namespace RAG.Domain.ValueObjects +{ + public record FileSize + { + public long Value { get; init; } + + private FileSize(long value) + { + Value = value; + } + + public static FileSize Create(long value) + { + if (value < 0) + throw new ArgumentException("File size cannot be negative"); + + return new FileSize(value); + } + + public static implicit operator long(FileSize fileSize) => fileSize.Value; + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FileType.cs b/backend/RAG.Domain/ValueObjects/FileType.cs new file mode 100644 index 0000000..3117187 --- /dev/null +++ b/backend/RAG.Domain/ValueObjects/FileType.cs @@ -0,0 +1,22 @@ +namespace RAG.Domain.ValueObjects +{ + public record FileType + { + public string Value { get; init; } + + private FileType(string value) + { + Value = value; + } + + public static FileType Create(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("File type cannot be empty"); + + return new FileType(value.ToLowerInvariant()); + } + + public static implicit operator string(FileType fileType) => fileType.Value; + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index 5f21a8b..e6f1a71 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -1,26 +1,26 @@ -// using Microsoft.EntityFrameworkCore; -// using RAG.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Entities; -// namespace RAG.Infrastructure.Data -// { -// public class ApplicationDbContext : DbContext -// { -// public ApplicationDbContext(DbContextOptions options) -// : base(options) -// {} +namespace RAG.Infrastructure.Data +{ + public class ApplicationDbContext : DbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + {} -// // DbSets for your entities -// public DbSet Documents { get; set; } -// public DbSet ChatHistories { get; set; } -// public DbSet Users { get; set; } + // DbSets for your entities + public DbSet Documents { get; set; } + public DbSet ChatHistories { get; set; } + public DbSet Users { get; set; } -// protected override void OnModelCreating(ModelBuilder modelBuilder) -// { -// base.OnModelCreating(modelBuilder); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); -// // Configure entity mappings if needed -// modelBuilder.Entity() -// .HasIndex(d => d.Title); -// } -// } -// } \ No newline at end of file + // Configure entity mappings if needed + modelBuilder.Entity() + .HasIndex(d => d.Title); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index 2b89d02..a673a92 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -5,6 +5,15 @@ + + + + + + + + + net9.0 enable diff --git a/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs new file mode 100644 index 0000000..4bbe243 --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; + +namespace RAG.Infrastructure.Repositories +{ + public class ApplicationUnitOfWork : IUnitOfWork + { + private readonly ApplicationDbContext _context; + private readonly Dictionary _repositories = new(); + + public ApplicationUnitOfWork(ApplicationDbContext context) + { + _context = context; + } + + public IRepository GetRepository() where TEntity : class + { + var type = typeof(TEntity); + if (!_repositories.ContainsKey(type)) + { + var repositoryType = typeof(Repository<>).MakeGenericType(type); + var repository = Activator.CreateInstance(repositoryType, _context); + _repositories[type] = repository; + } + return (IRepository)_repositories[type]; + } + + public async Task SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/Repository.cs b/backend/RAG.Infrastructure/Repositories/Repository.cs new file mode 100644 index 0000000..c306450 --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/Repository.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; +using System.Linq.Expressions; + +namespace RAG.Infrastructure.Repositories +{ + public class Repository : IRepository where TEntity : class + { + protected readonly ApplicationDbContext _context; + protected readonly DbSet _dbSet; + + public Repository(ApplicationDbContext context) + { + _context = context; + _dbSet = context.Set(); + } + + public async Task GetByIdAsync(int id) + { + return await _dbSet.FindAsync(id); + } + + public async Task> GetAllAsync() + { + return await _dbSet.ToListAsync(); + } + + public async Task> FindAsync(Expression> predicate) + { + return await _dbSet.Where(predicate).ToListAsync(); + } + + public async Task SingleOrDefaultAsync(Expression> predicate) + { + return await _dbSet.SingleOrDefaultAsync(predicate); + } + + public async Task AddAsync(TEntity entity) + { + await _dbSet.AddAsync(entity); + } + + public async Task AddRangeAsync(IEnumerable entities) + { + await _dbSet.AddRangeAsync(entities); + } + + public void Remove(TEntity entity) + { + _dbSet.Remove(entity); + } + + public void RemoveRange(IEnumerable entities) + { + _dbSet.RemoveRange(entities); + } + + public async Task CountAsync(Expression> predicate = null) + { + if (predicate == null) + { + return await _dbSet.CountAsync(); + } + return await _dbSet.CountAsync(predicate); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs index 5ab2d03..bbd0f53 100644 --- a/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs +++ b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs @@ -1,143 +1,244 @@ -// using System; -// using System.Collections.Generic; -// using System.IO; -// using System.Linq; -// using System.Threading.Tasks; -// using iTextSharp.text.pdf; -// using iTextSharp.text.pdf.parser; -// using Microsoft.Extensions.Logging; -// using Microsoft.Extensions.Configuration; -// using RAG.Domain.Entities; -// using RAG.Domain.Repositories; -// using RAG.Domain.Services; - -// namespace RAG.Infrastructure.Services -// { -// public class DocumentProcessingService : IDocumentProcessingService -// { -// private readonly ILogger _logger; -// private readonly IVectorStore _vectorStore; -// private readonly IConfiguration _configuration; - -// public DocumentProcessingService( -// ILogger logger, -// IVectorStore vectorStore, -// IConfiguration configuration) -// { -// _logger = logger; -// _vectorStore = vectorStore; -// _configuration = configuration; -// } - -// public async Task ProcessDocumentAsync(Stream fileStream, string fileName, string contentType, int userId) -// { -// try -// { -// _logger.LogInformation("Processing document: {FileName}", fileName); - -// // Extract file extension -// var fileExtension = Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; -// if (string.IsNullOrEmpty(fileExtension)) -// { -// throw new ArgumentException("File extension is required."); -// } - -// // Check if file type is supported -// if (!SupportsFileType(fileExtension)) -// { -// throw new NotSupportedException($"File type {fileExtension} is not supported."); -// } - -// // Extract text from document -// var textChunks = await ExtractTextFromDocumentAsync(fileStream, fileExtension); - -// // Create document entity -// var document = new Document -// { -// Title = Path.GetFileNameWithoutExtension(fileName), -// FilePath = fileName, -// FileType = fileExtension, -// UserId = userId, -// Content = string.Join("\n", textChunks) -// }; - -// // Process each chunk for vector storage -// for (int i = 0; i < textChunks.Length; i++) -// { -// var chunk = textChunks[i]; -// document.Chunks.Add(new DocumentChunk -// { -// Content = chunk, -// Position = i -// }); - -// // In a real implementation, you would generate embeddings here -// // For example using OpenAI API or other embedding service -// // var embedding = await GenerateEmbeddingAsync(chunk); -// // await _vectorStore.StoreVectorAsync(chunk, embedding, new { documentId = document.Id, chunkId = i }); -// } - -// _logger.LogInformation("Document processed successfully: {FileName}", fileName); -// return document; -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, "Error processing document: {FileName}", fileName); -// throw; -// } -// } - -// public bool SupportsFileType(string fileExtension) -// { -// var supportedTypes = new List { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; -// return supportedTypes.Contains(fileExtension); -// } - -// public async Task ExtractTextFromDocumentAsync(Stream fileStream, string fileExtension) -// { -// switch (fileExtension) -// { -// case ".pdf": -// return await ExtractTextFromPdfAsync(fileStream); -// case ".txt": -// return await ExtractTextFromTextAsync(fileStream); -// // Add cases for other file types as needed -// default: -// throw new NotSupportedException($"File type {fileExtension} is not supported for text extraction."); -// } -// } - -// private async Task ExtractTextFromPdfAsync(Stream fileStream) -// { -// var textChunks = new List(); - -// using (var reader = new PdfReader(fileStream)) -// { -// for (int i = 1; i <= reader.NumberOfPages; i++) -// { -// var text = PdfTextExtractor.GetTextFromPage(reader, i); -// textChunks.Add(text); -// } -// } - -// return textChunks.ToArray(); -// } - -// private async Task ExtractTextFromTextAsync(Stream fileStream) -// { -// using (var reader = new StreamReader(fileStream)) -// { -// var text = await reader.ReadToEndAsync(); -// // Split text into chunks (simple implementation) -// return new[] { text }; -// } -// } - -// // In a real implementation, you would implement this method -// // private async Task GenerateEmbeddingAsync(string text) -// // { -// // // Call OpenAI API or other embedding service -// // // return embedding; -// // } -// } -// } \ No newline at end of file +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using iTextSharp.text.pdf; +using iTextSharp.text.pdf.parser; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Domain.Services; +using System.Text; +using NPOI.XSSF.UserModel; +using NPOI.HSSF.UserModel; +using NPOI.SS.UserModel; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; + +namespace RAG.Infrastructure.Services +{ + public class DocumentProcessingService : IDocumentProcessingService + { + private readonly ILogger _logger; + private readonly IVectorStore _vectorStore; + private readonly IConfiguration _configuration; + + public DocumentProcessingService( + ILogger logger, + IVectorStore vectorStore, + IConfiguration configuration) + { + _logger = logger; + _vectorStore = vectorStore; + _configuration = configuration; + } + + public async Task ProcessDocumentAsync(Stream fileStream, string fileName, string contentType, int userId) + { + try + { + _logger.LogInformation("Processing document: {FileName}", fileName); + + // Extract file extension + var fileExtension = System.IO.Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; + if (string.IsNullOrEmpty(fileExtension)) + { + throw new ArgumentException("File extension is required."); + } + + // Check if file type is supported + if (!SupportsFileType(fileExtension)) + { + throw new NotSupportedException($"File type {fileExtension} is not supported."); + } + + // Extract text from document + var textChunks = await ExtractTextFromDocumentAsync(fileStream, fileExtension); + + // Create document entity + var document = new Document + { + Title = System.IO.Path.GetFileNameWithoutExtension(fileName), + FilePath = fileName, + FileType = fileExtension, + UserId = userId, + Content = string.Join("\n", textChunks) + }; + + // Process each chunk for vector storage + for (int i = 0; i < textChunks.Length; i++) + { + var chunk = textChunks[i]; + document.Chunks.Add(new DocumentChunk + { + Content = chunk, + Position = i + }); + + // 生成向量 + var embedding = await GenerateEmbeddingAsync(chunk); + // 存储向量 + await _vectorStore.StoreVectorAsync(chunk, embedding, new { documentId = document.Id, chunkId = i }); + } + + _logger.LogInformation("Document processed successfully: {FileName}", fileName); + return document; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing document: {FileName}", fileName); + throw; + } + } + + public bool SupportsFileType(string fileExtension) + { + var supportedTypes = new List { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; + return supportedTypes.Contains(fileExtension); + } + + public async Task ExtractTextFromDocumentAsync(Stream fileStream, string fileExtension) + { + switch (fileExtension) + { + case ".pdf": + return await ExtractTextFromPdfAsync(fileStream); + case ".txt": + return await ExtractTextFromTxtAsync(fileStream); + case ".docx": + return await ExtractTextFromDocxAsync(fileStream); + case ".doc": + throw new NotSupportedException("Legacy .doc files are not supported. Please convert to .docx."); + case ".xlsx": + case ".xls": + return await ExtractTextFromExcelAsync(fileStream, fileExtension); + default: + throw new NotSupportedException($"File type {fileExtension} is not supported for text extraction."); + } + } + + private async Task ExtractTextFromPdfAsync(Stream fileStream) + { + var textBuilder = new StringBuilder(); + using (var reader = new PdfReader(fileStream)) + { + for (int i = 1; i <= reader.NumberOfPages; i++) + { + textBuilder.AppendLine(PdfTextExtractor.GetTextFromPage(reader, i)); + } + } + return SplitTextIntoChunks(textBuilder.ToString()); + } + + private async Task ExtractTextFromTxtAsync(Stream fileStream) + { + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) + { + var text = await reader.ReadToEndAsync(); + return SplitTextIntoChunks(text); + } + } + + private async Task ExtractTextFromDocxAsync(Stream fileStream) + { + var textBuilder = new StringBuilder(); + using (var document = WordprocessingDocument.Open(fileStream, false)) + { + var mainPart = document.MainDocumentPart; + if (mainPart != null) + { + var body = mainPart.Document.Body; + if (body != null) + { + foreach (var paragraph in body.Descendants()) + { + textBuilder.AppendLine(paragraph.InnerText); + } + } + } + } + return SplitTextIntoChunks(textBuilder.ToString()); + } + + private async Task ExtractTextFromExcelAsync(Stream fileStream, string fileExtension) + { + var textBuilder = new StringBuilder(); + IWorkbook workbook = null; + + if (fileExtension == ".xlsx") + { + workbook = new XSSFWorkbook(fileStream); + } + else if (fileExtension == ".xls") + { + workbook = new HSSFWorkbook(fileStream); + } + + if (workbook != null) + { + for (int sheetIndex = 0; sheetIndex < workbook.NumberOfSheets; sheetIndex++) + { + var sheet = workbook.GetSheetAt(sheetIndex); + textBuilder.AppendLine($"Sheet: {sheet.SheetName}"); + + for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++) + { + var row = sheet.GetRow(rowIndex); + if (row != null) + { + for (int cellIndex = 0; cellIndex < row.LastCellNum; cellIndex++) + { + var cell = row.GetCell(cellIndex); + if (cell != null) + { + textBuilder.Append(cell.ToString() + "\t"); + } + } + textBuilder.AppendLine(); + } + } + } + } + return SplitTextIntoChunks(textBuilder.ToString()); + } + + private string[] SplitTextIntoChunks(string text, int chunkSize = 1000) + { + var chunks = new List(); + int start = 0; + + while (start < text.Length) + { + int end = Math.Min(start + chunkSize, text.Length); + // Try to split at sentence boundary if possible + if (end < text.Length) + { + int lastPeriod = text.LastIndexOf('.', end, end - start); + if (lastPeriod > start) + { + end = lastPeriod + 1; + } + } + chunks.Add(text.Substring(start, end - start).Trim()); + start = end; + } + return chunks.ToArray(); + } + + // 添加向量生成功能(模拟实现,实际项目中应使用真实的嵌入服务) + private async Task GenerateEmbeddingAsync(string text) + { + // 这里是模拟实现,实际项目中应调用OpenAI或其他嵌入服务 + // 示例使用随机向量作为占位符 + var random = new Random(); + var embedding = new float[1536]; // 假设使用1536维度的向量 + for (int i = 0; i < embedding.Length; i++) + { + embedding[i] = (float)(random.NextDouble() * 2 - 1); // 范围 [-1, 1) + } + return embedding; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs index 926bfa4..6229963 100644 --- a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs +++ b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs @@ -1,109 +1,111 @@ -// using System.Data; -// using Microsoft.Extensions.Configuration; -// using Npgsql; -// using RAG.Domain.Repositories; -// using RAG.Domain.Entities; - -// namespace RAG.Infrastructure.VectorStore -// { -// public class PgVectorStore : IVectorStore -// { -// private readonly string _connectionString; -// private readonly string _tableName; - -// public PgVectorStore(IConfiguration configuration) -// { -// _connectionString = configuration.GetSection("VectorStore:ConnectionString").Value; -// _tableName = configuration.GetSection("VectorStore:TableName").Value ?? "vectors"; -// } - -// public async Task InitializeAsync() -// { -// // Create extension if not exists -// await ExecuteNonQueryAsync("CREATE EXTENSION IF NOT EXISTS vector"); - -// // Create table if not exists -// await ExecuteNonQueryAsync($@"CREATE TABLE IF NOT EXISTS {_tableName} ( -// id SERIAL PRIMARY KEY, -// content TEXT NOT NULL, -// metadata JSONB, -// embedding VECTOR(1536) NOT NULL -// )"); - -// // Create index if not exists -// await ExecuteNonQueryAsync($@"CREATE INDEX IF NOT EXISTS {_tableName}_embedding_idx ON {_tableName} USING ivfflat (embedding vector_cosine_ops)"); -// } - -// public async Task StoreVectorAsync(string content, float[] embedding, object metadata = null) -// { -// var metadataJson = metadata != null ? System.Text.Json.JsonSerializer.Serialize(metadata) : null; - -// await ExecuteNonQueryAsync($@"INSERT INTO {_tableName} (content, metadata, embedding) -// VALUES (@content, @metadata, @embedding)", -// new NpgsqlParameter("@content", content), -// new NpgsqlParameter("@metadata", metadataJson ?? (object)DBNull.Value), -// new NpgsqlParameter("@embedding", NpgsqlTypes.NpgsqlDbType.Vector) { Value = embedding }); -// } - -// public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5) -// { -// var results = new List(); - -// using (var connection = new NpgsqlConnection(_connectionString)) -// { -// await connection.OpenAsync(); - -// using (var command = new NpgsqlCommand($@"SELECT id, content, metadata, embedding <=> @queryVector AS similarity -// FROM {_tableName} -// ORDER BY similarity -// LIMIT @topK", connection)) -// { -// command.Parameters.AddWithValue("@queryVector", NpgsqlTypes.NpgsqlDbType.Vector, queryVector); -// command.Parameters.AddWithValue("@topK", topK); - -// using (var reader = await command.ExecuteReaderAsync()) -// { -// while (await reader.ReadAsync()) -// { -// results.Add(new VectorResult -// { -// Id = reader.GetInt32(0), -// Content = reader.GetString(1), -// Metadata = reader.IsDBNull(2) ? null : System.Text.Json.JsonSerializer.Deserialize>(reader.GetString(2)), -// Similarity = reader.GetDouble(3) -// }); -// } -// } -// } -// } - -// return results; -// } - -// private async Task ExecuteNonQueryAsync(string sql, params NpgsqlParameter[] parameters) -// { -// using (var connection = new NpgsqlConnection(_connectionString)) -// { -// await connection.OpenAsync(); - -// using (var command = new NpgsqlCommand(sql, connection)) -// { -// if (parameters != null) -// { -// command.Parameters.AddRange(parameters); -// } - -// await command.ExecuteNonQueryAsync(); -// } -// } -// } -// } - -// public class VectorResult -// { -// public int Id { get; set; } -// public string Content { get; set; } -// public Dictionary Metadata { get; set; } -// public double Similarity { get; set; } -// } -// } \ No newline at end of file +using System.Data; +using Microsoft.Extensions.Configuration; +using Npgsql; +using RAG.Domain.Repositories; +using RAG.Domain.Entities; + +namespace RAG.Infrastructure.VectorStore +{ + public class PgVectorStore : IVectorStore + + + { + private readonly string _connectionString; + private readonly string _tableName; + + public PgVectorStore(IConfiguration configuration) + { + _connectionString = configuration.GetSection("VectorStore:ConnectionString").Value; + _tableName = configuration.GetSection("VectorStore:TableName").Value ?? "vectors"; + } + + public async Task InitializeAsync() + { + // Create extension if not exists + await ExecuteNonQueryAsync("CREATE EXTENSION IF NOT EXISTS vector"); + + // Create table if not exists + await ExecuteNonQueryAsync($@"CREATE TABLE IF NOT EXISTS {_tableName} ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + metadata JSONB, + embedding VECTOR(1536) NOT NULL + )"); + + // Create index if not exists + await ExecuteNonQueryAsync($@"CREATE INDEX IF NOT EXISTS {_tableName}_embedding_idx ON {_tableName} USING ivfflat (embedding vector_cosine_ops)"); + } + + public async Task StoreVectorAsync(string content, float[] embedding, object metadata = null) + { + var metadataJson = metadata != null ? System.Text.Json.JsonSerializer.Serialize(metadata) : null; + + await ExecuteNonQueryAsync($@"INSERT INTO {_tableName} (content, metadata, embedding) + VALUES (@content, @metadata, @embedding)", + new NpgsqlParameter("@content", content), + new NpgsqlParameter("@metadata", metadataJson ?? (object)DBNull.Value), + new NpgsqlParameter("@embedding", NpgsqlTypes.NpgsqlDbType.Vector) { Value = embedding }); + } + + public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5) + { + var results = new List(); + + using (var connection = new NpgsqlConnection(_connectionString)) + { + await connection.OpenAsync(); + + using (var command = new NpgsqlCommand($@"SELECT id, content, metadata, embedding <=> @queryVector AS similarity + FROM {_tableName} + ORDER BY similarity + LIMIT @topK", connection)) + { + command.Parameters.AddWithValue("@queryVector", NpgsqlTypes.NpgsqlDbType.Vector, queryVector); + command.Parameters.AddWithValue("@topK", topK); + + using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + results.Add(new VectorResult + { + Id = reader.GetInt32(0), + Content = reader.GetString(1), + Metadata = reader.IsDBNull(2) ? null : System.Text.Json.JsonSerializer.Deserialize>(reader.GetString(2)), + Similarity = reader.GetDouble(3) + }); + } + } + } + } + + return results; + } + + private async Task ExecuteNonQueryAsync(string sql, params NpgsqlParameter[] parameters) + { + using (var connection = new NpgsqlConnection(_connectionString)) + { + await connection.OpenAsync(); + + using (var command = new NpgsqlCommand(sql, connection)) + { + if (parameters != null) + { + command.Parameters.AddRange(parameters); + } + + await command.ExecuteNonQueryAsync(); + } + } + } + } + + public class VectorResult + { + public int Id { get; set; } + public string Content { get; set; } + public Dictionary Metadata { get; set; } + public double Similarity { get; set; } + } +} \ No newline at end of file -- Gitee From 29dd0872036aba53d6ba3115b6220c16a99807d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Tue, 5 Aug 2025 15:44:10 +0800 Subject: [PATCH 04/31] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=88=90=E5=8A=9F=EF=BC=8C=E5=8F=AF=E4=BB=A5=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=8C=E4=BD=86=E8=BF=98?= =?UTF-8?q?=E6=9C=AA=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/DocumentController.cs | 58 --- backend/RAG.Api/Program.cs | 62 ++- backend/RAG.Api/RAG.Api.csproj | 8 +- backend/RAG.Api/appsettings.json | 2 +- .../Commands/UploadDocumentCommand.cs | 8 - .../Handlers/UploadDocumentHandler.cs | 66 --- .../RAG.Application/RAG.Application.csproj | 5 +- backend/RAG.Domain/Entities/ChatHistory.cs | 80 +++- backend/RAG.Domain/Entities/Document.cs | 87 ++-- backend/RAG.Domain/Entities/DocumentChunk.cs | 41 ++ backend/RAG.Domain/Entities/DocumentStatus.cs | 10 + .../Entities/{BaseEntity.cs => EntityBase.cs} | 8 +- backend/RAG.Domain/Entities/User.cs | 39 +- .../RAG.Domain/Events/DocumentCreatedEvent.cs | 15 + .../DocumentProcessingCompletedEvent.cs | 15 + .../Events/DocumentProcessingStartedEvent.cs | 15 + backend/RAG.Domain/RAG.Domain.csproj | 4 +- .../Repositories/IChatHistoryRepository.cs | 12 + .../Repositories/IDocumentChunkRepository.cs | 12 + .../Repositories/IDocumentRepository.cs | 13 + .../RAG.Domain/Repositories/IUnitOfWork.cs | 7 + .../Repositories/IUserRepository.cs | 10 + .../RAG.Domain/ValueObjects/AccessLevel.cs | 27 -- .../RAG.Domain/ValueObjects/DocumentTitle.cs | 25 - backend/RAG.Domain/ValueObjects/FileName.cs | 25 - backend/RAG.Domain/ValueObjects/FilePath.cs | 22 - backend/RAG.Domain/ValueObjects/FileSize.cs | 22 - backend/RAG.Domain/ValueObjects/FileType.cs | 22 - .../Data/ApplicationDbContext.cs | 160 ++++++- .../20250805073332_InitialCreate.Designer.cs | 427 ++++++++++++++++++ .../20250805073332_InitialCreate.cs | 324 +++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 424 +++++++++++++++++ .../RAG.Infrastructure.csproj | 18 +- .../Repositories/ApplicationUnitOfWork.cs | 34 -- .../Repositories/Repository.cs | 68 --- .../Services/DocumentProcessingService.cs | 244 ---------- .../VectorStore/PgVectorStore.cs | 111 ----- .../RAG.Tests/DomainTests/DocumentTests.cs | 0 backend/RAG.Tests/RAG.Tests.csproj | 5 +- 39 files changed, 1657 insertions(+), 878 deletions(-) create mode 100644 backend/RAG.Domain/Entities/DocumentChunk.cs create mode 100644 backend/RAG.Domain/Entities/DocumentStatus.cs rename backend/RAG.Domain/Entities/{BaseEntity.cs => EntityBase.cs} (67%) create mode 100644 backend/RAG.Domain/Events/DocumentCreatedEvent.cs create mode 100644 backend/RAG.Domain/Events/DocumentProcessingCompletedEvent.cs create mode 100644 backend/RAG.Domain/Events/DocumentProcessingStartedEvent.cs create mode 100644 backend/RAG.Domain/Repositories/IChatHistoryRepository.cs create mode 100644 backend/RAG.Domain/Repositories/IDocumentChunkRepository.cs create mode 100644 backend/RAG.Domain/Repositories/IDocumentRepository.cs create mode 100644 backend/RAG.Domain/Repositories/IUserRepository.cs delete mode 100644 backend/RAG.Domain/ValueObjects/AccessLevel.cs delete mode 100644 backend/RAG.Domain/ValueObjects/DocumentTitle.cs delete mode 100644 backend/RAG.Domain/ValueObjects/FileName.cs delete mode 100644 backend/RAG.Domain/ValueObjects/FilePath.cs delete mode 100644 backend/RAG.Domain/ValueObjects/FileSize.cs delete mode 100644 backend/RAG.Domain/ValueObjects/FileType.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs create mode 100644 backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 backend/RAG.Tests/DomainTests/DocumentTests.cs diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index 766cee6..e69de29 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -1,58 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Http; -using System.Threading.Tasks; -using MediatR; -using RAG.Application.Commands; -using RAG.Domain.ValueObjects; -using Microsoft.Extensions.Logging; - -namespace RAG.Api.Controllers -{ - [ApiController] - [Route("api/[controller]")] - public class DocumentController : ControllerBase - { - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public DocumentController( - IMediator mediator, - ILogger logger) - { - _mediator = mediator; - _logger = logger; - } - - [HttpPost("upload")] - public async Task UploadDocument(IFormFile file, [FromForm] string accessLevel = "internal") - { - try - { - if (file == null || file.Length == 0) - { - return BadRequest("No file uploaded."); - } - - // 创建访问级别值对象 - var accessLevelObj = AccessLevel.Create(accessLevel); - - // 发送上传文档命令 - var documentId = await _mediator.Send(new UploadDocumentCommand(file, accessLevelObj)); - - return Ok(new { DocumentId = documentId }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error uploading document"); - return StatusCode(500, "Internal server error"); - } - } - - [HttpGet("supported-types")] - public IActionResult GetSupportedFileTypes() - { - var supportedTypes = new[] { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; - return Ok(supportedTypes); - } - } -} \ No newline at end of file diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs index b21db8c..ac60a97 100644 --- a/backend/RAG.Api/Program.cs +++ b/backend/RAG.Api/Program.cs @@ -1,53 +1,44 @@ using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using RAG.Application.Commands; -using RAG.Application.Handlers; -using RAG.Domain.Repositories; -using RAG.Domain.Services; -using RAG.Infrastructure.Data; -using RAG.Infrastructure.Repositories; -using RAG.Infrastructure.Services; -using RAG.Infrastructure.VectorStore; +using Microsoft.EntityFrameworkCore; using MediatR; +using RAG.Infrastructure.Data; using System.Reflection; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllers(); - -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +// 配置数据库上下文 +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); -// Add MediatR -builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(UploadDocumentCommand).Assembly)); +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); -builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(UploadDocumentHandler).Assembly)); +// 配置MediatR +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(RAG.Application.Class1).Assembly)); -// Add DbContext -builder.Services.AddDbContext(options => - options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +// 配置AutoMapper +builder.Services.AddAutoMapper(cfg => cfg.AddMaps(Assembly.GetExecutingAssembly())); +builder.Services.AddAutoMapper(cfg => cfg.AddMaps(typeof(RAG.Application.Class1).Assembly)); -// Add Repositories and Unit of Work -builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); -builder.Services.AddScoped(); -// Add Services -builder.Services.AddScoped(); -builder.Services.AddScoped(); -// Add CORS builder.Services.AddCors(options => { - options.AddPolicy("AllowAll", policy => - { - policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); - }); + options.AddPolicy("AllowAll", + builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); }); var app = builder.Build(); @@ -55,15 +46,18 @@ var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors("AllowAll"); +app.UseAuthorization(); + app.MapControllers(); app.Run(); -public partial class Program {} +public partial class Program { } \ No newline at end of file diff --git a/backend/RAG.Api/RAG.Api.csproj b/backend/RAG.Api/RAG.Api.csproj index e7b12a3..2a17132 100644 --- a/backend/RAG.Api/RAG.Api.csproj +++ b/backend/RAG.Api/RAG.Api.csproj @@ -1,7 +1,7 @@ - net9.0 + net8.0 enable enable @@ -9,9 +9,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json index 147f30e..f7852dc 100644 --- a/backend/RAG.Api/appsettings.json +++ b/backend/RAG.Api/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=rag_db;Username=postgres;Password=your_password_here" + "DefaultConnection": "Host=localhost;port=5432;Database=rag_db;Username=postgres;Password=xyq0217" }, "VectorStore": { "Type": "Postgres", diff --git a/backend/RAG.Application/Commands/UploadDocumentCommand.cs b/backend/RAG.Application/Commands/UploadDocumentCommand.cs index f9cb20f..e69de29 100644 --- a/backend/RAG.Application/Commands/UploadDocumentCommand.cs +++ b/backend/RAG.Application/Commands/UploadDocumentCommand.cs @@ -1,8 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Http; -using RAG.Domain.ValueObjects; - -namespace RAG.Application.Commands -{ - public record UploadDocumentCommand(IFormFile File, AccessLevel AccessLevel) : IRequest; -} \ No newline at end of file diff --git a/backend/RAG.Application/Handlers/UploadDocumentHandler.cs b/backend/RAG.Application/Handlers/UploadDocumentHandler.cs index e0c7ef4..e69de29 100644 --- a/backend/RAG.Application/Handlers/UploadDocumentHandler.cs +++ b/backend/RAG.Application/Handlers/UploadDocumentHandler.cs @@ -1,66 +0,0 @@ -using MediatR; -using Microsoft.Extensions.Logging; -using RAG.Application.Commands; -using RAG.Domain.Entities; -using RAG.Domain.Repositories; -using RAG.Domain.Services; -using RAG.Domain.ValueObjects; - -namespace RAG.Application.Handlers -{ - public class UploadDocumentHandler : IRequestHandler - { - private readonly IDocumentProcessingService _documentProcessingService; - private readonly IRepository _documentRepository; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - - public UploadDocumentHandler( - IDocumentProcessingService documentProcessingService, - IRepository documentRepository, - IUnitOfWork unitOfWork, - ILogger logger) - { - _documentProcessingService = documentProcessingService; - _documentRepository = documentRepository; - _unitOfWork = unitOfWork; - _logger = logger; - } - - public async Task Handle(UploadDocumentCommand request, CancellationToken cancellationToken) - { - try - { - _logger.LogInformation("Handling document upload request"); - - // 获取当前用户ID(实际应用中应从身份验证上下文中获取) - var userId = 1; // 占位符 - - // 使用文档处理服务处理上传的文件 - using (var stream = request.File.OpenReadStream()) - { - var document = await _documentProcessingService.ProcessDocumentAsync( - stream, - request.File.FileName, - request.File.ContentType, - userId); - - // 设置文档访问级别 - document.AccessLevel = request.AccessLevel.Value; - - // 保存文档到数据库 - await _documentRepository.AddAsync(document); - await _unitOfWork.SaveChangesAsync(); - - _logger.LogInformation("Document uploaded successfully with ID: {DocumentId}", document.Id); - return document.Id; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error handling document upload request"); - throw; - } - } - } -} \ No newline at end of file diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj index 748d7dd..c826abf 100644 --- a/backend/RAG.Application/RAG.Application.csproj +++ b/backend/RAG.Application/RAG.Application.csproj @@ -1,4 +1,4 @@ - + @@ -8,10 +8,11 @@ + - net9.0 + net8.0 enable enable diff --git a/backend/RAG.Domain/Entities/ChatHistory.cs b/backend/RAG.Domain/Entities/ChatHistory.cs index 5c7c5bd..d6e2ac8 100644 --- a/backend/RAG.Domain/Entities/ChatHistory.cs +++ b/backend/RAG.Domain/Entities/ChatHistory.cs @@ -1,37 +1,71 @@ using System; using System.Collections.Generic; +using System; namespace RAG.Domain.Entities { - public class ChatHistory + public class ChatHistory : EntityBase { - public int Id { get; set; } - public string SessionId { get; set; } - public int UserId { get; set; } - public User User { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - public ICollection Messages { get; set; } = new List(); + private ChatHistory() { } + + public static ChatHistory Create(long userId, string sessionId) + { + return new ChatHistory + { + Id = new Random().NextInt64(1, long.MaxValue), + SessionId = sessionId, + UserId = userId + }; + } + + public string SessionId { get; private set; } + public long UserId { get; private set; } + public User User { get; private set; } + public ICollection Messages { get; private set; } = new List(); } - public class ChatMessage + public class ChatMessage : EntityBase { - public int Id { get; set; } - public int ChatHistoryId { get; set; } - public ChatHistory ChatHistory { get; set; } - public string Content { get; set; } - public bool IsUserMessage { get; set; } - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - public ICollection References { get; set; } = new List(); + private ChatMessage() { } + + public static ChatMessage Create(long chatHistoryId, string content, bool isUserMessage) + { + return new ChatMessage + { + Id = new Random().NextInt64(1, long.MaxValue), + ChatHistoryId = chatHistoryId, + Content = content, + IsUserMessage = isUserMessage + }; + } + + public long ChatHistoryId { get; private set; } + public ChatHistory ChatHistory { get; private set; } + public string Content { get; private set; } + public bool IsUserMessage { get; private set; } + public DateTime Timestamp { get; private set; } = DateTime.UtcNow; + public ICollection References { get; private set; } = new List(); } - public class MessageReference + public class MessageReference : EntityBase { - public int Id { get; set; } - public int ChatMessageId { get; set; } - public ChatMessage ChatMessage { get; set; } - public int DocumentChunkId { get; set; } - public DocumentChunk DocumentChunk { get; set; } - public double RelevanceScore { get; set; } + private MessageReference() { } + + public static MessageReference Create(long chatMessageId, long documentChunkId, double relevanceScore) + { + return new MessageReference + { + Id = new Random().NextInt64(1, long.MaxValue), + ChatMessageId = chatMessageId, + DocumentChunkId = documentChunkId, + RelevanceScore = relevanceScore + }; + } + + public long ChatMessageId { get; private set; } + public ChatMessage ChatMessage { get; private set; } + public long DocumentChunkId { get; private set; } + public DocumentChunk DocumentChunk { get; private set; } + public double RelevanceScore { get; private set; } } } \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs index 1cb36ed..52d5864 100644 --- a/backend/RAG.Domain/Entities/Document.cs +++ b/backend/RAG.Domain/Entities/Document.cs @@ -1,11 +1,10 @@ +// RAG.Domain/Entities/Document.cs using System; -using System.Collections.Generic; -using RAG.Domain.ValueObjects; using RAG.Domain.Events; namespace RAG.Domain.Entities { - public class Document : BaseEntity + public class Document : EntityBase { private readonly List _chunks = new(); @@ -14,24 +13,36 @@ namespace RAG.Domain.Entities // 工厂方法 public static Document Create( - DocumentTitle title, - FileName fileName, - FilePath filePath, - FileType fileType, - FileSize fileSize, - AccessLevel accessLevel) + string title, + string fileName, + string filePath, + string fileType, + long fileSize, + string accessLevel) { + if (string.IsNullOrWhiteSpace(title)) + throw new ArgumentException("Title cannot be empty"); + if (string.IsNullOrWhiteSpace(fileName)) + throw new ArgumentException("File name cannot be empty"); + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentException("File path cannot be empty"); + if (string.IsNullOrWhiteSpace(fileType)) + throw new ArgumentException("File type cannot be empty"); + if (fileSize < 0) + throw new ArgumentException("File size cannot be negative"); + if (string.IsNullOrWhiteSpace(accessLevel) || (accessLevel.ToLower() != "internal" && accessLevel.ToLower() != "public")) + throw new ArgumentException("Access level must be 'internal' or 'public'"); + var document = new Document { + Id = new Random().NextInt64(1, long.MaxValue), Title = title, FileName = fileName, FilePath = filePath, FileType = fileType, FileSize = fileSize, - AccessLevel = accessLevel, - Status = "pending", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow + AccessLevel = accessLevel.ToLower(), + Status = DocumentStatus.Pending }; // 发布领域事件 @@ -40,30 +51,24 @@ namespace RAG.Domain.Entities return document; } - public int Id { get; private set; } - public DocumentTitle Title { get; private set; } - public FileName FileName { get; private set; } - public FilePath FilePath { get; private set; } - public FileType FileType { get; private set; } - public FileSize FileSize { get; private set; } - public string Description { get; set; } - public string Tags { get; set; } - public AccessLevel AccessLevel { get; private set; } - public string Status { get; private set; } - public DateTime CreatedAt { get; private set; } - public DateTime UpdatedAt { get; private set; } - public int UserId { get; set; } - public User User { get; set; } + public long Id { get; private set; } + public string Title { get; private set; } + public string FileName { get; private set; } + public string FilePath { get; private set; } + public string FileType { get; private set; } + public long FileSize { get; private set; } + public string AccessLevel { get; private set; } + public DocumentStatus Status { get; private set; } public IReadOnlyList Chunks => _chunks.AsReadOnly(); // 业务方法 public void UpdateProcessingStatus() { - if (Status != "pending") + if (Status != DocumentStatus.Pending) throw new InvalidOperationException("Document must be in pending status"); - Status = "processing"; + Status = DocumentStatus.Processing; UpdatedAt = DateTime.UtcNow; AddDomainEvent(new DocumentProcessingStartedEvent(Id)); @@ -71,10 +76,10 @@ namespace RAG.Domain.Entities public void CompleteProcessing() { - if (Status != "processing") + if (Status != DocumentStatus.Processing) throw new InvalidOperationException("Document must be in processing status"); - Status = "completed"; + Status = DocumentStatus.Completed; UpdatedAt = DateTime.UtcNow; AddDomainEvent(new DocumentProcessingCompletedEvent(Id)); @@ -88,24 +93,4 @@ namespace RAG.Domain.Entities _chunks.Add(chunk); } } - - public class DocumentChunk - { - public int Id { get; set; } - public int DocumentId { get; set; } - public Document Document { get; set; } - public string Content { get; set; } - public int Position { get; set; } - public int TokenCount { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - } - - public class Vector - { - public int Id { get; set; } - public int ChunkId { get; set; } - public DocumentChunk Chunk { get; set; } - public float[] Embedding { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - } } \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/DocumentChunk.cs b/backend/RAG.Domain/Entities/DocumentChunk.cs new file mode 100644 index 0000000..c69c10e --- /dev/null +++ b/backend/RAG.Domain/Entities/DocumentChunk.cs @@ -0,0 +1,41 @@ + + +using System; + +namespace RAG.Domain.Entities +{ + public class DocumentChunk : EntityBase + { + private DocumentChunk() { } + + public static DocumentChunk Create( + long documentId, + string content, + int position, + int tokenCount) + { + if (documentId <= 0) + throw new ArgumentException("Document ID must be greater than 0"); + if (string.IsNullOrWhiteSpace(content)) + throw new ArgumentException("Content cannot be empty"); + if (position < 0) + throw new ArgumentException("Position cannot be negative"); + if (tokenCount < 0) + throw new ArgumentException("Token count cannot be negative"); + + return new DocumentChunk + { + Id = new Random().NextInt64(1, long.MaxValue), + DocumentId = documentId, + Content = content, + Position = position, + TokenCount = tokenCount + }; + } + + public long DocumentId { get; private set; } + public string Content { get; private set; } + public int Position { get; private set; } + public int TokenCount { get; private set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/DocumentStatus.cs b/backend/RAG.Domain/Entities/DocumentStatus.cs new file mode 100644 index 0000000..5aec26f --- /dev/null +++ b/backend/RAG.Domain/Entities/DocumentStatus.cs @@ -0,0 +1,10 @@ +namespace RAG.Domain.Entities +{ + public enum DocumentStatus + { + Pending, + Processing, + Completed, + Failed + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/BaseEntity.cs b/backend/RAG.Domain/Entities/EntityBase.cs similarity index 67% rename from backend/RAG.Domain/Entities/BaseEntity.cs rename to backend/RAG.Domain/Entities/EntityBase.cs index 092db55..2a2ab07 100644 --- a/backend/RAG.Domain/Entities/BaseEntity.cs +++ b/backend/RAG.Domain/Entities/EntityBase.cs @@ -1,17 +1,21 @@ -using System; using System.Collections.Generic; namespace RAG.Domain.Entities { - public abstract class BaseEntity + public abstract class EntityBase { private readonly List _domainEvents = new(); + public long Id { get; protected set; } + public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; protected set; } = DateTime.UtcNow; + public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); protected void AddDomainEvent(DomainEvent domainEvent) { _domainEvents.Add(domainEvent); + UpdatedAt = DateTime.UtcNow; } public void ClearDomainEvents() diff --git a/backend/RAG.Domain/Entities/User.cs b/backend/RAG.Domain/Entities/User.cs index b7787a3..b78bf42 100644 --- a/backend/RAG.Domain/Entities/User.cs +++ b/backend/RAG.Domain/Entities/User.cs @@ -1,19 +1,36 @@ using System; using System.Collections.Generic; +using System; namespace RAG.Domain.Entities { - public class User + public class User : EntityBase { - public int Id { get; set; } - public string Username { get; set; } - public string Email { get; set; } - public string PasswordHash { get; set; } - public string FullName { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - public bool IsActive { get; set; } = true; - public ICollection Documents { get; set; } = new List(); - public ICollection ChatHistories { get; set; } = new List(); + private User() { } + + public static User Create( + string username, + string email, + string passwordHash, + string fullName) + { + return new User + { + Id = new Random().NextInt64(1, long.MaxValue), + Username = username, + Email = email, + PasswordHash = passwordHash, + FullName = fullName, + IsActive = true + }; + } + + public string Username { get; private set; } + public string Email { get; private set; } + public string PasswordHash { get; private set; } + public string FullName { get; private set; } + public bool IsActive { get; private set; } + public ICollection Documents { get; private set; } = new List(); + public ICollection ChatHistories { get; private set; } = new List(); } } \ No newline at end of file diff --git a/backend/RAG.Domain/Events/DocumentCreatedEvent.cs b/backend/RAG.Domain/Events/DocumentCreatedEvent.cs new file mode 100644 index 0000000..3ef12d0 --- /dev/null +++ b/backend/RAG.Domain/Events/DocumentCreatedEvent.cs @@ -0,0 +1,15 @@ +using RAG.Domain.Entities; +namespace RAG.Domain.Events +{ + public class DocumentCreatedEvent : DomainEvent + { + public long DocumentId { get; } + + public DocumentCreatedEvent(long documentId) + { + if (documentId <= 0) + throw new ArgumentException("Document ID must be greater than 0"); + DocumentId = documentId; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Events/DocumentProcessingCompletedEvent.cs b/backend/RAG.Domain/Events/DocumentProcessingCompletedEvent.cs new file mode 100644 index 0000000..cd7258a --- /dev/null +++ b/backend/RAG.Domain/Events/DocumentProcessingCompletedEvent.cs @@ -0,0 +1,15 @@ +using RAG.Domain.Entities; +namespace RAG.Domain.Events +{ + public class DocumentProcessingCompletedEvent : DomainEvent + { + public long DocumentId { get; } + + public DocumentProcessingCompletedEvent(long documentId) + { + if (documentId <= 0) + throw new ArgumentException("Document ID must be greater than 0"); + DocumentId = documentId; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Events/DocumentProcessingStartedEvent.cs b/backend/RAG.Domain/Events/DocumentProcessingStartedEvent.cs new file mode 100644 index 0000000..8fa972c --- /dev/null +++ b/backend/RAG.Domain/Events/DocumentProcessingStartedEvent.cs @@ -0,0 +1,15 @@ +using RAG.Domain.Entities; +namespace RAG.Domain.Events +{ + public class DocumentProcessingStartedEvent : DomainEvent + { + public long DocumentId { get; } + + public DocumentProcessingStartedEvent(long documentId) + { + if (documentId <= 0) + throw new ArgumentException("Document ID must be greater than 0"); + DocumentId = documentId; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/RAG.Domain.csproj b/backend/RAG.Domain/RAG.Domain.csproj index 125f4c9..30402ac 100644 --- a/backend/RAG.Domain/RAG.Domain.csproj +++ b/backend/RAG.Domain/RAG.Domain.csproj @@ -1,7 +1,7 @@ - + - net9.0 + net8.0 enable enable diff --git a/backend/RAG.Domain/Repositories/IChatHistoryRepository.cs b/backend/RAG.Domain/Repositories/IChatHistoryRepository.cs new file mode 100644 index 0000000..ef230eb --- /dev/null +++ b/backend/RAG.Domain/Repositories/IChatHistoryRepository.cs @@ -0,0 +1,12 @@ +using RAG.Domain.Entities; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RAG.Domain.Repositories +{ + public interface IChatHistoryRepository : IRepository + { + Task> GetByUserIdAsync(long userId); + Task CountByUserIdAsync(long userId); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IDocumentChunkRepository.cs b/backend/RAG.Domain/Repositories/IDocumentChunkRepository.cs new file mode 100644 index 0000000..d74f7a7 --- /dev/null +++ b/backend/RAG.Domain/Repositories/IDocumentChunkRepository.cs @@ -0,0 +1,12 @@ +using RAG.Domain.Entities; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RAG.Domain.Repositories +{ + public interface IDocumentChunkRepository : IRepository + { + Task> GetByDocumentIdAsync(long documentId); + Task CountByDocumentIdAsync(long documentId); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IDocumentRepository.cs b/backend/RAG.Domain/Repositories/IDocumentRepository.cs new file mode 100644 index 0000000..86f8eba --- /dev/null +++ b/backend/RAG.Domain/Repositories/IDocumentRepository.cs @@ -0,0 +1,13 @@ +using RAG.Domain.Entities; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RAG.Domain.Repositories +{ + public interface IDocumentRepository : IRepository + { + Task> GetByStatusAsync(DocumentStatus status); + Task> GetByAccessLevelAsync(string accessLevel); + Task CountByStatusAsync(DocumentStatus status); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IUnitOfWork.cs b/backend/RAG.Domain/Repositories/IUnitOfWork.cs index 20429ac..b655f93 100644 --- a/backend/RAG.Domain/Repositories/IUnitOfWork.cs +++ b/backend/RAG.Domain/Repositories/IUnitOfWork.cs @@ -1,10 +1,17 @@ using System.Threading.Tasks; +using RAG.Domain.Entities; + namespace RAG.Domain.Repositories { public interface IUnitOfWork { IRepository GetRepository() where TEntity : class; Task SaveChangesAsync(); + + IUserRepository Users { get; } + IDocumentRepository Documents { get; } + IChatHistoryRepository ChatHistories { get; } + IDocumentChunkRepository DocumentChunks { get; } } } \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IUserRepository.cs b/backend/RAG.Domain/Repositories/IUserRepository.cs new file mode 100644 index 0000000..add4a52 --- /dev/null +++ b/backend/RAG.Domain/Repositories/IUserRepository.cs @@ -0,0 +1,10 @@ +using RAG.Domain.Entities; + +namespace RAG.Domain.Repositories +{ + public interface IUserRepository : IRepository + { + Task GetByUsernameAsync(string username); + Task GetByEmailAsync(string email); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/AccessLevel.cs b/backend/RAG.Domain/ValueObjects/AccessLevel.cs deleted file mode 100644 index 633f3a0..0000000 --- a/backend/RAG.Domain/ValueObjects/AccessLevel.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace RAG.Domain.ValueObjects -{ - public record AccessLevel - { - public string Value { get; init; } - - private AccessLevel(string value) - { - Value = value; - } - - public static AccessLevel Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Access level cannot be empty"); - - value = value.ToLowerInvariant(); - - if (value != "internal" && value != "public") - throw new ArgumentException("Access level must be 'internal' or 'public'"); - - return new AccessLevel(value); - } - - public static implicit operator string(AccessLevel accessLevel) => accessLevel.Value; - } -} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/DocumentTitle.cs b/backend/RAG.Domain/ValueObjects/DocumentTitle.cs deleted file mode 100644 index 50ddd1f..0000000 --- a/backend/RAG.Domain/ValueObjects/DocumentTitle.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace RAG.Domain.ValueObjects -{ - public record DocumentTitle - { - public string Value { get; init; } - - private DocumentTitle(string value) - { - Value = value; - } - - public static DocumentTitle Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Document title cannot be empty"); - - if (value.Length > 255) - throw new ArgumentException("Document title cannot exceed 255 characters"); - - return new DocumentTitle(value); - } - - public static implicit operator string(DocumentTitle title) => title.Value; - } -} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FileName.cs b/backend/RAG.Domain/ValueObjects/FileName.cs deleted file mode 100644 index 22aa95a..0000000 --- a/backend/RAG.Domain/ValueObjects/FileName.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace RAG.Domain.ValueObjects -{ - public record FileName - { - public string Value { get; init; } - - private FileName(string value) - { - Value = value; - } - - public static FileName Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("File name cannot be empty"); - - if (value.Length > 255) - throw new ArgumentException("File name cannot exceed 255 characters"); - - return new FileName(value); - } - - public static implicit operator string(FileName fileName) => fileName.Value; - } -} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FilePath.cs b/backend/RAG.Domain/ValueObjects/FilePath.cs deleted file mode 100644 index 7263ee4..0000000 --- a/backend/RAG.Domain/ValueObjects/FilePath.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace RAG.Domain.ValueObjects -{ - public record FilePath - { - public string Value { get; init; } - - private FilePath(string value) - { - Value = value; - } - - public static FilePath Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("File path cannot be empty"); - - return new FilePath(value); - } - - public static implicit operator string(FilePath filePath) => filePath.Value; - } -} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FileSize.cs b/backend/RAG.Domain/ValueObjects/FileSize.cs deleted file mode 100644 index 63f5288..0000000 --- a/backend/RAG.Domain/ValueObjects/FileSize.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace RAG.Domain.ValueObjects -{ - public record FileSize - { - public long Value { get; init; } - - private FileSize(long value) - { - Value = value; - } - - public static FileSize Create(long value) - { - if (value < 0) - throw new ArgumentException("File size cannot be negative"); - - return new FileSize(value); - } - - public static implicit operator long(FileSize fileSize) => fileSize.Value; - } -} \ No newline at end of file diff --git a/backend/RAG.Domain/ValueObjects/FileType.cs b/backend/RAG.Domain/ValueObjects/FileType.cs deleted file mode 100644 index 3117187..0000000 --- a/backend/RAG.Domain/ValueObjects/FileType.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace RAG.Domain.ValueObjects -{ - public record FileType - { - public string Value { get; init; } - - private FileType(string value) - { - Value = value; - } - - public static FileType Create(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("File type cannot be empty"); - - return new FileType(value.ToLowerInvariant()); - } - - public static implicit operator string(FileType fileType) => fileType.Value; - } -} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index e6f1a71..d4b2a5b 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -5,22 +5,168 @@ namespace RAG.Infrastructure.Data { public class ApplicationDbContext : DbContext { - public ApplicationDbContext(DbContextOptions options) - : base(options) + public ApplicationDbContext(DbContextOptions options) : base(options) {} - // DbSets for your entities + // 核心数据表 + public DbSet Users { get; set; } public DbSet Documents { get; set; } + public DbSet DocumentChunks { get; set; } public DbSet ChatHistories { get; set; } - public DbSet Users { get; set; } + public DbSet ChatMessages { get; set; } + public DbSet MessageReferences { get; set; } + + // 向量存储表 + public DbSet VectorStores { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // Configure entity mappings if needed - modelBuilder.Entity() - .HasIndex(d => d.Title); + // 忽略所有实体的DomainEvents属性 + modelBuilder.Entity().Ignore(u => u.DomainEvents); + modelBuilder.Entity().Ignore(d => d.DomainEvents); + modelBuilder.Entity().Ignore(dc => dc.DomainEvents); + modelBuilder.Entity().Ignore(ch => ch.DomainEvents); + modelBuilder.Entity().Ignore(cm => cm.DomainEvents); + modelBuilder.Entity().Ignore(mr => mr.DomainEvents); + + // 配置User实体 + modelBuilder.Entity(entity => + { + entity.HasKey(u => u.Id); + entity.Property(u => u.Username).IsRequired().HasMaxLength(50); + entity.Property(u => u.Email).IsRequired().HasMaxLength(100); + entity.Property(u => u.PasswordHash).IsRequired().HasMaxLength(255); + entity.Property(u => u.FullName).HasMaxLength(100); + entity.Property(u => u.IsActive).IsRequired(); + + // 索引 + entity.HasIndex(u => u.Username).IsUnique(); + entity.HasIndex(u => u.Email).IsUnique(); + }); + + // 配置Document实体 + modelBuilder.Entity(entity => + { + entity.HasKey(d => d.Id); + entity.Property(d => d.Title).IsRequired().HasMaxLength(200); + entity.Property(d => d.FileName).IsRequired().HasMaxLength(255); + entity.Property(d => d.FilePath).IsRequired().HasMaxLength(500); + entity.Property(d => d.FileType).IsRequired().HasMaxLength(50); + entity.Property(d => d.FileSize).IsRequired(); + entity.Property(d => d.AccessLevel).IsRequired().HasMaxLength(20); + entity.Property(d => d.Status).IsRequired(); + + // 索引 + entity.HasIndex(d => d.Status); + entity.HasIndex(d => d.AccessLevel); + }); + + // 配置DocumentChunk实体 + modelBuilder.Entity(entity => + { + entity.HasKey(dc => dc.Id); + entity.Property(dc => dc.DocumentId).IsRequired(); + entity.Property(dc => dc.Content).IsRequired(); + entity.Property(dc => dc.Position).IsRequired(); + entity.Property(dc => dc.TokenCount).IsRequired(); + + // 关系 + entity.HasOne() + .WithMany(d => d.Chunks) + .HasForeignKey(dc => dc.DocumentId) + .OnDelete(DeleteBehavior.Cascade); + + // 索引 + entity.HasIndex(dc => dc.DocumentId); + }); + + // 配置ChatHistory实体 + modelBuilder.Entity(entity => + { + entity.HasKey(ch => ch.Id); + entity.Property(ch => ch.SessionId).IsRequired().HasMaxLength(100); + entity.Property(ch => ch.UserId).IsRequired(); + + // 关系 + entity.HasOne() + .WithMany(u => u.ChatHistories) + .HasForeignKey(ch => ch.UserId) + .OnDelete(DeleteBehavior.Cascade); + + // 索引 + entity.HasIndex(ch => ch.SessionId).IsUnique(); + entity.HasIndex(ch => ch.UserId); + }); + + // 配置ChatMessage实体 + modelBuilder.Entity(entity => + { + entity.HasKey(cm => cm.Id); + entity.Property(cm => cm.ChatHistoryId).IsRequired(); + entity.Property(cm => cm.Content).IsRequired(); + entity.Property(cm => cm.IsUserMessage).IsRequired(); + entity.Property(cm => cm.Timestamp).IsRequired(); + + // 关系 + entity.HasOne() + .WithMany(ch => ch.Messages) + .HasForeignKey(cm => cm.ChatHistoryId) + .OnDelete(DeleteBehavior.Cascade); + + // 索引 + entity.HasIndex(cm => cm.ChatHistoryId); + entity.HasIndex(cm => cm.Timestamp); + }); + + // 配置MessageReference实体 + modelBuilder.Entity(entity => + { + entity.HasKey(mr => mr.Id); + entity.Property(mr => mr.ChatMessageId).IsRequired(); + entity.Property(mr => mr.DocumentChunkId).IsRequired(); + entity.Property(mr => mr.RelevanceScore).IsRequired(); + + // 关系 + entity.HasOne() + .WithMany(cm => cm.References) + .HasForeignKey(mr => mr.ChatMessageId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne() + .WithMany() + .HasForeignKey(mr => mr.DocumentChunkId) + .OnDelete(DeleteBehavior.Cascade); + + // 索引 + entity.HasIndex(mr => new { mr.ChatMessageId, mr.DocumentChunkId }).IsUnique(); + }); + + // 配置VectorStore实体 + modelBuilder.Entity(entity => + { + entity.HasKey(vs => vs.Id); + entity.Property(vs => vs.DocumentChunkId).IsRequired(); + entity.Property(vs => vs.Vector).IsRequired(); + + // 关系 + entity.HasOne() + .WithMany() + .HasForeignKey(vs => vs.DocumentChunkId) + .OnDelete(DeleteBehavior.Cascade); + + // 索引 + entity.HasIndex(vs => vs.DocumentChunkId).IsUnique(); + }); } } + + // 向量存储实体 + public class VectorStore + { + public long Id { get; set; } + public long DocumentChunkId { get; set; } + public byte[] Vector { get; set; } + } } \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs new file mode 100644 index 0000000..e6132e5 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs @@ -0,0 +1,427 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250805073332_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs b/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs new file mode 100644 index 0000000..277e645 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs @@ -0,0 +1,324 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Username = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + FullName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ChatHistories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + UserId1 = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatHistories", x => x.Id); + table.ForeignKey( + name: "FK_ChatHistories_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ChatHistories_Users_UserId1", + column: x => x.UserId1, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Documents", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + FileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + FilePath = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + FileType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + FileSize = table.Column(type: "bigint", nullable: false), + AccessLevel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Status = table.Column(type: "integer", nullable: false), + UserId = table.Column(type: "bigint", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Documents", x => x.Id); + table.ForeignKey( + name: "FK_Documents_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ChatMessages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ChatHistoryId = table.Column(type: "bigint", nullable: false), + ChatHistoryId1 = table.Column(type: "bigint", nullable: false), + Content = table.Column(type: "text", nullable: false), + IsUserMessage = table.Column(type: "boolean", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatMessages", x => x.Id); + table.ForeignKey( + name: "FK_ChatMessages_ChatHistories_ChatHistoryId", + column: x => x.ChatHistoryId, + principalTable: "ChatHistories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ChatMessages_ChatHistories_ChatHistoryId1", + column: x => x.ChatHistoryId1, + principalTable: "ChatHistories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DocumentChunks", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DocumentId = table.Column(type: "bigint", nullable: false), + Content = table.Column(type: "text", nullable: false), + Position = table.Column(type: "integer", nullable: false), + TokenCount = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DocumentChunks", x => x.Id); + table.ForeignKey( + name: "FK_DocumentChunks_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MessageReferences", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ChatMessageId = table.Column(type: "bigint", nullable: false), + ChatMessageId1 = table.Column(type: "bigint", nullable: false), + DocumentChunkId = table.Column(type: "bigint", nullable: false), + DocumentChunkId1 = table.Column(type: "bigint", nullable: false), + RelevanceScore = table.Column(type: "double precision", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MessageReferences", x => x.Id); + table.ForeignKey( + name: "FK_MessageReferences_ChatMessages_ChatMessageId", + column: x => x.ChatMessageId, + principalTable: "ChatMessages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MessageReferences_ChatMessages_ChatMessageId1", + column: x => x.ChatMessageId1, + principalTable: "ChatMessages", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MessageReferences_DocumentChunks_DocumentChunkId", + column: x => x.DocumentChunkId, + principalTable: "DocumentChunks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MessageReferences_DocumentChunks_DocumentChunkId1", + column: x => x.DocumentChunkId1, + principalTable: "DocumentChunks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "VectorStores", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DocumentChunkId = table.Column(type: "bigint", nullable: false), + Vector = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VectorStores", x => x.Id); + table.ForeignKey( + name: "FK_VectorStores_DocumentChunks_DocumentChunkId", + column: x => x.DocumentChunkId, + principalTable: "DocumentChunks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ChatHistories_SessionId", + table: "ChatHistories", + column: "SessionId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ChatHistories_UserId", + table: "ChatHistories", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_ChatHistories_UserId1", + table: "ChatHistories", + column: "UserId1"); + + migrationBuilder.CreateIndex( + name: "IX_ChatMessages_ChatHistoryId", + table: "ChatMessages", + column: "ChatHistoryId"); + + migrationBuilder.CreateIndex( + name: "IX_ChatMessages_ChatHistoryId1", + table: "ChatMessages", + column: "ChatHistoryId1"); + + migrationBuilder.CreateIndex( + name: "IX_ChatMessages_Timestamp", + table: "ChatMessages", + column: "Timestamp"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentChunks_DocumentId", + table: "DocumentChunks", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_AccessLevel", + table: "Documents", + column: "AccessLevel"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_Status", + table: "Documents", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_UserId", + table: "Documents", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_MessageReferences_ChatMessageId_DocumentChunkId", + table: "MessageReferences", + columns: new[] { "ChatMessageId", "DocumentChunkId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MessageReferences_ChatMessageId1", + table: "MessageReferences", + column: "ChatMessageId1"); + + migrationBuilder.CreateIndex( + name: "IX_MessageReferences_DocumentChunkId", + table: "MessageReferences", + column: "DocumentChunkId"); + + migrationBuilder.CreateIndex( + name: "IX_MessageReferences_DocumentChunkId1", + table: "MessageReferences", + column: "DocumentChunkId1"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_VectorStores_DocumentChunkId", + table: "VectorStores", + column: "DocumentChunkId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MessageReferences"); + + migrationBuilder.DropTable( + name: "VectorStores"); + + migrationBuilder.DropTable( + name: "ChatMessages"); + + migrationBuilder.DropTable( + name: "DocumentChunks"); + + migrationBuilder.DropTable( + name: "ChatHistories"); + + migrationBuilder.DropTable( + name: "Documents"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..514b6d6 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,424 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index a673a92..74d9204 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -1,4 +1,4 @@ - + @@ -6,18 +6,22 @@ - - - - - + + + + + - net9.0 + net8.0 enable enable + + + + diff --git a/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs index 4bbe243..e69de29 100644 --- a/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs +++ b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RAG.Domain.Repositories; -using RAG.Infrastructure.Data; - -namespace RAG.Infrastructure.Repositories -{ - public class ApplicationUnitOfWork : IUnitOfWork - { - private readonly ApplicationDbContext _context; - private readonly Dictionary _repositories = new(); - - public ApplicationUnitOfWork(ApplicationDbContext context) - { - _context = context; - } - - public IRepository GetRepository() where TEntity : class - { - var type = typeof(TEntity); - if (!_repositories.ContainsKey(type)) - { - var repositoryType = typeof(Repository<>).MakeGenericType(type); - var repository = Activator.CreateInstance(repositoryType, _context); - _repositories[type] = repository; - } - return (IRepository)_repositories[type]; - } - - public async Task SaveChangesAsync() - { - return await _context.SaveChangesAsync(); - } - } -} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/Repository.cs b/backend/RAG.Infrastructure/Repositories/Repository.cs index c306450..e69de29 100644 --- a/backend/RAG.Infrastructure/Repositories/Repository.cs +++ b/backend/RAG.Infrastructure/Repositories/Repository.cs @@ -1,68 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RAG.Domain.Repositories; -using RAG.Infrastructure.Data; -using System.Linq.Expressions; - -namespace RAG.Infrastructure.Repositories -{ - public class Repository : IRepository where TEntity : class - { - protected readonly ApplicationDbContext _context; - protected readonly DbSet _dbSet; - - public Repository(ApplicationDbContext context) - { - _context = context; - _dbSet = context.Set(); - } - - public async Task GetByIdAsync(int id) - { - return await _dbSet.FindAsync(id); - } - - public async Task> GetAllAsync() - { - return await _dbSet.ToListAsync(); - } - - public async Task> FindAsync(Expression> predicate) - { - return await _dbSet.Where(predicate).ToListAsync(); - } - - public async Task SingleOrDefaultAsync(Expression> predicate) - { - return await _dbSet.SingleOrDefaultAsync(predicate); - } - - public async Task AddAsync(TEntity entity) - { - await _dbSet.AddAsync(entity); - } - - public async Task AddRangeAsync(IEnumerable entities) - { - await _dbSet.AddRangeAsync(entities); - } - - public void Remove(TEntity entity) - { - _dbSet.Remove(entity); - } - - public void RemoveRange(IEnumerable entities) - { - _dbSet.RemoveRange(entities); - } - - public async Task CountAsync(Expression> predicate = null) - { - if (predicate == null) - { - return await _dbSet.CountAsync(); - } - return await _dbSet.CountAsync(predicate); - } - } -} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs index bbd0f53..e69de29 100644 --- a/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs +++ b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs @@ -1,244 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using iTextSharp.text.pdf; -using iTextSharp.text.pdf.parser; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Configuration; -using RAG.Domain.Entities; -using RAG.Domain.Repositories; -using RAG.Domain.Services; -using System.Text; -using NPOI.XSSF.UserModel; -using NPOI.HSSF.UserModel; -using NPOI.SS.UserModel; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Wordprocessing; - -namespace RAG.Infrastructure.Services -{ - public class DocumentProcessingService : IDocumentProcessingService - { - private readonly ILogger _logger; - private readonly IVectorStore _vectorStore; - private readonly IConfiguration _configuration; - - public DocumentProcessingService( - ILogger logger, - IVectorStore vectorStore, - IConfiguration configuration) - { - _logger = logger; - _vectorStore = vectorStore; - _configuration = configuration; - } - - public async Task ProcessDocumentAsync(Stream fileStream, string fileName, string contentType, int userId) - { - try - { - _logger.LogInformation("Processing document: {FileName}", fileName); - - // Extract file extension - var fileExtension = System.IO.Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty; - if (string.IsNullOrEmpty(fileExtension)) - { - throw new ArgumentException("File extension is required."); - } - - // Check if file type is supported - if (!SupportsFileType(fileExtension)) - { - throw new NotSupportedException($"File type {fileExtension} is not supported."); - } - - // Extract text from document - var textChunks = await ExtractTextFromDocumentAsync(fileStream, fileExtension); - - // Create document entity - var document = new Document - { - Title = System.IO.Path.GetFileNameWithoutExtension(fileName), - FilePath = fileName, - FileType = fileExtension, - UserId = userId, - Content = string.Join("\n", textChunks) - }; - - // Process each chunk for vector storage - for (int i = 0; i < textChunks.Length; i++) - { - var chunk = textChunks[i]; - document.Chunks.Add(new DocumentChunk - { - Content = chunk, - Position = i - }); - - // 生成向量 - var embedding = await GenerateEmbeddingAsync(chunk); - // 存储向量 - await _vectorStore.StoreVectorAsync(chunk, embedding, new { documentId = document.Id, chunkId = i }); - } - - _logger.LogInformation("Document processed successfully: {FileName}", fileName); - return document; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing document: {FileName}", fileName); - throw; - } - } - - public bool SupportsFileType(string fileExtension) - { - var supportedTypes = new List { ".pdf", ".txt", ".docx", ".doc", ".xlsx", ".xls" }; - return supportedTypes.Contains(fileExtension); - } - - public async Task ExtractTextFromDocumentAsync(Stream fileStream, string fileExtension) - { - switch (fileExtension) - { - case ".pdf": - return await ExtractTextFromPdfAsync(fileStream); - case ".txt": - return await ExtractTextFromTxtAsync(fileStream); - case ".docx": - return await ExtractTextFromDocxAsync(fileStream); - case ".doc": - throw new NotSupportedException("Legacy .doc files are not supported. Please convert to .docx."); - case ".xlsx": - case ".xls": - return await ExtractTextFromExcelAsync(fileStream, fileExtension); - default: - throw new NotSupportedException($"File type {fileExtension} is not supported for text extraction."); - } - } - - private async Task ExtractTextFromPdfAsync(Stream fileStream) - { - var textBuilder = new StringBuilder(); - using (var reader = new PdfReader(fileStream)) - { - for (int i = 1; i <= reader.NumberOfPages; i++) - { - textBuilder.AppendLine(PdfTextExtractor.GetTextFromPage(reader, i)); - } - } - return SplitTextIntoChunks(textBuilder.ToString()); - } - - private async Task ExtractTextFromTxtAsync(Stream fileStream) - { - using (var reader = new StreamReader(fileStream, Encoding.UTF8)) - { - var text = await reader.ReadToEndAsync(); - return SplitTextIntoChunks(text); - } - } - - private async Task ExtractTextFromDocxAsync(Stream fileStream) - { - var textBuilder = new StringBuilder(); - using (var document = WordprocessingDocument.Open(fileStream, false)) - { - var mainPart = document.MainDocumentPart; - if (mainPart != null) - { - var body = mainPart.Document.Body; - if (body != null) - { - foreach (var paragraph in body.Descendants()) - { - textBuilder.AppendLine(paragraph.InnerText); - } - } - } - } - return SplitTextIntoChunks(textBuilder.ToString()); - } - - private async Task ExtractTextFromExcelAsync(Stream fileStream, string fileExtension) - { - var textBuilder = new StringBuilder(); - IWorkbook workbook = null; - - if (fileExtension == ".xlsx") - { - workbook = new XSSFWorkbook(fileStream); - } - else if (fileExtension == ".xls") - { - workbook = new HSSFWorkbook(fileStream); - } - - if (workbook != null) - { - for (int sheetIndex = 0; sheetIndex < workbook.NumberOfSheets; sheetIndex++) - { - var sheet = workbook.GetSheetAt(sheetIndex); - textBuilder.AppendLine($"Sheet: {sheet.SheetName}"); - - for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++) - { - var row = sheet.GetRow(rowIndex); - if (row != null) - { - for (int cellIndex = 0; cellIndex < row.LastCellNum; cellIndex++) - { - var cell = row.GetCell(cellIndex); - if (cell != null) - { - textBuilder.Append(cell.ToString() + "\t"); - } - } - textBuilder.AppendLine(); - } - } - } - } - return SplitTextIntoChunks(textBuilder.ToString()); - } - - private string[] SplitTextIntoChunks(string text, int chunkSize = 1000) - { - var chunks = new List(); - int start = 0; - - while (start < text.Length) - { - int end = Math.Min(start + chunkSize, text.Length); - // Try to split at sentence boundary if possible - if (end < text.Length) - { - int lastPeriod = text.LastIndexOf('.', end, end - start); - if (lastPeriod > start) - { - end = lastPeriod + 1; - } - } - chunks.Add(text.Substring(start, end - start).Trim()); - start = end; - } - return chunks.ToArray(); - } - - // 添加向量生成功能(模拟实现,实际项目中应使用真实的嵌入服务) - private async Task GenerateEmbeddingAsync(string text) - { - // 这里是模拟实现,实际项目中应调用OpenAI或其他嵌入服务 - // 示例使用随机向量作为占位符 - var random = new Random(); - var embedding = new float[1536]; // 假设使用1536维度的向量 - for (int i = 0; i < embedding.Length; i++) - { - embedding[i] = (float)(random.NextDouble() * 2 - 1); // 范围 [-1, 1) - } - return embedding; - } - } -} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs index 6229963..e69de29 100644 --- a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs +++ b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs @@ -1,111 +0,0 @@ -using System.Data; -using Microsoft.Extensions.Configuration; -using Npgsql; -using RAG.Domain.Repositories; -using RAG.Domain.Entities; - -namespace RAG.Infrastructure.VectorStore -{ - public class PgVectorStore : IVectorStore - - - { - private readonly string _connectionString; - private readonly string _tableName; - - public PgVectorStore(IConfiguration configuration) - { - _connectionString = configuration.GetSection("VectorStore:ConnectionString").Value; - _tableName = configuration.GetSection("VectorStore:TableName").Value ?? "vectors"; - } - - public async Task InitializeAsync() - { - // Create extension if not exists - await ExecuteNonQueryAsync("CREATE EXTENSION IF NOT EXISTS vector"); - - // Create table if not exists - await ExecuteNonQueryAsync($@"CREATE TABLE IF NOT EXISTS {_tableName} ( - id SERIAL PRIMARY KEY, - content TEXT NOT NULL, - metadata JSONB, - embedding VECTOR(1536) NOT NULL - )"); - - // Create index if not exists - await ExecuteNonQueryAsync($@"CREATE INDEX IF NOT EXISTS {_tableName}_embedding_idx ON {_tableName} USING ivfflat (embedding vector_cosine_ops)"); - } - - public async Task StoreVectorAsync(string content, float[] embedding, object metadata = null) - { - var metadataJson = metadata != null ? System.Text.Json.JsonSerializer.Serialize(metadata) : null; - - await ExecuteNonQueryAsync($@"INSERT INTO {_tableName} (content, metadata, embedding) - VALUES (@content, @metadata, @embedding)", - new NpgsqlParameter("@content", content), - new NpgsqlParameter("@metadata", metadataJson ?? (object)DBNull.Value), - new NpgsqlParameter("@embedding", NpgsqlTypes.NpgsqlDbType.Vector) { Value = embedding }); - } - - public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5) - { - var results = new List(); - - using (var connection = new NpgsqlConnection(_connectionString)) - { - await connection.OpenAsync(); - - using (var command = new NpgsqlCommand($@"SELECT id, content, metadata, embedding <=> @queryVector AS similarity - FROM {_tableName} - ORDER BY similarity - LIMIT @topK", connection)) - { - command.Parameters.AddWithValue("@queryVector", NpgsqlTypes.NpgsqlDbType.Vector, queryVector); - command.Parameters.AddWithValue("@topK", topK); - - using (var reader = await command.ExecuteReaderAsync()) - { - while (await reader.ReadAsync()) - { - results.Add(new VectorResult - { - Id = reader.GetInt32(0), - Content = reader.GetString(1), - Metadata = reader.IsDBNull(2) ? null : System.Text.Json.JsonSerializer.Deserialize>(reader.GetString(2)), - Similarity = reader.GetDouble(3) - }); - } - } - } - } - - return results; - } - - private async Task ExecuteNonQueryAsync(string sql, params NpgsqlParameter[] parameters) - { - using (var connection = new NpgsqlConnection(_connectionString)) - { - await connection.OpenAsync(); - - using (var command = new NpgsqlCommand(sql, connection)) - { - if (parameters != null) - { - command.Parameters.AddRange(parameters); - } - - await command.ExecuteNonQueryAsync(); - } - } - } - } - - public class VectorResult - { - public int Id { get; set; } - public string Content { get; set; } - public Dictionary Metadata { get; set; } - public double Similarity { get; set; } - } -} \ No newline at end of file diff --git a/backend/RAG.Tests/DomainTests/DocumentTests.cs b/backend/RAG.Tests/DomainTests/DocumentTests.cs new file mode 100644 index 0000000..e69de29 diff --git a/backend/RAG.Tests/RAG.Tests.csproj b/backend/RAG.Tests/RAG.Tests.csproj index 66603ab..1d8592f 100644 --- a/backend/RAG.Tests/RAG.Tests.csproj +++ b/backend/RAG.Tests/RAG.Tests.csproj @@ -1,7 +1,7 @@ - + - net9.0 + net8.0 enable enable false @@ -12,6 +12,7 @@ + -- Gitee From db3d10cbec07368d267b4ec946d01fec846122f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Tue, 5 Aug 2025 16:38:45 +0800 Subject: [PATCH 05/31] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/appsettings.json | 4 ++-- query | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 query diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json index f7852dc..57a1dc2 100644 --- a/backend/RAG.Api/appsettings.json +++ b/backend/RAG.Api/appsettings.json @@ -7,10 +7,10 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Host=localhost;port=5432;Database=rag_db;Username=postgres;Password=xyq0217" + "DefaultConnection": "Host=www.mingyuy.cn;Port=5432;Database=postgres;Username=postgres;Password=mingyuy." }, "VectorStore": { "Type": "Postgres", - "ConnectionString": "Host=localhost;Port=5432;Database=rag_db;Username=postgres;Password=your_password_here" + "ConnectionString": "Host=www.mingyuy.cn;Port=5432;Database=postgres;Username=postgres;Password=mingyuy." } } diff --git a/query b/query new file mode 100644 index 0000000..a02a1cc --- /dev/null +++ b/query @@ -0,0 +1 @@ +postgresql -- Gitee From 61cbd382b769ca49a093c317eb75806a88c11a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 09:46:15 +0800 Subject: [PATCH 06/31] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=8D=95=E4=B8=AA=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/DocumentController.cs | 192 ++++++++++++ ...bec1bf_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...55f33b_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...fa8aca_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...cf2560_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...2fbe7-14c5-49c4-8904-9bc293a594a0_test.txt | 1 + ...a1a26-588a-4ddc-9613-cf97d5f447c3_test.txt | 1 + ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes ...ef7d08_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes ...5712dc_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...33fd45_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...2a7ebf_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...591c52_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...c592c-97b1-4efa-8284-5c760bbc32db_test.txt | 1 + ...b615ea_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes backend/RAG.Api/Program.cs | 4 + backend/RAG.Api/RAG.Api.http | 111 ++++++- backend/RAG.Application/DTOs/DocumentDto.cs | 18 ++ .../DTOs/DocumentPreviewDto.cs | 12 + .../RAG.Application/DTOs/DocumentUpdateDto.cs | 10 + .../RAG.Application/DTOs/DocumentUploadDto.cs | 13 + .../Interfaces/IDocumentService.cs | 48 +++ .../RAG.Application/RAG.Application.csproj | 2 + .../Services/DocumentService.cs | 285 ++++++++++++++++++ backend/RAG.Domain/Entities/Document.cs | 33 +- .../FileStorage/IFileStorageService.cs | 20 ++ .../FileStorage/LocalFileStorageService.cs | 64 ++++ .../RAG.Infrastructure.csproj | 1 - test-api.html | 103 +++++++ test-apifox-request.ps1 | 71 +++++ test-upload-api.ps1 | 47 +++ 32 files changed, 1025 insertions(+), 12 deletions(-) create mode 100644 backend/RAG.Api/Files/012bdc47-067f-42c9-9aa0-935006bec1bf_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/0abd8ca9-d0f3-4d41-9ae4-2bfb9555f33b_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/162dfd2a-80e6-4ae2-810e-1cb5b3fa8aca_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/229e8727-50b1-4cbe-9e0d-270cb1cf2560_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/3262fbe7-14c5-49c4-8904-9bc293a594a0_test.txt create mode 100644 backend/RAG.Api/Files/431a1a26-588a-4ddc-9613-cf97d5f447c3_test.txt create mode 100644 "backend/RAG.Api/Files/505c2bb3-0b8b-488c-ab2a-d4d72d1089be_\346\234\252\345\221\275\345\220\2151.pdf" create mode 100644 backend/RAG.Api/Files/5746168e-1832-4556-8385-d8dd21ef7d08_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 "backend/RAG.Api/Files/588d5e1e-ec83-4d01-9dcf-e75acdd7aecf_\346\234\252\345\221\275\345\220\2151.pdf" create mode 100644 backend/RAG.Api/Files/60d65403-5750-457f-b947-7c71b45712dc_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/73c79518-a1bb-4e3c-a12a-3578ae33fd45_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/82b608fc-3ccc-4ad0-962c-c9f4fd2a7ebf_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/9699d9e7-c206-43b9-a525-ff1aea591c52_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/bbcc592c-97b1-4efa-8284-5c760bbc32db_test.txt create mode 100644 backend/RAG.Api/Files/c7be2844-4774-4945-9245-c614a5b615ea_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Application/DTOs/DocumentDto.cs create mode 100644 backend/RAG.Application/DTOs/DocumentPreviewDto.cs create mode 100644 backend/RAG.Application/DTOs/DocumentUpdateDto.cs create mode 100644 backend/RAG.Application/DTOs/DocumentUploadDto.cs create mode 100644 backend/RAG.Application/Interfaces/IDocumentService.cs create mode 100644 backend/RAG.Application/Services/DocumentService.cs create mode 100644 backend/RAG.Infrastructure/FileStorage/IFileStorageService.cs create mode 100644 backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs create mode 100644 test-api.html create mode 100644 test-apifox-request.ps1 create mode 100644 test-upload-api.ps1 diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index e69de29..d69f016 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -0,0 +1,192 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services; + +namespace RAG.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class DocumentController : ControllerBase + { + private readonly IDocumentService _documentService; + + public DocumentController(IDocumentService documentService) + { + _documentService = documentService; + } + + /// + /// 上传单个文档 + /// + [HttpPost("upload")] + public async Task UploadDocument([FromForm] DocumentUploadDto uploadDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + var result = await _documentService.UploadDocumentAsync(uploadDto); + return Ok(result); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// 上传多个文档 + /// + [HttpPost("upload-multiple")] + public async Task UploadDocuments([FromForm] List files, [FromForm] string titlePrefix, [FromForm] string accessLevel = "internal") + { + if (files == null || files.Count == 0) + { + return BadRequest("No files uploaded"); + } + + var uploadDtos = new List(); + for (int i = 0; i < files.Count; i++) + { + uploadDtos.Add(new DocumentUploadDto + { + File = files[i], + Title = $"{titlePrefix}_{i + 1}", + AccessLevel = accessLevel + }); + } + + try + { + var results = await _documentService.UploadDocumentsAsync(uploadDtos); + return Ok(results); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// 获取文档列表 + /// + [HttpGet] + public async Task GetDocuments( + [FromQuery] string? searchTerm, + [FromQuery] string? fileType, + [FromQuery] System.DateTime? startDate, + [FromQuery] System.DateTime? endDate, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10) + { + var query = new DocumentQuery + { + SearchTerm = searchTerm, + FileType = fileType, + StartDate = startDate, + EndDate = endDate, + Page = page, + PageSize = pageSize + }; + + var results = await _documentService.GetDocumentsAsync(query); + return Ok(results); + } + + /// + /// 获取单个文档 + /// + [HttpGet("{id}")] + public async Task GetDocument(long id) + { + try + { + var result = await _documentService.GetDocumentByIdAsync(id); + return Ok(result); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + + /// + /// 预览文档 + /// + [HttpGet("{id}/preview")] + public async Task PreviewDocument(long id) + { + try + { + var result = await _documentService.PreviewDocumentAsync(id); + return Ok(result); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + + /// + /// 更新文档 + /// + [HttpPut("{id}")] + public async Task UpdateDocument(long id, [FromBody] DocumentUpdateDto updateDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + var result = await _documentService.UpdateDocumentAsync(id, updateDto); + return Ok(result); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + + /// + /// 删除单个文档 + /// + [HttpDelete("{id}")] + public async Task DeleteDocument(long id) + { + var result = await _documentService.DeleteDocumentAsync(id); + if (!result) + { + return NotFound($"Document with id {id} not found"); + } + return Ok(); + } + + /// + /// 批量删除文档 + /// + [HttpDelete("batch")] + public async Task DeleteDocuments([FromBody] List ids) + { + if (ids == null || ids.Count == 0) + { + return BadRequest("No document ids provided"); + } + + var result = await _documentService.DeleteDocumentsAsync(ids); + if (!result) + { + return NotFound("No documents found for the provided ids"); + } + return Ok(); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Files/012bdc47-067f-42c9-9aa0-935006bec1bf_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/012bdc47-067f-42c9-9aa0-935006bec1bf_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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 literal 0 HcmV?d00001 diff --git a/backend/RAG.Api/Files/5746168e-1832-4556-8385-d8dd21ef7d08_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/5746168e-1832-4556-8385-d8dd21ef7d08_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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 literal 0 HcmV?d00001 diff --git a/backend/RAG.Api/Files/60d65403-5750-457f-b947-7c71b45712dc_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/60d65403-5750-457f-b947-7c71b45712dc_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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 cfg.RegisterServicesFromAssembly(typeof(RAG.A builder.Services.AddAutoMapper(cfg => cfg.AddMaps(Assembly.GetExecutingAssembly())); builder.Services.AddAutoMapper(cfg => cfg.AddMaps(typeof(RAG.Application.Class1).Assembly)); +// 注册文件存储服务 +builder.Services.AddScoped(); +// 注册文档服务 +builder.Services.AddScoped(); builder.Services.AddCors(options => { diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index df87c00..b73af8c 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -1,6 +1,113 @@ -@RAG.Api_HostAddress = http://localhost:5097 +@url = http://localhost:5097 -GET {{RAG.Api_HostAddress}}/weatherforecast/ +### 文档管理 API 测试 + +#### 1. 上传单个文档 +POST {{url}}/api/document/upload +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="File"; filename="test.txt" +Content-Type: text/plain + +这是测试文档内容 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="Title" + +测试文档 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="AccessLevel" + +internal +----WebKitFormBoundary7MA4YWxkTrZu0gW + +### + +#### 2. 上传多个文档 +POST {{url}}/api/document/upload-multiple +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="files"; filename="file1.txt" +Content-Type: text/plain + +文档1内容 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="files"; filename="file2.txt" +Content-Type: text/plain + +文档2内容 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="titlePrefix" + +批量上传测试 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="accessLevel" + +internal +----WebKitFormBoundary7MA4YWxkTrZu0gW + +### + +#### 3. 获取文档列表 +GET {{url}}/api/document?page=1&pageSize=10 +Accept: application/json + +### + +#### 4. 搜索文档 +GET {{url}}/api/document?searchTerm=测试&page=1&pageSize=10 +Accept: application/json + +### + +#### 5. 按文件类型筛选 +GET {{url}}/api/document?fileType=txt&page=1&pageSize=10 +Accept: application/json + +### + +#### 6. 获取单个文档 +GET {{url}}/api/document/1 +Accept: application/json + +### + +#### 7. 预览文档 +GET {{url}}/api/document/1/preview +Accept: application/json + +### + +#### 8. 更新文档 +PUT {{url}}/api/document/1 +Content-Type: application/json + +{ + "Title": "更新后的文档标题", + "Description": "这是更新后的文档描述", + "AccessLevel": "public" +} + +### + +#### 9. 删除单个文档 +DELETE {{url}}/api/document/1 + +### + +#### 10. 批量删除文档 +DELETE {{url}}/api/document/batch +Content-Type: application/json + +[ + 1, 2, 3 +] + +### + +#### 原始天气 API 测试 (保留) +GET {{url}}/weatherforecast/ Accept: application/json ### diff --git a/backend/RAG.Application/DTOs/DocumentDto.cs b/backend/RAG.Application/DTOs/DocumentDto.cs new file mode 100644 index 0000000..03d2d97 --- /dev/null +++ b/backend/RAG.Application/DTOs/DocumentDto.cs @@ -0,0 +1,18 @@ +using System; +using RAG.Domain.Entities; + +namespace RAG.Application.DTOs +{ + public class DocumentDto + { + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string FileType { get; set; } = string.Empty; + public long FileSize { get; set; } + public string AccessLevel { get; set; } = string.Empty; + public DocumentStatus Status { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/DocumentPreviewDto.cs b/backend/RAG.Application/DTOs/DocumentPreviewDto.cs new file mode 100644 index 0000000..225f3e3 --- /dev/null +++ b/backend/RAG.Application/DTOs/DocumentPreviewDto.cs @@ -0,0 +1,12 @@ +namespace RAG.Application.DTOs +{ + public class DocumentPreviewDto + { + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string FileType { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public byte[]? FileData { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/DocumentUpdateDto.cs b/backend/RAG.Application/DTOs/DocumentUpdateDto.cs new file mode 100644 index 0000000..6f9a256 --- /dev/null +++ b/backend/RAG.Application/DTOs/DocumentUpdateDto.cs @@ -0,0 +1,10 @@ +namespace RAG.Application.DTOs +{ + public class DocumentUpdateDto + { + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public List? Tags { get; set; } + public string? AccessLevel { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/DocumentUploadDto.cs b/backend/RAG.Application/DTOs/DocumentUploadDto.cs new file mode 100644 index 0000000..09daf79 --- /dev/null +++ b/backend/RAG.Application/DTOs/DocumentUploadDto.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Http; + +namespace RAG.Application.DTOs +{ + public class DocumentUploadDto + { + public IFormFile File { get; set; } = null!; + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public string AccessLevel { get; set; } = "internal"; + public List? Tags { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Interfaces/IDocumentService.cs b/backend/RAG.Application/Interfaces/IDocumentService.cs new file mode 100644 index 0000000..3c9a677 --- /dev/null +++ b/backend/RAG.Application/Interfaces/IDocumentService.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using RAG.Application.DTOs; + +namespace RAG.Application.Interfaces +{ + public interface IDocumentService + { + // 上传文档 + Task UploadDocumentAsync(DocumentUploadDto uploadDto); + Task> UploadDocumentsAsync(IEnumerable uploadDtos); + + // 获取文档列表 + Task> GetDocumentsAsync(DocumentQuery query); + + // 获取单个文档 + Task GetDocumentByIdAsync(long id); + + // 预览文档 + Task PreviewDocumentAsync(long id); + + // 编辑文档 + Task UpdateDocumentAsync(long id, DocumentUpdateDto updateDto); + + // 删除文档 + Task DeleteDocumentAsync(long id); + Task DeleteDocumentsAsync(IEnumerable ids); + } + + public class DocumentQuery + { + public string? SearchTerm { get; set; } + public string? FileType { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } + + public class PagedResult + { + public IEnumerable Items { get; set; } = Enumerable.Empty(); + public int TotalCount { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); + } +} \ No newline at end of file diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj index c826abf..7baf0da 100644 --- a/backend/RAG.Application/RAG.Application.csproj +++ b/backend/RAG.Application/RAG.Application.csproj @@ -2,6 +2,7 @@ + @@ -9,6 +10,7 @@ + diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs new file mode 100644 index 0000000..2f64ae0 --- /dev/null +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Domain.Entities; +using RAG.Infrastructure.FileStorage; +using RAG.Infrastructure.Data; + +namespace RAG.Application.Services +{ + public class DocumentService : IDocumentService + { + private readonly ApplicationDbContext _context; + private readonly IFileStorageService _fileStorageService; + + public DocumentService(ApplicationDbContext context, IFileStorageService fileStorageService) + { + _context = context; + _fileStorageService = fileStorageService; + } + + public async Task UploadDocumentAsync(DocumentUploadDto uploadDto) + { + // 验证文件格式 + var allowedExtensions = new[] { ".txt", ".md", ".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".png", ".jpg", ".jpeg" }; + var fileExtension = Path.GetExtension(uploadDto.File.FileName).ToLower(); + + if (!allowedExtensions.Contains(fileExtension)) + { + throw new ArgumentException($"Unsupported file format: {fileExtension}"); + } + + // 验证文件大小 (50MB) + const long maxFileSize = 50 * 1024 * 1024; + if (uploadDto.File.Length > maxFileSize) + { + throw new ArgumentException("File size exceeds the maximum limit of 50MB"); + } + + // 上传文件到存储服务 + using (var stream = uploadDto.File.OpenReadStream()) + { + var filePath = await _fileStorageService.UploadFileAsync(stream, uploadDto.File.FileName, uploadDto.File.ContentType); + + // 创建文档实体 + var document = Document.Create( + uploadDto.Title, + uploadDto.File.FileName, + filePath, + fileExtension.TrimStart('.'), + uploadDto.File.Length, + uploadDto.AccessLevel); + + // 保存到数据库 + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + // 返回DTO + return new DocumentDto + { + Id = document.Id, + Title = document.Title, + FileName = document.FileName, + FileType = document.FileType, + FileSize = document.FileSize, + AccessLevel = document.AccessLevel, + Status = document.Status, + CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt + }; + } + } + + public async Task> UploadDocumentsAsync(IEnumerable uploadDtos) + { + var results = new List(); + + foreach (var uploadDto in uploadDtos) + { + var result = await UploadDocumentAsync(uploadDto); + results.Add(result); + } + + return results; + } + + public async Task> GetDocumentsAsync(DocumentQuery query) + { + var queryable = _context.Documents.AsQueryable(); + + // 应用筛选条件 + if (!string.IsNullOrWhiteSpace(query.SearchTerm)) + { + queryable = queryable.Where(d => d.Title.Contains(query.SearchTerm) || d.FileName.Contains(query.SearchTerm)); + } + + if (!string.IsNullOrWhiteSpace(query.FileType)) + { + queryable = queryable.Where(d => d.FileType == query.FileType); + } + + if (query.StartDate.HasValue) + { + queryable = queryable.Where(d => d.CreatedAt >= query.StartDate.Value); + } + + if (query.EndDate.HasValue) + { + queryable = queryable.Where(d => d.CreatedAt <= query.EndDate.Value); + } + + // 分页 + var totalCount = await queryable.CountAsync(); + var items = await queryable + .OrderByDescending(d => d.CreatedAt) + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(d => new DocumentDto + { + Id = d.Id, + Title = d.Title, + FileName = d.FileName, + FileType = d.FileType, + FileSize = d.FileSize, + AccessLevel = d.AccessLevel, + Status = d.Status, + CreatedAt = d.CreatedAt, + UpdatedAt = d.UpdatedAt + }) + .ToListAsync(); + + return new PagedResult + { + Items = items, + TotalCount = totalCount, + Page = query.Page, + PageSize = query.PageSize + }; + } + + public async Task GetDocumentByIdAsync(long id) + { + var document = await _context.Documents.FindAsync(id); + + if (document == null) + { + throw new KeyNotFoundException($"Document with id {id} not found"); + } + + return new DocumentDto + { + Id = document.Id, + Title = document.Title, + FileName = document.FileName, + FileType = document.FileType, + FileSize = document.FileSize, + AccessLevel = document.AccessLevel, + Status = document.Status, + CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt + }; + } + + public async Task PreviewDocumentAsync(long id) + { + var document = await _context.Documents.FindAsync(id); + + if (document == null) + { + throw new KeyNotFoundException($"Document with id {id} not found"); + } + + // 对于文本文档和PDF,我们可以尝试读取内容 + string content = string.Empty; + byte[] fileData = null; + + if (document.FileType.ToLower() == "txt" || document.FileType.ToLower() == "md") + { + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + } + else + { + // 对于其他类型的文件,我们返回文件数据 + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var memoryStream = new MemoryStream()) + { + await stream.CopyToAsync(memoryStream); + fileData = memoryStream.ToArray(); + } + } + + return new DocumentPreviewDto + { + Id = document.Id, + Title = document.Title, + FileName = document.FileName, + FileType = document.FileType, + Content = content, + FileData = fileData + }; + } + + public async Task UpdateDocumentAsync(long id, DocumentUpdateDto updateDto) + { + var document = await _context.Documents.FindAsync(id); + + if (document == null) + { + throw new KeyNotFoundException($"Document with id {id} not found"); + } + + // 更新文档属性 +// 由于 Document.Title 的 set 访问器不可访问,推测应该调用文档实体的更新方法 +// 假设 Document 类有 UpdateTitle 方法用于更新标题 +// 使用实体的业务方法更新标题 +document.UpdateTitle(updateDto.Title); +// 更新文档修改时间 +document.UpdateUpdatedAt(DateTime.UtcNow); + + await _context.SaveChangesAsync(); + + return new DocumentDto + { + Id = document.Id, + Title = document.Title, + FileName = document.FileName, + FileType = document.FileType, + FileSize = document.FileSize, + AccessLevel = document.AccessLevel, + Status = document.Status, + CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt + }; + } + + public async Task DeleteDocumentAsync(long id) + { + var document = await _context.Documents.FindAsync(id); + + if (document == null) + { + return false; + } + + // 删除文件 + await _fileStorageService.DeleteFileAsync(document.FilePath); + + // 删除数据库记录 + _context.Documents.Remove(document); + await _context.SaveChangesAsync(); + + return true; + } + + public async Task DeleteDocumentsAsync(IEnumerable ids) + { + var documents = await _context.Documents.Where(d => ids.Contains(d.Id)).ToListAsync(); + + if (documents.Count == 0) + { + return false; + } + + // 删除文件 + foreach (var document in documents) + { + await _fileStorageService.DeleteFileAsync(document.FilePath); + } + + // 删除数据库记录 + _context.Documents.RemoveRange(documents); + await _context.SaveChangesAsync(); + + return true; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs index 52d5864..3aeaec1 100644 --- a/backend/RAG.Domain/Entities/Document.cs +++ b/backend/RAG.Domain/Entities/Document.cs @@ -63,16 +63,31 @@ namespace RAG.Domain.Entities public IReadOnlyList Chunks => _chunks.AsReadOnly(); // 业务方法 - public void UpdateProcessingStatus() - { - if (Status != DocumentStatus.Pending) - throw new InvalidOperationException("Document must be in pending status"); - - Status = DocumentStatus.Processing; - UpdatedAt = DateTime.UtcNow; + public void UpdateTitle(string newTitle) + { + if (string.IsNullOrWhiteSpace(newTitle)) + throw new ArgumentException("Title cannot be empty"); - AddDomainEvent(new DocumentProcessingStartedEvent(Id)); - } + Title = newTitle; + UpdatedAt = DateTime.UtcNow; + } + + // 更新修改时间 + public void UpdateUpdatedAt(DateTime updatedTime) + { + UpdatedAt = updatedTime; + } + + public void UpdateProcessingStatus() + { + if (Status != DocumentStatus.Pending) + throw new InvalidOperationException("Document must be in pending status"); + + Status = DocumentStatus.Processing; + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new DocumentProcessingStartedEvent(Id)); + } public void CompleteProcessing() { diff --git a/backend/RAG.Infrastructure/FileStorage/IFileStorageService.cs b/backend/RAG.Infrastructure/FileStorage/IFileStorageService.cs new file mode 100644 index 0000000..0f81ed4 --- /dev/null +++ b/backend/RAG.Infrastructure/FileStorage/IFileStorageService.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Threading.Tasks; + +namespace RAG.Infrastructure.FileStorage +{ + public interface IFileStorageService + { + // 上传文件 + Task UploadFileAsync(Stream fileStream, string fileName, string contentType); + + // 获取文件流 + Task GetFileStreamAsync(string filePath); + + // 删除文件 + Task DeleteFileAsync(string filePath); + + // 检查文件是否存在 + Task FileExistsAsync(string filePath); + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs b/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs new file mode 100644 index 0000000..8eb15dc --- /dev/null +++ b/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; + +namespace RAG.Infrastructure.FileStorage +{ + public class LocalFileStorageService : IFileStorageService + { + private readonly string _storagePath; + + public LocalFileStorageService(IConfiguration configuration) + { + // 从配置中获取存储路径,如果没有配置则使用默认路径 + _storagePath = configuration.GetValue("FileStorage:LocalPath") ?? Path.Combine(Directory.GetCurrentDirectory(), "Files"); + + // 确保存储目录存在 + if (!Directory.Exists(_storagePath)) + { + Directory.CreateDirectory(_storagePath); + } + } + + public async Task UploadFileAsync(Stream fileStream, string fileName, string contentType) + { + // 生成唯一文件名,避免冲突 + var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; + var filePath = Path.Combine(_storagePath, uniqueFileName); + + using (var outputStream = new FileStream(filePath, FileMode.Create)) + { + await fileStream.CopyToAsync(outputStream); + } + + // 返回文件路径 + return filePath; + } + + public async Task GetFileStreamAsync(string filePath) + { + if (!await FileExistsAsync(filePath)) + { + throw new FileNotFoundException("File not found", filePath); + } + + return new FileStream(filePath, FileMode.Open, FileAccess.Read); + } + + public async Task DeleteFileAsync(string filePath) + { + if (!await FileExistsAsync(filePath)) + { + return false; + } + + File.Delete(filePath); + return true; + } + + public async Task FileExistsAsync(string filePath) + { + return File.Exists(filePath); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index 74d9204..b489d8b 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -2,7 +2,6 @@ - diff --git a/test-api.html b/test-api.html new file mode 100644 index 0000000..73a36cd --- /dev/null +++ b/test-api.html @@ -0,0 +1,103 @@ + + + + + + API测试页面 + + + +
+

文档上传API测试

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/test-apifox-request.ps1 b/test-apifox-request.ps1 new file mode 100644 index 0000000..9ef0e2b --- /dev/null +++ b/test-apifox-request.ps1 @@ -0,0 +1,71 @@ +#!/usr/bin/env pwsh +# 模拟APIFox请求的测试脚本 + +# 配置参数 +$apiUrl = "http://localhost:5097/api/document/upload" +$filePath = "C:\Users\Asus\Desktop\963c1d70789c87eefcdce5cb8d873b1.png" # 替换为实际文件路径 +$title = "测试文档" +$accessLevel = "internal" + +# 检查文件是否存在 +if (-not (Test-Path -Path $filePath)) { + Write-Host "错误: 找不到文件 '$filePath'" + exit 1 +} + +# 创建临时边界字符串 +$boundary = "--------------------------833393852902126438284443" + +# 创建HTTP客户端 +$client = New-Object System.Net.Http.HttpClient + +# 设置请求头 +$client.DefaultRequestHeaders.UserAgent.ParseAdd("Apifox/1.0.0 (https://apifox.com)") +$client.DefaultRequestHeaders.Accept.Add((New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("*/*"))) + +# 创建multipart/form-data内容 +$content = New-Object System.Net.Http.MultipartFormDataContent($boundary) + +# 添加文件内容 +$fileStream = [System.IO.File]::OpenRead($filePath) +$fileContent = New-Object System.Net.Http.StreamContent($fileStream) +$fileContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue("multipart/form-data") +$content.Add($fileContent, "File", [System.IO.Path]::GetFileName($filePath)) + +# 添加其他表单字段 +$titleContent = New-Object System.Net.Http.StringContent($title) +$content.Add($titleContent, "Title") + +$accessLevelContent = New-Object System.Net.Http.StringContent($accessLevel) +$content.Add($accessLevelContent, "AccessLevel") + +try { + # 发送请求 + Write-Host "正在发送请求到: $apiUrl" + $response = $client.PostAsync($apiUrl, $content).Result + + # 输出响应状态码 + Write-Host "\n响应状态码: $($response.StatusCode)" + + # 输出响应头 + Write-Host "\n响应头:" + $response.Headers | Format-List | Out-Host + + # 输出响应体 + $responseBody = $response.Content.ReadAsStringAsync().Result + Write-Host "\n响应体:" + try { + # 尝试格式化JSON + $jsonBody = ConvertFrom-Json -InputObject $responseBody + $jsonBody | ConvertTo-Json -Depth 10 | Out-Host + } catch { + # 如果不是JSON,直接输出 + Write-Host $responseBody + } +} catch { + Write-Host "\n请求失败: $_" +} finally { + # 释放资源 + $fileStream.Dispose() + $client.Dispose() +} \ No newline at end of file diff --git a/test-upload-api.ps1 b/test-upload-api.ps1 new file mode 100644 index 0000000..5c95ebe --- /dev/null +++ b/test-upload-api.ps1 @@ -0,0 +1,47 @@ +# 创建测试文件 +$testFilePath = ".\test.txt" +Set-Content -Path $testFilePath -Value "This is a test document content." + +# 定义API端点 +$apiUrl = "http://localhost:5097/api/document/upload" + +# 使用HttpClient发送multipart/form-data请求 +Add-Type -AssemblyName System.Net.Http + +$client = New-Object System.Net.Http.HttpClient +$content = New-Object System.Net.Http.MultipartFormDataContent + +# 添加文件 +$fileStream = [System.IO.File]::OpenRead($testFilePath) +$fileContent = New-Object System.Net.Http.StreamContent($fileStream) +$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/plain") +$content.Add($fileContent, "File", [System.IO.Path]::GetFileName($testFilePath)) + +# 添加其他字段 +$content.Add((New-Object System.Net.Http.StringContent("Test Document")), "Title") +$content.Add((New-Object System.Net.Http.StringContent("internal")), "AccessLevel") + +# 发送请求并获取响应 +try { + $response = $client.PostAsync($apiUrl, $content).Result + Write-Host "请求状态码: $($response.StatusCode)" + + if ($response.IsSuccessStatusCode) { + Write-Host "请求成功!" + $responseContent = $response.Content.ReadAsStringAsync().Result + Write-Host "响应内容:" + $responseContent + } else { + Write-Host "请求失败!" + $errorContent = $response.Content.ReadAsStringAsync().Result + Write-Host "错误内容: $errorContent" + } +} catch { + Write-Host "请求发生异常: $($_.Exception.Message)" +} finally { + $fileStream.Dispose() + $client.Dispose() +} + +# 清理测试文件 +Remove-Item -Path $testFilePath -Force \ No newline at end of file -- Gitee From aaeefa7021fea422a222772455384aac2d5287b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 10:00:12 +0800 Subject: [PATCH 07/31] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=A4=9A=E4=B8=AA=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=88=90=E5=8A=9F=E4=B8=94=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/DocumentController.cs | 2 +- ...c71516ee_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...1_\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes ...d_\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes ...bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...425ea096_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...a_\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes 7 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 backend/RAG.Api/Files/5c35a45b-e55e-4abe-bd9b-e5d1c71516ee_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 "backend/RAG.Api/Files/75c10c9e-4fe7-4b5d-b58a-9cd1504fcfa1_\346\234\252\345\221\275\345\220\2151.pdf" create mode 100644 "backend/RAG.Api/Files/87eece55-1821-4937-879b-be2db857a9fd_\346\234\252\345\221\275\345\220\2151.pdf" create mode 100644 backend/RAG.Api/Files/9c33fc55-92b7-4415-ac72-64c5bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/ab000b4e-4f92-4969-ac4a-9670425ea096_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 "backend/RAG.Api/Files/e30f2fdb-e3e8-45a0-9306-4b97805fa2ca_\346\234\252\345\221\275\345\220\2151.pdf" diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index d69f016..d2fec49 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -66,7 +66,7 @@ namespace RAG.Api.Controllers try { var results = await _documentService.UploadDocumentsAsync(uploadDtos); - return Ok(results); + return Ok(new { documents = results }); } catch (ArgumentException ex) { diff --git a/backend/RAG.Api/Files/5c35a45b-e55e-4abe-bd9b-e5d1c71516ee_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/5c35a45b-e55e-4abe-bd9b-e5d1c71516ee_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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
!hH6A zUD8@L_X-o`N)pk0r1dBHhG>vY$z>ZkfT>t&q=@hfbocFtaH5JJ z$V29zL2seni2UdGk)aPFDVlcOTNK$3ER`3#%Ck|MBHFs@s=6o@!){yZYX4D2M+^FO0i7|c{6CS;v=Pbzfsw`#rK60zh)2Y!Xcsz6}L4m zl(j;A7CU!#i*whd^+oy1$)#Gp?&6H9K@AnAIv;MN$oa>~T)(ZLYoz|=Iy+>fNL+0i zyUXePYz*xG9vhSW#Iqq&4gcYjVQ-D@WAj*ivJQ(=-ELkM*f2K zK(HKf>7(3j#WM6J#!#S^;a^{t^-SS?#o0bVAMGOF4d7NCzHgdZ8@x{a@h^+V;8Hom zc$=I3QC!PALS9DRg>TCkCqd*TqDo`!1Nq}qhIQ}cjJ7ZC+Fx-@N;3t-%C|LN z?P6_@n!u!^GUwD0>p$65aStNJmYeW!2H*fQ2*g`H=b6LyPpjUu{!oykG`IAYcjpc2f;w&R}Nx*N3iE&8a;tC{afmH0<{yR@gxuGiZW+eg z%TLoDXFhjAOb0>QE2~vThZX5c9PEYn+z`J%W6K*rF?p1hZmE9dG`Uf;3BdINWroVU z8qKFFk(t?w0N77&;p+!vZbiwA80ssNb|DaBX`P0n+S6jJtS-1B5_S*$PJ56`)8C|2 zi@Xk5Re{;ILI9mATHam2#qmEbO{3e~u;N3s50An5tV>$$Bw#PjxocamFT=3?Z+FU@ z2DA7!bZh;(Lh|zT75|APonochGH3(vg@)Oz0R91nWR(n*U&EG_>pm?_2~mAy4Bl1% zCz(9*#>X1YO-|m|cF{$>P`k#Ny{E8dhA;UtEi>RH_1q!8sHTUgU;~IoJ2v?>>#mP& zX>kq}!UR|7x&^mt;r7#|+IXSJP;`0jt={$Y&6Gw;n%Yc{=dpH^Y3`8(S3fwb@OWre zUhlJ=ZNvD~9gk-sRc}K&a=;w4m)xQo|E#Z&omVPwDnPOPbAhx85;&oDXPXj!;C}0I z=pvF0I6*JAlKQ!z=bNV}V_MKx`;ITK@L!(sz=`g7(U_L?zSS30D=wc3c`n^g$F*3zBs z=nY?s%EY_X{Fk@NYRqEN!MKk)(;url=fbYL8fsG@```I7w(Q#>L z`c)pO=&2>`HnQ;gxJE}wL5gQ7{`0q$s@4)F1my!l+}_f$?^z>#Ihl@T|*M~LI}l;mDDA%gRQW3pJE@N zm)*AWKEq(J^BC;B4>Bn)O_p}PSbFmO|4;mT9;w<9(F=6!TGdZqzx}chfFQrTu2_wR z{VJZxQon^PRSAgxr3-xDK3}M5n#mH-WyJ2p6TQK!P5*W-?I2e1G@sa=RE}gj8|MJQ zwc3(dqWvwCzNr)Bi%bDzpQK4S0Bo5#w3kjxZ{OdjoE=qjp|0|xR=0zEHj|kuSIc>v z-I`S)W0mHah?T5~ml$FSIb_fyn6e|t4h_7eeV-SJ@jA@%69aVx(W=q&`)qsQ1~fD?4gy`k=7p_Bh< zq`|+nHZB|MD4i-<*wIw;6>^j((ms{hL|p;w`F^Q1{+V@icl3w$;X!NoOvaa=C-Ik$ zcR!wge|pgP{p6?V={j#c6MVk%Vy+Nag>B3kUcXMDi!+TT6u>}D_6${=!fKlpDFy6+HZmW4fb{|7Xd+o0juYt(*h7aZh74FCWD literal 0 HcmV?d00001 diff --git "a/backend/RAG.Api/Files/87eece55-1821-4937-879b-be2db857a9fd_\346\234\252\345\221\275\345\220\2151.pdf" "b/backend/RAG.Api/Files/87eece55-1821-4937-879b-be2db857a9fd_\346\234\252\345\221\275\345\220\2151.pdf" new file mode 100644 index 0000000000000000000000000000000000000000..1e53172a688b1d8bc9e974c6aa799b17ad32eeea GIT binary patch literal 1243 zcmZ{kO>5gQ7{`0q$s@4)F1my!l+}_f$?^z>#Ihl@T|*M~LI}l;mDDA%gRQW3pJE@N zm)*AWKEq(J^BC;B4>Bn)O_p}PSbFmO|4;mT9;w<9(F=6!TGdZqzx}chfFQrTu2_wR z{VJZxQon^PRSAgxr3-xDK3}M5n#mH-WyJ2p6TQK!P5*W-?I2e1G@sa=RE}gj8|MJQ zwc3(dqWvwCzNr)Bi%bDzpQK4S0Bo5#w3kjxZ{OdjoE=qjp|0|xR=0zEHj|kuSIc>v z-I`S)W0mHah?T5~ml$FSIb_fyn6e|t4h_7eeV-SJ@jA@%69aVx(W=q&`)qsQ1~fD?4gy`k=7p_Bh< zq`|+nHZB|MD4i-<*wIw;6>^j((ms{hL|p;w`F^Q1{+V@icl3w$;X!NoOvaa=C-Ik$ zcR!wge|pgP{p6?V={j#c6MVk%Vy+Nag>B3kUcXMDi!+TT6u>}D_6${=!fKlpDFy6+HZmW4fb{|7Xd+o0juYt(*h7aZh74FCWD literal 0 HcmV?d00001 diff --git a/backend/RAG.Api/Files/9c33fc55-92b7-4415-ac72-64c5bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/9c33fc55-92b7-4415-ac72-64c5bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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 literal 0 HcmV?d00001 -- Gitee From a343501c453e0b9cff825ac6015d52efac01ed0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 10:02:59 +0800 Subject: [PATCH 08/31] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E8=8E=B7=E5=8F=96=E6=96=87=E6=A1=A3=E5=88=97=E8=A1=A8?= =?UTF-8?q?=EF=BC=8C=E6=90=9C=E7=B4=A2=E6=96=87=E6=A1=A3=EF=BC=8C=E6=8C=89?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=B1=BB=E5=9E=8B=E7=AD=9B=E9=80=89=E4=B8=89?= =?UTF-8?q?=E4=B8=AA=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index b73af8c..475a73f 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -65,7 +65,7 @@ Accept: application/json GET {{url}}/api/document?fileType=txt&page=1&pageSize=10 Accept: application/json -### +### #### 6. 获取单个文档 GET {{url}}/api/document/1 -- Gitee From 178ac8a2c7f8c3785bbd9f60ba473a657f2a93df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 10:05:16 +0800 Subject: [PATCH 09/31] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E8=8E=B7=E5=8F=96=E5=8D=95=E4=B8=AA=E6=96=87=E6=A1=A3?= =?UTF-8?q?=EF=BC=8C=E9=A2=84=E8=A7=88=E6=96=87=E6=A1=A3=E4=BF=A9=E4=B8=AA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=EF=BC=8C=E4=B8=94=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 475a73f..439b4f8 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -68,13 +68,13 @@ Accept: application/json ### #### 6. 获取单个文档 -GET {{url}}/api/document/1 +GET {{url}}/api/document/2826681530093839908 Accept: application/json ### #### 7. 预览文档 -GET {{url}}/api/document/1/preview +GET {{url}}/api/document/8371670605660901366/preview Accept: application/json ### -- Gitee From b297b6e372f18a24f4b7b19b1165ed3384a1585d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 10:06:59 +0800 Subject: [PATCH 10/31] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3=EF=BC=8C=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=8D=95=E4=B8=AA=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BF=A9?= =?UTF-8?q?=E4=B8=AA=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes backend/RAG.Api/RAG.Api.http | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 backend/RAG.Api/Files/9c33fc55-92b7-4415-ac72-64c5bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png diff --git a/backend/RAG.Api/Files/9c33fc55-92b7-4415-ac72-64c5bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/9c33fc55-92b7-4415-ac72-64c5bf735b6e_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 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 Date: Wed, 6 Aug 2025 10:08:31 +0800 Subject: [PATCH 11/31] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E5=A4=9A=E4=B8=AA=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=88=90=E5=8A=9F=E4=B8=94=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=97=A0=E8=AF=AF=E3=80=82=E6=89=80=E6=9C=89=E7=9A=84=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1_\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes ...425ea096_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...a_\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes backend/RAG.Api/RAG.Api.http | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 "backend/RAG.Api/Files/75c10c9e-4fe7-4b5d-b58a-9cd1504fcfa1_\346\234\252\345\221\275\345\220\2151.pdf" delete mode 100644 backend/RAG.Api/Files/ab000b4e-4f92-4969-ac4a-9670425ea096_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 "backend/RAG.Api/Files/e30f2fdb-e3e8-45a0-9306-4b97805fa2ca_\346\234\252\345\221\275\345\220\2151.pdf" diff --git "a/backend/RAG.Api/Files/75c10c9e-4fe7-4b5d-b58a-9cd1504fcfa1_\346\234\252\345\221\275\345\220\2151.pdf" "b/backend/RAG.Api/Files/75c10c9e-4fe7-4b5d-b58a-9cd1504fcfa1_\346\234\252\345\221\275\345\220\2151.pdf" deleted file mode 100644 index 1e53172a688b1d8bc9e974c6aa799b17ad32eeea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1243 zcmZ{kO>5gQ7{`0q$s@4)F1my!l+}_f$?^z>#Ihl@T|*M~LI}l;mDDA%gRQW3pJE@N zm)*AWKEq(J^BC;B4>Bn)O_p}PSbFmO|4;mT9;w<9(F=6!TGdZqzx}chfFQrTu2_wR z{VJZxQon^PRSAgxr3-xDK3}M5n#mH-WyJ2p6TQK!P5*W-?I2e1G@sa=RE}gj8|MJQ zwc3(dqWvwCzNr)Bi%bDzpQK4S0Bo5#w3kjxZ{OdjoE=qjp|0|xR=0zEHj|kuSIc>v z-I`S)W0mHah?T5~ml$FSIb_fyn6e|t4h_7eeV-SJ@jA@%69aVx(W=q&`)qsQ1~fD?4gy`k=7p_Bh< zq`|+nHZB|MD4i-<*wIw;6>^j((ms{hL|p;w`F^Q1{+V@icl3w$;X!NoOvaa=C-Ik$ zcR!wge|pgP{p6?V={j#c6MVk%Vy+Nag>B3kUcXMDi!+TT6u>}D_6${=!fKlpDFy6+HZmW4fb{|7Xd+o0juYt(*h7aZh74FCWD diff --git a/backend/RAG.Api/Files/ab000b4e-4f92-4969-ac4a-9670425ea096_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/ab000b4e-4f92-4969-ac4a-9670425ea096_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 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 diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 07b4901..f720780 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -101,7 +101,7 @@ DELETE {{url}}/api/document/batch Content-Type: application/json [ - 1, 2, 3 + 6130474657473397545, 6804474346525374423, 9125508367891060242 ] ### -- Gitee From 0986148dd6894c801d87250b087cebe5ff783fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 10:37:57 +0800 Subject: [PATCH 12/31] =?UTF-8?q?=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/DocumentController.cs | 29 ++++++++++++++ backend/RAG.Api/RAG.Api.http | 16 +++++++- .../BatchUpdateDocumentsAccessLevelDto.cs | 17 ++++++++ .../Interfaces/IDocumentService.cs | 3 ++ .../Services/DocumentService.cs | 40 +++++++++++++++++-- backend/RAG.Domain/Entities/Document.cs | 10 +++++ 6 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 backend/RAG.Application/DTOs/BatchUpdateDocumentsAccessLevelDto.cs diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index d2fec49..fca1238 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -188,5 +188,34 @@ namespace RAG.Api.Controllers } return Ok(); } + + /// + /// 批量更新文档权限 + /// + [HttpPut("update-access-level")] + public async Task UpdateDocumentsAccessLevel([FromBody] BatchUpdateDocumentsAccessLevelDto updateDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + var result = await _documentService.UpdateDocumentsAccessLevelAsync(updateDto.Ids, updateDto.AccessLevel); + + + + if (!result) + { + return NotFound("No documents found with the provided ids"); + } + return Ok(); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + } } } \ No newline at end of file diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index f720780..f159594 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -79,13 +79,14 @@ Accept: application/json ### -#### 8. 更新文档 +#### 8. 更新文档(包含权限) PUT {{url}}/api/document/2826681530093839908 Content-Type: application/json { "Title": "更新后的文档标题", "Description": "这是更新后的文档描述", + "Tags": ["tag1", "tag2"], "AccessLevel": "public" } @@ -96,7 +97,18 @@ DELETE {{url}}/api/document/642342144492866748 ### -#### 10. 批量删除文档 +#### 10. 批量更新文档权限 +PUT {{url}}/api/document/update-access-level +Content-Type: application/json + +{ + "Ids": [4125359736557238617, 2517892471848059843], + "AccessLevel": "public" +} + +### + +#### 11. 批量删除文档 DELETE {{url}}/api/document/batch Content-Type: application/json diff --git a/backend/RAG.Application/DTOs/BatchUpdateDocumentsAccessLevelDto.cs b/backend/RAG.Application/DTOs/BatchUpdateDocumentsAccessLevelDto.cs new file mode 100644 index 0000000..5e22598 --- /dev/null +++ b/backend/RAG.Application/DTOs/BatchUpdateDocumentsAccessLevelDto.cs @@ -0,0 +1,17 @@ + + +using System.ComponentModel.DataAnnotations; + +namespace RAG.Application.DTOs +{ + public class BatchUpdateDocumentsAccessLevelDto + { + [Required] + [MinLength(1)] + public List Ids { get; set; } = new List(); + + [Required] + [RegularExpression("^(internal|public)$", ErrorMessage = "Access level must be 'internal' or 'public'")] + public string AccessLevel { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Interfaces/IDocumentService.cs b/backend/RAG.Application/Interfaces/IDocumentService.cs index 3c9a677..498e2df 100644 --- a/backend/RAG.Application/Interfaces/IDocumentService.cs +++ b/backend/RAG.Application/Interfaces/IDocumentService.cs @@ -25,6 +25,9 @@ namespace RAG.Application.Interfaces // 删除文档 Task DeleteDocumentAsync(long id); Task DeleteDocumentsAsync(IEnumerable ids); + + // 批量更新文档权限 + Task UpdateDocumentsAccessLevelAsync(IEnumerable ids, string accessLevel); } public class DocumentQuery diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs index 2f64ae0..df7c452 100644 --- a/backend/RAG.Application/Services/DocumentService.cs +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -219,11 +219,19 @@ namespace RAG.Application.Services // 更新文档属性 // 由于 Document.Title 的 set 访问器不可访问,推测应该调用文档实体的更新方法 -// 假设 Document 类有 UpdateTitle 方法用于更新标题 // 使用实体的业务方法更新标题 document.UpdateTitle(updateDto.Title); -// 更新文档修改时间 -document.UpdateUpdatedAt(DateTime.UtcNow); + +// 更新访问权限(如果提供) +if (!string.IsNullOrEmpty(updateDto.AccessLevel)) +{ + document.UpdateAccessLevel(updateDto.AccessLevel); +} +else +{ + // 如果未提供权限,更新修改时间 + document.UpdateUpdatedAt(DateTime.UtcNow); +} await _context.SaveChangesAsync(); @@ -281,5 +289,31 @@ document.UpdateUpdatedAt(DateTime.UtcNow); return true; } + + public async Task UpdateDocumentsAccessLevelAsync(IEnumerable ids, string accessLevel) + { + // 验证访问级别 + if (string.IsNullOrWhiteSpace(accessLevel) || (accessLevel.ToLower() != "internal" && accessLevel.ToLower() != "public")) + { + throw new ArgumentException("Access level must be 'internal' or 'public'"); + } + + // 查询文档 + var documents = await _context.Documents.Where(d => ids.Contains(d.Id)).ToListAsync(); + + if (documents.Count == 0) + { + return false; + } + + // 更新权限 + foreach (var document in documents) + { + document.UpdateAccessLevel(accessLevel); + } + + await _context.SaveChangesAsync(); + return true; + } } } \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs index 3aeaec1..cc27f21 100644 --- a/backend/RAG.Domain/Entities/Document.cs +++ b/backend/RAG.Domain/Entities/Document.cs @@ -78,6 +78,16 @@ namespace RAG.Domain.Entities UpdatedAt = updatedTime; } + // 更新访问权限 + public void UpdateAccessLevel(string newAccessLevel) + { + if (string.IsNullOrWhiteSpace(newAccessLevel) || (newAccessLevel.ToLower() != "internal" && newAccessLevel.ToLower() != "public")) + throw new ArgumentException("Access level must be 'internal' or 'public'"); + + AccessLevel = newAccessLevel.ToLower(); + UpdatedAt = DateTime.UtcNow; + } + public void UpdateProcessingStatus() { if (Status != DocumentStatus.Pending) -- Gitee From 78a0d437d07ed34b1a29c9f195bc526c5fd10f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 17:23:45 +0800 Subject: [PATCH 13/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E8=8E=B7=E5=8F=96=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=90=91=E9=87=8F?= =?UTF-8?q?=E5=8C=96=E5=8D=95=E4=B8=AA=E6=96=87=E6=A1=A3=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/Controllers/ChatController.cs | 12 +- .../Controllers/KnowledgeBaseController.cs | 152 +++++ .../Controllers/VectorizationController.cs | 134 +++++ ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes ...56a487_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...e282d2_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...6969b7_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 0 -> 1243 bytes ...4a5da7_963c1d70789c87eefcdce5cb8d873b1.png | Bin 0 -> 11973 bytes backend/RAG.Api/Program.cs | 24 + backend/RAG.Api/RAG.Api.http | 67 ++- backend/RAG.Api/appsettings.json | 4 +- .../DTOs/BatchVectorizationRequestDto.cs | 15 + .../DTOs/DocumentVectorizationStatusDto.cs | 16 + .../DTOs/KnowledgeBaseCreateDto.cs | 15 + .../RAG.Application/DTOs/KnowledgeBaseDto.cs | 17 + .../DTOs/KnowledgeBaseQuery.cs | 11 + .../DTOs/KnowledgeBaseStatsDto.cs | 41 ++ .../DTOs/KnowledgeBaseUpdateDto.cs | 15 + .../DTOs/VectorizationJobDto.cs | 20 + .../DTOs/VectorizationParamsDto.cs | 18 + .../Interfaces/IDocumentService.cs | 14 +- .../Interfaces/IKnowledgeBaseService.cs | 17 + .../Interfaces/IVectorizationService.cs | 12 + .../Services/DocumentService.cs | 493 +++++++++++++++- .../Services/KnowledgeBaseService.cs | 209 +++++++ .../Services/VectorizationService.cs | 279 ++++++++++ backend/RAG.Domain/Entities/ChatHistory.cs | 5 +- backend/RAG.Domain/Entities/Document.cs | 57 ++ backend/RAG.Domain/Entities/KnowledgeBase.cs | 55 ++ .../RAG.Domain/Entities/VectorizationJob.cs | 90 +++ .../Entities/VectorizationJobStatus.cs | 10 + .../Entities/VectorizationParams.cs | 31 ++ .../Entities/VectorizationStatus.cs | 10 + .../Repositories/IKnowledgeBaseRepository.cs | 12 + .../RAG.Domain/Repositories/IRepository.cs | 2 +- .../RAG.Domain/Repositories/IUnitOfWork.cs | 1 + .../RAG.Domain/Repositories/IVectorStore.cs | 6 +- .../Data/ApplicationDbContext.cs | 43 +- .../FileStorage/LocalFileStorageService.cs | 4 +- ...VectorizationFieldsToDocument.Designer.cs} | 22 +- ...74151_AddVectorizationFieldsToDocument.cs} | 12 +- ...252_UpdateUserTableStringTypes.Designer.cs | 445 +++++++++++++++ ...250806075252_UpdateUserTableStringTypes.cs | 98 ++++ ...1942_AddVectorizationJobEntity.Designer.cs | 525 ++++++++++++++++++ ...0250806091942_AddVectorizationJobEntity.cs | 58 ++ .../ApplicationDbContextModelSnapshot.cs | 106 +++- .../RAG.Infrastructure.csproj | 1 + .../Repositories/ApplicationUnitOfWork.cs | 94 ++++ .../Repositories/DocumentChunkRepository.cs | 29 + .../Repositories/DocumentRepository.cs | 36 ++ .../Repositories/KnowledgeBaseRepository.cs | 29 + .../Repositories/Repository.cs | 76 +++ .../VectorStore/PgVectorStore.cs | 125 +++++ test-upload-fix.ps1 | 76 +++ 55 files changed, 3606 insertions(+), 37 deletions(-) create mode 100644 backend/RAG.Api/Controllers/KnowledgeBaseController.cs create mode 100644 backend/RAG.Api/Controllers/VectorizationController.cs create mode 100644 "backend/RAG.Api/Files/0232ef8e-72b8-49c2-8fcc-81eda2167b8d_\346\234\252\345\221\275\345\220\2151.pdf" create mode 100644 backend/RAG.Api/Files/2fcbacff-d38a-41f7-8357-7c860656a487_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/54bde012-2d46-4a9f-b67b-dbc07ae282d2_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Api/Files/874b4073-c830-447e-a432-5939946969b7_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 "backend/RAG.Api/Files/87750046-e4b0-4269-888e-0dfd41a4a415_\346\234\252\345\221\275\345\220\2151.pdf" create mode 100644 backend/RAG.Api/Files/c93649d2-9f04-4dc0-b417-0861434a5da7_963c1d70789c87eefcdce5cb8d873b1.png create mode 100644 backend/RAG.Application/DTOs/BatchVectorizationRequestDto.cs create mode 100644 backend/RAG.Application/DTOs/DocumentVectorizationStatusDto.cs create mode 100644 backend/RAG.Application/DTOs/KnowledgeBaseCreateDto.cs create mode 100644 backend/RAG.Application/DTOs/KnowledgeBaseDto.cs create mode 100644 backend/RAG.Application/DTOs/KnowledgeBaseQuery.cs create mode 100644 backend/RAG.Application/DTOs/KnowledgeBaseStatsDto.cs create mode 100644 backend/RAG.Application/DTOs/KnowledgeBaseUpdateDto.cs create mode 100644 backend/RAG.Application/DTOs/VectorizationJobDto.cs create mode 100644 backend/RAG.Application/DTOs/VectorizationParamsDto.cs create mode 100644 backend/RAG.Application/Interfaces/IKnowledgeBaseService.cs create mode 100644 backend/RAG.Application/Interfaces/IVectorizationService.cs create mode 100644 backend/RAG.Application/Services/KnowledgeBaseService.cs create mode 100644 backend/RAG.Application/Services/VectorizationService.cs create mode 100644 backend/RAG.Domain/Entities/KnowledgeBase.cs create mode 100644 backend/RAG.Domain/Entities/VectorizationJob.cs create mode 100644 backend/RAG.Domain/Entities/VectorizationJobStatus.cs create mode 100644 backend/RAG.Domain/Entities/VectorizationParams.cs create mode 100644 backend/RAG.Domain/Entities/VectorizationStatus.cs create mode 100644 backend/RAG.Domain/Repositories/IKnowledgeBaseRepository.cs rename backend/RAG.Infrastructure/Migrations/{20250805073332_InitialCreate.Designer.cs => 20250806074151_AddVectorizationFieldsToDocument.Designer.cs} (94%) rename backend/RAG.Infrastructure/Migrations/{20250805073332_InitialCreate.cs => 20250806074151_AddVectorizationFieldsToDocument.cs} (95%) create mode 100644 backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs create mode 100644 backend/RAG.Infrastructure/Repositories/DocumentChunkRepository.cs create mode 100644 backend/RAG.Infrastructure/Repositories/DocumentRepository.cs create mode 100644 backend/RAG.Infrastructure/Repositories/KnowledgeBaseRepository.cs create mode 100644 test-upload-fix.ps1 diff --git a/backend/RAG.Api/Controllers/ChatController.cs b/backend/RAG.Api/Controllers/ChatController.cs index febaf3c..6314df2 100644 --- a/backend/RAG.Api/Controllers/ChatController.cs +++ b/backend/RAG.Api/Controllers/ChatController.cs @@ -69,21 +69,21 @@ namespace RAG.Api.Controllers public class ChatQuery { - public string Message { get; set; } + public required string Message { get; set; } public int? TopK { get; set; } - public string SessionId { get; set; } + public required string SessionId { get; set; } } public class ChatResponse { - public string Query { get; set; } - public string Response { get; set; } - public List References { get; set; } + public required string Query { get; set; } + public required string Response { get; set; } + public required List References { get; set; } } public class Reference { - public string Content { get; set; } + public required string Content { get; set; } public double Similarity { get; set; } } } \ No newline at end of file diff --git a/backend/RAG.Api/Controllers/KnowledgeBaseController.cs b/backend/RAG.Api/Controllers/KnowledgeBaseController.cs new file mode 100644 index 0000000..d8a3c86 --- /dev/null +++ b/backend/RAG.Api/Controllers/KnowledgeBaseController.cs @@ -0,0 +1,152 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services; + +namespace RAG.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class KnowledgeBaseController : ControllerBase + { + private readonly IDocumentService _documentService; + private readonly IKnowledgeBaseService _knowledgeBaseService; + private readonly IVectorizationService _vectorizationService; + + public KnowledgeBaseController( + IDocumentService documentService, + IKnowledgeBaseService knowledgeBaseService, + IVectorizationService vectorizationService) + { + _documentService = documentService; + _knowledgeBaseService = knowledgeBaseService; + _vectorizationService = vectorizationService; + } + + /// + /// 获取知识库统计信息 + /// + [HttpGet("stats")] + public async Task GetKnowledgeBaseStats() + { + try + { + var stats = await _knowledgeBaseService.GetKnowledgeBaseStatsAsync(); + return Ok(stats); + } + catch (System.Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + /// + /// 向量化单个文档 + /// + [HttpPost("documents/{documentId}/vectorize")] + public async Task VectorizeDocument(long documentId, [FromBody] VectorizationParamsDto parameters) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + var result = await _documentService.VectorizeDocumentAsync(documentId, parameters); + return Ok(result); + } + catch (System.ArgumentException ex) + { + return BadRequest(ex.Message); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + + /// + /// 批量向量化文档 + /// + [HttpPost("documents/batch-vectorize")] + public async Task BatchVectorizeDocuments([FromBody] RAG.Application.DTOs.BatchVectorizationRequestDto request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + // 验证请求参数 + if (request.DocumentIds == null || !request.DocumentIds.Any()) + { + return BadRequest("DocumentIds cannot be empty"); + } + var jobId = await _documentService.BatchVectorizeDocumentsAsync(request); + return Ok(new { jobId }); + } + catch (System.ArgumentException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// 获取文档向量化状态 + /// + [HttpGet("documents/{documentId}/vectorization-status")] + public async Task GetDocumentVectorizationStatus(long documentId) + { + try + { + var status = await _documentService.GetDocumentVectorizationStatusAsync(documentId); + return Ok(status); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + + /// + /// 获取向量化任务列表 + /// + [HttpGet("vectorization-jobs")] + public async Task GetVectorizationJobs( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? status = null) + { + try + { + // 直接传递分页参数 + var results = await _documentService.GetVectorizationJobsAsync(page, pageSize); + return Ok(results); + } + catch (System.Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + /// + /// 获取向量化任务详情 + /// + [HttpGet("vectorization-jobs/{jobId}")] + public async Task GetVectorizationJob(long jobId) + { + try + { + var job = await _documentService.GetVectorizationJobAsync(jobId); + return Ok(job); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Controllers/VectorizationController.cs b/backend/RAG.Api/Controllers/VectorizationController.cs new file mode 100644 index 0000000..467f52b --- /dev/null +++ b/backend/RAG.Api/Controllers/VectorizationController.cs @@ -0,0 +1,134 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; + +namespace RAG.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class VectorizationController : ControllerBase + { + private readonly IDocumentService _documentService; + + public VectorizationController(IDocumentService documentService) + { + _documentService = documentService; + } + + /// + /// 获取知识库统计信息 + /// + [HttpGet("knowledge-base-stats")] + public async Task GetKnowledgeBaseStats() + { + var stats = await _documentService.GetKnowledgeBaseStatsAsync(); + return Ok(stats); + } + + /// + /// 对单个文档进行向量化 + /// + [HttpPost("documents/{id}/vectorize")] + public async Task VectorizeDocument(long id, [FromBody] VectorizationParamsDto parameters) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + var result = await _documentService.VectorizeDocumentAsync(id, parameters); + return Ok(result); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (System.InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// 批量向量化文档 + /// + [HttpPost("documents/batch-vectorize")] + public async Task BatchVectorizeDocuments([FromBody] BatchVectorizationRequestDto request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + var result = await _documentService.BatchVectorizeDocumentsAsync(request); + return Ok(result); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (System.InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// 获取文档的向量化状态 + /// + [HttpGet("documents/{id}/vectorization-status")] + public async Task GetDocumentVectorizationStatus(long id) + { + try + { + var status = await _documentService.GetDocumentVectorizationStatusAsync(id); + return Ok(status); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + + /// + /// 获取向量化任务列表 + /// + [HttpGet("jobs")] + public async Task GetVectorizationJobs( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10) + { + try + { + var jobs = await _documentService.GetVectorizationJobsAsync(page, pageSize); + return Ok(jobs); + } + catch (System.ArgumentException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// 获取单个向量化任务 + /// + [HttpGet("jobs/{id}")] + public async Task GetVectorizationJob(long id) + { + try + { + var job = await _documentService.GetVectorizationJobAsync(id); + return Ok(job); + } + catch (System.Collections.Generic.KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + } + } +} \ No newline at end of file diff --git "a/backend/RAG.Api/Files/0232ef8e-72b8-49c2-8fcc-81eda2167b8d_\346\234\252\345\221\275\345\220\2151.pdf" "b/backend/RAG.Api/Files/0232ef8e-72b8-49c2-8fcc-81eda2167b8d_\346\234\252\345\221\275\345\220\2151.pdf" new file mode 100644 index 0000000000000000000000000000000000000000..1e53172a688b1d8bc9e974c6aa799b17ad32eeea GIT binary patch literal 1243 zcmZ{kO>5gQ7{`0q$s@4)F1my!l+}_f$?^z>#Ihl@T|*M~LI}l;mDDA%gRQW3pJE@N zm)*AWKEq(J^BC;B4>Bn)O_p}PSbFmO|4;mT9;w<9(F=6!TGdZqzx}chfFQrTu2_wR z{VJZxQon^PRSAgxr3-xDK3}M5n#mH-WyJ2p6TQK!P5*W-?I2e1G@sa=RE}gj8|MJQ zwc3(dqWvwCzNr)Bi%bDzpQK4S0Bo5#w3kjxZ{OdjoE=qjp|0|xR=0zEHj|kuSIc>v z-I`S)W0mHah?T5~ml$FSIb_fyn6e|t4h_7eeV-SJ@jA@%69aVx(W=q&`)qsQ1~fD?4gy`k=7p_Bh< zq`|+nHZB|MD4i-<*wIw;6>^j((ms{hL|p;w`F^Q1{+V@icl3w$;X!NoOvaa=C-Ik$ zcR!wge|pgP{p6?V={j#c6MVk%Vy+Nag>B3kUcXMDi!+TT6u>}D_6${=!fKlpDFy6+HZmW4fb{|7Xd+o0juYt(*h7aZh74FCWD literal 0 HcmV?d00001 diff --git a/backend/RAG.Api/Files/2fcbacff-d38a-41f7-8357-7c860656a487_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/2fcbacff-d38a-41f7-8357-7c860656a487_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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 literal 0 HcmV?d00001 diff --git a/backend/RAG.Api/Files/c93649d2-9f04-4dc0-b417-0861434a5da7_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/c93649d2-9f04-4dc0-b417-0861434a5da7_963c1d70789c87eefcdce5cb8d873b1.png new file mode 100644 index 0000000000000000000000000000000000000000..3cdeafe89383135294ae784cb8a30dfaa35fb558 GIT binary patch literal 11973 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 +// 注册知识库服务 +builder.Services.AddScoped(); + +// 注册向量存储服务 +builder.Services.AddScoped(); + +// 注册向量任务仓储 +builder.Services.AddScoped, RAG.Infrastructure.Repositories.Repository>(); + +// 注册知识库仓储 +builder.Services.AddScoped(); + +// 注册文档仓储 +builder.Services.AddScoped(); + +// 注册文档分块仓储 +builder.Services.AddScoped(); + +// 注册向量化服务 +builder.Services.AddScoped(); + +// 注册工作单元 +builder.Services.AddScoped(); + builder.Services.AddCors(options => { options.AddPolicy("AllowAll", diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index f159594..5f69673 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -68,19 +68,19 @@ Accept: application/json ### #### 6. 获取单个文档 -GET {{url}}/api/document/2826681530093839908 +GET {{url}}/api/document/8375959069814838810 Accept: application/json ### #### 7. 预览文档 -GET {{url}}/api/document/8371670605660901366/preview +GET {{url}}/api/document/8375959069814838810/preview Accept: application/json ### #### 8. 更新文档(包含权限) -PUT {{url}}/api/document/2826681530093839908 +PUT {{url}}/api/document/8375959069814838810 Content-Type: application/json { @@ -93,7 +93,7 @@ Content-Type: application/json ### #### 9. 删除单个文档 -DELETE {{url}}/api/document/642342144492866748 +DELETE {{url}}/api/document/4631834103230408532 ### @@ -102,7 +102,7 @@ PUT {{url}}/api/document/update-access-level Content-Type: application/json { - "Ids": [4125359736557238617, 2517892471848059843], + "Ids": [3040509232552355159, 1876963397423496466], "AccessLevel": "public" } @@ -113,11 +113,66 @@ DELETE {{url}}/api/document/batch Content-Type: application/json [ - 6130474657473397545, 6804474346525374423, 9125508367891060242 +3040509232552355159, 1876963397423496466 ] ### +#### 知识库管理 API 测试 + +#### 1. 获取知识库统计信息 +GET {{url}}/api/knowledgebase/stats +Accept: application/json + +### + +#### 2. 向量化单个文档 +POST {{url}}/api/knowledgebase/documents/7651925779515340169/vectorize +Content-Type: application/json + +{ + "ModelName": "default", + "ChunkSize": 500, + "ChunkOverlap": 50, + "Separator": "\n\n" +} + +### + +#### 3. 批量向量化文档 +POST {{url}}/api/knowledgebase/documents/batch-vectorize +Content-Type: application/json + +{ + "DocumentIds": [172579765607330619, 7483280351647518150, 32055697673725165], + "Parameters": { + "ModelName": "default", + "ChunkSize": 500, + "ChunkOverlap": 50, + "Separator": "\n\n" + } +} + +### + +#### 4. 获取文档向量化状态 +GET {{url}}/api/knowledgebase/documents/1/vectorization-status +Accept: application/json + +### + +#### 5. 获取向量化任务列表 +GET {{url}}/api/knowledgebase/vectorization-jobs?page=1&pageSize=10&status=Processing +Accept: application/json + +### + +#### 6. 获取向量化任务详情 +GET {{url}}/api/knowledgebase/vectorization-jobs/1 +Accept: application/json + +### + #### 原始天气 API 测试 (保留) GET {{url}}/weatherforecast/ Accept: application/json diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json index 57a1dc2..e496afb 100644 --- a/backend/RAG.Api/appsettings.json +++ b/backend/RAG.Api/appsettings.json @@ -7,10 +7,10 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Host=www.mingyuy.cn;Port=5432;Database=postgres;Username=postgres;Password=mingyuy." + "DefaultConnection": "Host=www.mingyuy.cn;Port=5432;Database=rag_db;Username=postgres;Password=mingyuy." }, "VectorStore": { "Type": "Postgres", - "ConnectionString": "Host=www.mingyuy.cn;Port=5432;Database=postgres;Username=postgres;Password=mingyuy." + "ConnectionString": "Host=www.mingyuy.cn;Port=5432;Database=rag_db;Username=postgres;Password=mingyuy." } } diff --git a/backend/RAG.Application/DTOs/BatchVectorizationRequestDto.cs b/backend/RAG.Application/DTOs/BatchVectorizationRequestDto.cs new file mode 100644 index 0000000..8eaf388 --- /dev/null +++ b/backend/RAG.Application/DTOs/BatchVectorizationRequestDto.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace RAG.Application.DTOs +{ + public class BatchVectorizationRequestDto + { + [Required] + [MinLength(1)] + public List DocumentIds { get; set; } = new List(); + + [Required] + public VectorizationParamsDto Parameters { get; set; } = new VectorizationParamsDto(); + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/DocumentVectorizationStatusDto.cs b/backend/RAG.Application/DTOs/DocumentVectorizationStatusDto.cs new file mode 100644 index 0000000..c7eabcd --- /dev/null +++ b/backend/RAG.Application/DTOs/DocumentVectorizationStatusDto.cs @@ -0,0 +1,16 @@ +using System; + +namespace RAG.Application.DTOs +{ + public class DocumentVectorizationStatusDto + { + public long DocumentId { get; set; } + public string Title { get; set; } = string.Empty; + public string VectorizationStatus { get; set; } = string.Empty; + public int VectorizedChunks { get; set; } + public int TotalChunks { get; set; } + public double Progress => TotalChunks > 0 ? (double)VectorizedChunks / TotalChunks * 100 : 0; + public string? ErrorMessage { get; set; } + public DateTime? LastVectorizedAt { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/KnowledgeBaseCreateDto.cs b/backend/RAG.Application/DTOs/KnowledgeBaseCreateDto.cs new file mode 100644 index 0000000..9fdb630 --- /dev/null +++ b/backend/RAG.Application/DTOs/KnowledgeBaseCreateDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + + +namespace RAG.Application.DTOs +{ + public class KnowledgeBaseCreateDto + { + [Required] + [MaxLength(100)] + public required string Name { get; set; } + + [MaxLength(500)] + public required string Description { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/KnowledgeBaseDto.cs b/backend/RAG.Application/DTOs/KnowledgeBaseDto.cs new file mode 100644 index 0000000..66ad8aa --- /dev/null +++ b/backend/RAG.Application/DTOs/KnowledgeBaseDto.cs @@ -0,0 +1,17 @@ +using System; + +using System; + +namespace RAG.Application.DTOs +{ + public class KnowledgeBaseDto + { + public long Id { get; set; } + public required string Name { get; set; } + public required string Description { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public int DocumentCount { get; set; } + public required KnowledgeBaseStatsDto Stats { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/KnowledgeBaseQuery.cs b/backend/RAG.Application/DTOs/KnowledgeBaseQuery.cs new file mode 100644 index 0000000..474a984 --- /dev/null +++ b/backend/RAG.Application/DTOs/KnowledgeBaseQuery.cs @@ -0,0 +1,11 @@ +namespace RAG.Application.DTOs +{ + public class KnowledgeBaseQuery +{ + public required string SearchTerm { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 10; + public string SortBy { get; set; } = "CreatedAt"; + public bool SortDescending { get; set; } = true; +} +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/KnowledgeBaseStatsDto.cs b/backend/RAG.Application/DTOs/KnowledgeBaseStatsDto.cs new file mode 100644 index 0000000..cbce8e5 --- /dev/null +++ b/backend/RAG.Application/DTOs/KnowledgeBaseStatsDto.cs @@ -0,0 +1,41 @@ +using System; + +namespace RAG.Application.DTOs +{ + public class KnowledgeBaseStatsDto + { + // 文档总数统计 + public int TotalDocuments { get; set; } + public int PendingDocuments { get; set; } + public int ProcessingDocuments { get; set; } + public int CompletedDocuments { get; set; } + public int FailedDocuments { get; set; } + public int VectorizedDocuments { get; set; } + + // 向量数据统计 + public int TotalChunks { get; set; } + public int VectorizedChunks { get; set; } + public int NotVectorizedChunks { get; set; } + public double VectorizationProgress => TotalChunks > 0 ? (double)VectorizedChunks / TotalChunks * 100 : 0; + public long VectorDataSizeBytes { get; set; } + public string VectorDataSize => FormatFileSize(VectorDataSizeBytes); + + // 存储空间统计 + public long TotalFileSizeBytes { get; set; } + public string TotalFileSize => FormatFileSize(TotalFileSizeBytes); + public long VectorStorageSizeBytes { get; set; } + public string VectorStorageSize => FormatFileSize(VectorStorageSizeBytes); + + private string FormatFileSize(long bytes) + { + if (bytes < 1024) + return $"{bytes} B"; + else if (bytes < 1024 * 1024) + return $"{bytes / 1024.0:F2} KB"; + else if (bytes < 1024 * 1024 * 1024) + return $"{bytes / (1024.0 * 1024):F2} MB"; + else + return $"{bytes / (1024.0 * 1024 * 1024):F2} GB"; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/KnowledgeBaseUpdateDto.cs b/backend/RAG.Application/DTOs/KnowledgeBaseUpdateDto.cs new file mode 100644 index 0000000..0e671e0 --- /dev/null +++ b/backend/RAG.Application/DTOs/KnowledgeBaseUpdateDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace RAG.Application.DTOs +{ + public class KnowledgeBaseUpdateDto + { + [MaxLength(100)] + [Required] + public string Name { get; set; } = null!; + + [MaxLength(500)] + [Required] + public string Description { get; set; } = null!; + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/VectorizationJobDto.cs b/backend/RAG.Application/DTOs/VectorizationJobDto.cs new file mode 100644 index 0000000..a656c9a --- /dev/null +++ b/backend/RAG.Application/DTOs/VectorizationJobDto.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace RAG.Application.DTOs +{ + public class VectorizationJobDto + { + public long Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public int ProcessedCount { get; set; } + public int TotalCount { get; set; } + public double Progress => TotalCount > 0 ? (double)ProcessedCount / TotalCount * 100 : 0; + public string? ErrorMessage { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public VectorizationParamsDto Parameters { get; set; } = new VectorizationParamsDto(); + public List DocumentIds { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/VectorizationParamsDto.cs b/backend/RAG.Application/DTOs/VectorizationParamsDto.cs new file mode 100644 index 0000000..8e00e74 --- /dev/null +++ b/backend/RAG.Application/DTOs/VectorizationParamsDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace RAG.Application.DTOs +{ + public class VectorizationParamsDto + { + [Required] + public string ModelName { get; set; } = string.Empty; + + [Range(1, int.MaxValue)] + public int ChunkSize { get; set; } = 1000; + + [Range(0, int.MaxValue)] + public int ChunkOverlap { get; set; } = 200; + + public string? Separator { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Interfaces/IDocumentService.cs b/backend/RAG.Application/Interfaces/IDocumentService.cs index 498e2df..c051fa4 100644 --- a/backend/RAG.Application/Interfaces/IDocumentService.cs +++ b/backend/RAG.Application/Interfaces/IDocumentService.cs @@ -28,6 +28,18 @@ namespace RAG.Application.Interfaces // 批量更新文档权限 Task UpdateDocumentsAccessLevelAsync(IEnumerable ids, string accessLevel); + + // 知识库统计 + Task GetKnowledgeBaseStatsAsync(); + + // 文档向量化 + Task VectorizeDocumentAsync(long documentId, VectorizationParamsDto parameters); + Task BatchVectorizeDocumentsAsync(BatchVectorizationRequestDto request); + + // 向量化状态 + Task GetDocumentVectorizationStatusAsync(long documentId); + Task> GetVectorizationJobsAsync(int page = 1, int pageSize = 10); + Task GetVectorizationJobAsync(long jobId); } public class DocumentQuery @@ -42,7 +54,7 @@ namespace RAG.Application.Interfaces public class PagedResult { - public IEnumerable Items { get; set; } = Enumerable.Empty(); +public IEnumerable Items { get; set; } = Array.Empty(); public int TotalCount { get; set; } public int Page { get; set; } public int PageSize { get; set; } diff --git a/backend/RAG.Application/Interfaces/IKnowledgeBaseService.cs b/backend/RAG.Application/Interfaces/IKnowledgeBaseService.cs new file mode 100644 index 0000000..b2adc50 --- /dev/null +++ b/backend/RAG.Application/Interfaces/IKnowledgeBaseService.cs @@ -0,0 +1,17 @@ + + + +using RAG.Application.DTOs; + +namespace RAG.Application.Interfaces +{ + public interface IKnowledgeBaseService + { + Task GetKnowledgeBaseStatsAsync(); + Task CreateKnowledgeBaseAsync(KnowledgeBaseCreateDto createDto); + Task UpdateKnowledgeBaseAsync(long id, KnowledgeBaseUpdateDto updateDto); + Task DeleteKnowledgeBaseAsync(long id); + Task GetKnowledgeBaseByIdAsync(long id); + Task> GetKnowledgeBasesAsync(KnowledgeBaseQuery query); + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Interfaces/IVectorizationService.cs b/backend/RAG.Application/Interfaces/IVectorizationService.cs new file mode 100644 index 0000000..467372a --- /dev/null +++ b/backend/RAG.Application/Interfaces/IVectorizationService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using RAG.Application.DTOs; + +namespace RAG.Application.Interfaces +{ + public interface IVectorizationService + { + Task VectorizeDocumentAsync(long documentId, VectorizationParamsDto parameters); + Task BatchVectorizeDocumentsAsync(IEnumerable documentIds, VectorizationParamsDto parameters); + Task GenerateEmbeddingAsync(string text); + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs index df7c452..7dedf98 100644 --- a/backend/RAG.Application/Services/DocumentService.cs +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -7,20 +7,28 @@ using Microsoft.EntityFrameworkCore; using RAG.Application.DTOs; using RAG.Application.Interfaces; using RAG.Domain.Entities; +using RAG.Domain.Repositories; using RAG.Infrastructure.FileStorage; using RAG.Infrastructure.Data; +using RAG.Infrastructure.VectorStore; namespace RAG.Application.Services { public class DocumentService : IDocumentService { private readonly ApplicationDbContext _context; - private readonly IFileStorageService _fileStorageService; + private readonly IFileStorageService _fileStorageService; + private readonly IVectorStore _vectorStore; + private readonly IRepository _vectorizationJobRepository; + private readonly IUnitOfWork _unitOfWork; - public DocumentService(ApplicationDbContext context, IFileStorageService fileStorageService) + public DocumentService(ApplicationDbContext context, IFileStorageService fileStorageService, IVectorStore vectorStore, IRepository vectorizationJobRepository, IUnitOfWork unitOfWork) { _context = context; - _fileStorageService = fileStorageService; + _fileStorageService = fileStorageService; + _vectorStore = vectorStore; + _vectorizationJobRepository = vectorizationJobRepository; + _unitOfWork = unitOfWork; } public async Task UploadDocumentAsync(DocumentUploadDto uploadDto) @@ -55,6 +63,10 @@ namespace RAG.Application.Services uploadDto.File.Length, uploadDto.AccessLevel); + // 更新文档状态为已完成 + document.UpdateProcessingStatus(); + document.CompleteProcessing(); + // 保存到数据库 _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -176,7 +188,7 @@ namespace RAG.Application.Services // 对于文本文档和PDF,我们可以尝试读取内容 string content = string.Empty; - byte[] fileData = null; + byte[]? fileData = null; if (document.FileType.ToLower() == "txt" || document.FileType.ToLower() == "md") { @@ -315,5 +327,478 @@ else await _context.SaveChangesAsync(); return true; } + public async Task GetKnowledgeBaseStatsAsync() + { + // 获取文档总数 + var totalDocuments = await _context.Documents.CountAsync(); + + // 获取已向量化的文档数 + var vectorizedDocuments = await _context.Documents + .CountAsync(d => d.VectorizationStatus == VectorizationStatus.Vectorized); + + // 获取文档块总数 + var totalChunks = await _context.DocumentChunks.CountAsync(); + + // 获取向量数据大小(假设每个向量1536维,每维4字节) + var vectorDataSizeBytes = totalChunks * 1536 * 4; + + // 获取存储空间使用量 +var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) ?? 0L; + + // 获取已向量化的文档数量 + vectorizedDocuments = await _context.Documents.CountAsync(d => d.VectorizationStatus == VectorizationStatus.Vectorized); + + // 获取已向量化的块数量(通过关联Document的VectorizationStatus) + var vectorizedChunks = await _context.DocumentChunks + .Join( + _context.Documents.Where(d => d.VectorizationStatus == VectorizationStatus.Vectorized), + chunk => chunk.DocumentId, + document => document.Id, + (chunk, document) => chunk + ) + .CountAsync(); + + // 计算向量存储空间大小(假设每个向量1536维,每维4字节) + var vectorStorageSizeBytes = vectorizedChunks * 1536 * sizeof(float); + + return new KnowledgeBaseStatsDto + { + TotalDocuments = totalDocuments, + VectorizedDocuments = vectorizedDocuments, + TotalChunks = totalChunks, + VectorDataSizeBytes = vectorDataSizeBytes, + TotalFileSizeBytes = storageSizeBytes, + VectorStorageSizeBytes = vectorStorageSizeBytes + }; + } + + public async Task VectorizeDocumentAsync(long documentId, VectorizationParamsDto parameters) + { + // 验证文档存在 + var document = await _context.Documents.FindAsync(documentId); + if (document == null) + { + throw new KeyNotFoundException($"Document with id {documentId} not found"); + } + + // 验证文档状态 + if (document.Status != DocumentStatus.Completed) + { + throw new InvalidOperationException($"Document {documentId} is not in Completed status"); + } + + // 检查是否已向量化或正在向量化 + if (document.VectorizationStatus == VectorizationStatus.Vectorized) + { + throw new InvalidOperationException($"Document {documentId} is already vectorized"); + } + if (document.VectorizationStatus == VectorizationStatus.Vectorizing) + { + throw new InvalidOperationException($"Document {documentId} is already being vectorized"); + } + + // 创建向量化任务 + var job = VectorizationJob.Create( + name: $"Vectorize document: {document.Title}", + documentIds: new List { documentId }, + parameters: new VectorizationParams( + modelName: parameters.ModelName, + chunkSize: parameters.ChunkSize, + chunkOverlap: parameters.ChunkOverlap, + separator: parameters.Separator) + ); + + // 使用 AddAsync 方法添加向量化任务 + await _vectorizationJobRepository.AddAsync(job); + await _unitOfWork.SaveChangesAsync(); + + // 开始文档向量化 + // 读取文档内容以确定总块数 + string content; + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + var chunks = SplitTextIntoChunks( + content, + parameters.ChunkSize, + parameters.ChunkOverlap, + parameters.Separator ?? string.Empty); + document.StartVectorization(chunks.Count); + await _unitOfWork.SaveChangesAsync(); + + // 异步处理向量化 + await Task.Run(async () => + { + try + { + // 读取文档内容 + string content; + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + + // 文本分块 + var chunks = SplitTextIntoChunks( + content, + parameters.ChunkSize, + parameters.ChunkOverlap, + parameters.Separator ?? string.Empty); + + document.StartVectorization(chunks.Count); + await _unitOfWork.SaveChangesAsync(); + + // 处理每个块 + for (int i = 0; i < chunks.Count; i++) + { + var chunkContent = chunks[i]; + + // 生成嵌入向量 + var embedding = GenerateEmbedding(chunkContent); + + // 存储向量 + await _vectorStore.StoreVectorAsync( + chunkContent, + embedding, + new + { + documentId = document.Id, + chunkId = i + 1, + position = i + 1 + }); + + // 创建文档块 + var chunk = DocumentChunk.Create( + document.Id, + chunkContent, + i + 1, + chunkContent.Length); + + _context.DocumentChunks.Add(chunk); + document.AddChunk(chunk); + + // 更新进度 + document.UpdateVectorizationProgress(i + 1); + await _unitOfWork.SaveChangesAsync(); + } + + // 完成文档向量化 + document.CompleteVectorization(); + job.Complete(); + await _unitOfWork.SaveChangesAsync(); + } + catch (Exception ex) + { + // 记录文档向量化失败 + document.FailVectorization(ex.Message); + job.Fail(ex.Message); + await _unitOfWork.SaveChangesAsync(); + } + }); + + return true; + } + + public async Task BatchVectorizeDocumentsAsync(BatchVectorizationRequestDto request) + { + // 验证文档存在 + var documents = await _context.Documents + .Where(d => request.DocumentIds.Contains(d.Id)) + .ToListAsync(); + + var invalidDocuments = request.DocumentIds + .Where(id => !documents.Any(d => d.Id == id)) + .ToList(); + + if (invalidDocuments.Any()) + { + throw new KeyNotFoundException($"Documents not found: {string.Join(", ", invalidDocuments)}"); + } + + // 验证文档状态 + var invalidStatusDocuments = documents + .Where(d => d.Status != DocumentStatus.Completed) + .ToList(); + + if (invalidStatusDocuments.Any()) + { + throw new InvalidOperationException($"Documents not in Completed status: {string.Join(", ", invalidStatusDocuments.Select(d => d.Id))}"); + } + + // 检查是否已向量化或正在向量化 + var alreadyVectorizedDocuments = documents + .Where(d => d.VectorizationStatus == VectorizationStatus.Vectorized) + .ToList(); + + if (alreadyVectorizedDocuments.Count != 0) + { + throw new InvalidOperationException($"Documents already vectorized: {string.Join(", ", alreadyVectorizedDocuments.Select(d => d.Id))}"); + } + + var alreadyVectorizingDocuments = documents + .Where(d => d.VectorizationStatus == VectorizationStatus.Vectorizing) + .ToList(); + + if (alreadyVectorizingDocuments.Count != 0) + { + throw new InvalidOperationException($"Documents already being vectorized: {string.Join(", ", alreadyVectorizingDocuments.Select(d => d.Id))}"); + } + + // 创建向量化任务 + var job = VectorizationJob.Create( + name: $"Batch vectorization ({request.DocumentIds.Count} documents)", + documentIds: request.DocumentIds.ToList(), + parameters: new VectorizationParams( + modelName: request.Parameters.ModelName, + chunkSize: request.Parameters.ChunkSize, + chunkOverlap: request.Parameters.ChunkOverlap, + separator: request.Parameters.Separator) + ); + + await _vectorizationJobRepository.AddAsync(job); + await _unitOfWork.SaveChangesAsync(); + + // 异步处理批量向量化 + await Task.Run(async () => + { + try + { + foreach (var document in documents) + { + try + { + // 读取文档内容 + string content; + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + // 文本分块 + var chunks = SplitTextIntoChunks( + content, + request.Parameters.ChunkSize, + request.Parameters.ChunkOverlap, + request.Parameters.Separator ?? string.Empty); + // 开始文档向量化 + document.StartVectorization(chunks.Count); + await _unitOfWork.SaveChangesAsync(); + + // 处理每个块 + for (int i = 0; i < chunks.Count; i++) + { + var chunkContent = chunks[i]; + + // 生成嵌入向量 + var embedding = GenerateEmbedding(chunkContent); + + // 存储向量 + await _vectorStore.StoreVectorAsync( + chunkContent, + embedding, + new + { + documentId = document.Id, + chunkId = i + 1, + position = i + 1 + }); + + // 创建文档块 + var chunk = DocumentChunk.Create( + document.Id, + chunkContent, + i + 1, + chunkContent.Length); + + _context.DocumentChunks.Add(chunk); + document.AddChunk(chunk); + + // 更新进度 + document.UpdateVectorizationProgress(i + 1); + await _unitOfWork.SaveChangesAsync(); + } + + // 完成文档向量化 + document.CompleteVectorization(); + await _unitOfWork.SaveChangesAsync(); + } + catch (Exception ex) + { + // 记录文档向量化失败 + document.FailVectorization(ex.Message); + await _unitOfWork.SaveChangesAsync(); + } + } + + // 检查是否所有文档都已处理完成 + var updatedDocuments = await _context.Documents + .Where(d => request.DocumentIds.Contains(d.Id)) + .ToListAsync(); + + var allCompleted = updatedDocuments.All(d => + d.VectorizationStatus == VectorizationStatus.Vectorized || + d.VectorizationStatus == VectorizationStatus.VectorizationFailed); + + if (allCompleted) + { + // 如果所有文档都已完成,标记任务为完成 + job.Complete(); + } + else + { + // 否则标记为失败 + job.Fail("Some documents failed to vectorize"); + } + await _unitOfWork.SaveChangesAsync(); + } + catch (Exception ex) + { + // 记录任务失败 + job.Fail(ex.Message); + await _unitOfWork.SaveChangesAsync(); + } + }); + + return job.Id; + } + + public async Task GetDocumentVectorizationStatusAsync(long documentId) + { + var document = await _context.Documents.FindAsync(documentId); + if (document == null) + { + throw new KeyNotFoundException($"Document with id {documentId} not found"); + } + + return new DocumentVectorizationStatusDto + { + DocumentId = document.Id, + Title = document.Title, + VectorizationStatus = document.VectorizationStatus.ToString(), + TotalChunks = document.TotalChunks, + VectorizedChunks = document.VectorizedChunks, + ErrorMessage = document.VectorizationErrorMessage, + LastVectorizedAt = document.LastVectorizedAt + }; + } + + public async Task> GetVectorizationJobsAsync(int page = 1, int pageSize = 10) + { + if (page <= 0) + throw new ArgumentException("Page must be greater than 0"); + if (pageSize <= 0) + throw new ArgumentException("PageSize must be greater than 0"); + + // 获取所有任务并转换为查询able + var allJobs = await _vectorizationJobRepository.GetAllAsync(); + var queryable = allJobs.AsQueryable(); + + // 分页 + var totalCount = queryable.Count(); + var items = queryable + .OrderByDescending(j => j.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(j => new VectorizationJobDto + { + Id = j.Id, + Name = j.Name, + Status = j.Status.ToString(), + TotalCount = j.DocumentIds.Count, + CreatedAt = j.CreatedAt, + UpdatedAt = j.UpdatedAt + }) + .ToList(); + + return new PagedResult + { + Items = items, + TotalCount = totalCount, + Page = page, + PageSize = pageSize + }; + } + + public async Task GetVectorizationJobAsync(long jobId) + { + var job = await _vectorizationJobRepository.GetByIdAsync((int)jobId); + if (job == null) + { + throw new KeyNotFoundException($"Vectorization job with id {jobId} not found"); + } + + return new VectorizationJobDto + { + Id = job.Id, + Name = job.Name, + Status = job.Status.ToString(), + TotalCount = job.DocumentIds.Count, + CreatedAt = job.CreatedAt, + UpdatedAt = job.UpdatedAt + }; + } + + // 文本分块辅助方法 + private static List SplitTextIntoChunks(string text, int chunkSize, int chunkOverlap, string separator) + { + var chunks = new List(); + if (string.IsNullOrEmpty(text)) + return chunks; + + // 使用分隔符分割文本 + var paragraphs = text.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries); + + var currentChunk = new List(); + var currentSize = 0; + + foreach (var paragraph in paragraphs) + { + var paragraphSize = paragraph.Length; + + if (currentSize + paragraphSize > chunkSize && currentChunk.Any()) + { + // 达到块大小,添加到结果 + chunks.Add(string.Join(separator, currentChunk)); + + // 保留重叠部分 + while (currentChunk.Count > 0 && currentSize - currentChunk[0].Length > chunkSize - chunkOverlap) + { + currentSize -= currentChunk[0].Length; + currentChunk.RemoveAt(0); + } + } + + // 添加当前段落 + currentChunk.Add(paragraph); + currentSize += paragraphSize; + } + + // 添加最后一个块 + if (currentChunk.Count > 0) + { + chunks.Add(string.Join(separator, currentChunk)); + } + + return chunks; + } + + // 生成嵌入向量的辅助方法(模拟) + private float[] GenerateEmbedding(string content) + { + // 模拟嵌入向量生成 + // 实际应用中,这里应该调用嵌入模型API + var random = new Random(content.GetHashCode()); + var embedding = new float[1536]; // 假设使用1536维向量 + + for (int i = 0; i < embedding.Length; i++) + { + embedding[i] = (float)(random.NextDouble() * 2 - 1); // 生成-1到1之间的随机数 + } + + return embedding; + } } } \ No newline at end of file diff --git a/backend/RAG.Application/Services/KnowledgeBaseService.cs b/backend/RAG.Application/Services/KnowledgeBaseService.cs new file mode 100644 index 0000000..4483549 --- /dev/null +++ b/backend/RAG.Application/Services/KnowledgeBaseService.cs @@ -0,0 +1,209 @@ +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using Microsoft.Extensions.Logging; +using System.Linq.Expressions; + +namespace RAG.Application.Services +{ + public class KnowledgeBaseService( + IKnowledgeBaseRepository knowledgeBaseRepository, + IDocumentRepository documentRepository, + IDocumentChunkRepository documentChunkRepository, + IUnitOfWork unitOfWork, + ILogger logger) : IKnowledgeBaseService + { + private readonly IKnowledgeBaseRepository _knowledgeBaseRepository = knowledgeBaseRepository; + private readonly IDocumentRepository _documentRepository = documentRepository; + private readonly IDocumentChunkRepository _documentChunkRepository = documentChunkRepository; + private readonly IUnitOfWork _unitOfWork = unitOfWork; + private readonly ILogger _logger = logger; + + public async Task GetKnowledgeBaseStatsAsync() + { + try + { + // 获取文档统计信息 + var totalDocuments = await _documentRepository.CountAsync(); + var pendingDocuments = await _documentRepository.CountByStatusAsync(DocumentStatus.Pending); + var processingDocuments = await _documentRepository.CountByStatusAsync(DocumentStatus.Processing); + var completedDocuments = await _documentRepository.CountByStatusAsync(DocumentStatus.Completed); + var failedDocuments = totalDocuments - pendingDocuments - processingDocuments - completedDocuments; + + // 获取分块统计信息 + var allDocuments = await _documentRepository.GetAllAsync(); + var totalChunks = allDocuments.Sum(d => d.TotalChunks); + var vectorizedChunks = allDocuments.Sum(d => d.VectorizedChunks); + var notVectorizedChunks = totalChunks - vectorizedChunks; + + // 获取存储统计信息 + var totalFileSizeBytes = allDocuments.Sum(d => d.FileSize); + var vectorStorageSizeBytes = vectorizedChunks * 1536 * sizeof(float); // 假设每个向量1536维,每维4字节 + + return new KnowledgeBaseStatsDto + { + TotalDocuments = totalDocuments, + PendingDocuments = pendingDocuments, + ProcessingDocuments = processingDocuments, + CompletedDocuments = completedDocuments, + FailedDocuments = failedDocuments, + TotalChunks = totalChunks, + VectorizedChunks = vectorizedChunks, + NotVectorizedChunks = notVectorizedChunks, + TotalFileSizeBytes = totalFileSizeBytes, + VectorStorageSizeBytes = vectorStorageSizeBytes + }; + } + catch (System.Exception ex) + { + _logger.LogError(ex, "Failed to get knowledge base stats"); + throw; + } + } + + public async Task CreateKnowledgeBaseAsync(KnowledgeBaseCreateDto createDto) + { + try + { + if (createDto == null) + throw new ArgumentNullException(nameof(createDto)); + + // 验证输入 + if (string.IsNullOrWhiteSpace(createDto.Name)) + throw new ArgumentException("Knowledge base name cannot be empty"); + + // 创建知识库实体 + var knowledgeBase = KnowledgeBase.Create(createDto.Name, createDto.Description); + + // 保存到数据库 + await _knowledgeBaseRepository.AddAsync(knowledgeBase); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation($"Created knowledge base with id {knowledgeBase.Id}"); + return knowledgeBase.Id; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create knowledge base"); + throw; + } + } + + public async Task UpdateKnowledgeBaseAsync(long id, KnowledgeBaseUpdateDto updateDto) + { + try + { + if (updateDto == null) + throw new ArgumentNullException(nameof(updateDto)); + + // 查找知识库 + var knowledgeBase = await _knowledgeBaseRepository.GetByIdAsync((int)id); + + if (knowledgeBase == null) + return false; + + // 更新属性 + if (!string.IsNullOrWhiteSpace(updateDto.Name)) + knowledgeBase.UpdateName(updateDto.Name); + + if (updateDto.Description != null) + knowledgeBase.UpdateDescription(updateDto.Description); + + // 保存更改 +// 无需显式调用 Update 方法,因为实体状态变更会被 UnitOfWork 跟踪 +// 直接调用 SaveChangesAsync 即可保存更改 +// _knowledgeBaseRepository.Update(knowledgeBase); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation("Updated knowledge base with id {id}", id); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update knowledge base with id {id}", id); + throw; + } + } + + public async Task DeleteKnowledgeBaseAsync(long id) + { + try + { + // 查找知识库 + var knowledgeBase = await _knowledgeBaseRepository.GetByIdAsync((int)id); + if (knowledgeBase == null) + return false; + + // 删除知识库 + _knowledgeBaseRepository.Remove(knowledgeBase); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation($"Deleted knowledge base with id {id}"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete knowledge base with id {id}", id); + throw; + } + } + + public async Task GetKnowledgeBaseByIdAsync(long id) + { + try + { + // 查找知识库 + var knowledgeBase = await _knowledgeBaseRepository.GetByIdAsync((int)id); + if (knowledgeBase == null) + throw new KeyNotFoundException($"Knowledge base with id {id} not found"); + + // 获取文档数量 + var documentCount = knowledgeBase.Documents.Count; + + // 创建并返回DTO + return new KnowledgeBaseDto + { + Id = knowledgeBase.Id, + Name = knowledgeBase.Name, + Description = knowledgeBase.Description, + CreatedAt = knowledgeBase.CreatedAt, + UpdatedAt = knowledgeBase.UpdatedAt, + DocumentCount = documentCount, + Stats = await GetKnowledgeBaseStatsAsync() + }; + } + catch (KeyNotFoundException) + { + _logger.LogWarning("Knowledge base with id {id} not found", id); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get knowledge base with id {id}", id); + throw; + } + } + + public Task> GetKnowledgeBasesAsync(KnowledgeBaseQuery query) + + + { + try + { + ArgumentNullException.ThrowIfNull(query, nameof(query)); + throw new ArgumentNullException(nameof(query)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get knowledge bases"); + throw; + } + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/VectorizationService.cs b/backend/RAG.Application/Services/VectorizationService.cs new file mode 100644 index 0000000..989db80 --- /dev/null +++ b/backend/RAG.Application/Services/VectorizationService.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using Microsoft.Extensions.Logging; +using RAG.Infrastructure.FileStorage; + +namespace RAG.Application.Services +{ + public class VectorizationService( + IVectorStore vectorStore, + IDocumentRepository documentRepository, + IFileStorageService fileStorageService, + IUnitOfWork unitOfWork, + ILogger logger) : IVectorizationService + { + private readonly IVectorStore _vectorStore = vectorStore; + private readonly IDocumentRepository _documentRepository = documentRepository; + private readonly IFileStorageService _fileStorageService = fileStorageService; + private readonly IUnitOfWork _unitOfWork = unitOfWork; + private readonly ILogger _logger = logger; + + public async Task VectorizeDocumentAsync(long documentId, VectorizationParamsDto parameters) + { + try + { + // 初始化向量存储 + // 直接调用 InitializeAsync 方法,该方法是幂等的 + await _vectorStore.InitializeAsync(); + + // 获取文档 + var document = await _documentRepository.GetByIdAsync((int)documentId); + if (document == null) + { + _logger.LogError("Document with id {DocumentId} not found", documentId); + return false; + } + + // 验证文档状态 + if (document.Status != DocumentStatus.Completed) + { + _logger.LogError("Document {DocumentId} is not in Completed status", documentId); + return false; + } + + // 读取文档内容 + string content; + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + + // 文本分块 + var chunks = SplitTextIntoChunks( + content, + parameters.ChunkSize, + parameters.ChunkOverlap, + parameters.Separator ?? string.Empty); + + // 启动文档向量化过程并设置总块数 +document.StartVectorization(chunks.Count); + await _unitOfWork.SaveChangesAsync(); + + // 处理每个块 + for (int i = 0; i < chunks.Count; i++) + { + var chunkContent = chunks[i]; + + // 生成嵌入向量 + var embedding = await GenerateEmbeddingAsync(chunkContent); + + // 存储向量 + await _vectorStore.StoreVectorAsync( + chunkContent, + embedding, + new + { + documentId = document.Id, + chunkId = i + 1, + position = i + 1 + }); + + // 更新进度 + document.UpdateVectorizationProgress(i + 1); + await _unitOfWork.SaveChangesAsync(); + } + + // 完成文档向量化 + document.CompleteVectorization(); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation("Successfully vectorized document {DocumentId}", documentId); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to vectorize document {DocumentId}", documentId); + return false; + } + } + + public async Task BatchVectorizeDocumentsAsync(IEnumerable documentIds, VectorizationParamsDto parameters) + { + try + { + // 初始化向量存储 + // 初始化向量存储(幂等操作) + await _vectorStore.InitializeAsync(); + + // 获取所有文档 + // 优化:使用IEnumerable避免不必要的数组转换 + var documentIdsInt = documentIds.Select(id => (int)id).ToList(); + + // 优化:使用FindAsync方法批量获取文档(利用仓储的批量查询能力) + // 直接使用.Contains避免多次单条查询 + var documents = (await _documentRepository.FindAsync(d => documentIds.Contains((long)d.Id))).ToList(); + + // 过滤掉null值(如果有) + documents = documents.Where(doc => doc != null).ToList(); + if (documents == null || documents.Count == 0) + { + _logger.LogError("No documents found for batch vectorization"); + return false; + } + + foreach (var document in documents) + { + // 验证文档状态 + if (document.Status != DocumentStatus.Completed) + { + _logger.LogError("Document {DocumentId} is not in Completed status", document.Id); + continue; + } + + try + { + // 读取文档内容 + string content; + using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + + // 文本分块 + var chunks = SplitTextIntoChunks( + content, + parameters.ChunkSize, + parameters.ChunkOverlap, + parameters.Separator ?? string.Empty); + +document.StartVectorization(chunks.Count); + await _unitOfWork.SaveChangesAsync(); + + // 处理每个块 + for (int i = 0; i < chunks.Count; i++) + { + var chunkContent = chunks[i]; + + // 生成嵌入向量 + var embedding = await GenerateEmbeddingAsync(chunkContent); + + // 存储向量 + await _vectorStore.StoreVectorAsync( + chunkContent, + embedding, + new + { + documentId = document.Id, + chunkId = i + 1, + position = i + 1 + }); + + // 更新进度 + document.UpdateVectorizationProgress(i + 1); + await _unitOfWork.SaveChangesAsync(); + } + + // 完成文档向量化 + document.CompleteVectorization(); + await _unitOfWork.SaveChangesAsync(); + + _logger.LogInformation("Successfully vectorized document {DocumentId}", document.Id); + } + catch (Exception ex) + { + document.FailVectorization(ex.Message); + await _unitOfWork.SaveChangesAsync(); + _logger.LogError(ex, "Failed to vectorize document {DocumentId}", document.Id); + } + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to batch vectorize documents"); + return false; + } + } + + public async Task GenerateEmbeddingAsync(string text) + { + try + { + // 实际应用中,这里应该调用第三方AI服务生成嵌入向量 + // 例如OpenAI的Embeddings API、Azure OpenAI或本地模型 + + // 为了演示,这里生成随机向量 + var random = new Random(); + var embedding = new float[1536]; // 假设使用1536维向量 + for (int i = 0; i < embedding.Length; i++) + { + embedding[i] = (float)(random.NextDouble() * 2 - 1); // 范围[-1, 1] + } + + await Task.CompletedTask; + return embedding; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to generate embedding"); + throw; + } + } + + // 文本分块辅助方法 + private List SplitTextIntoChunks(string text, int chunkSize, int chunkOverlap, string separator) + { + var chunks = new List(); + if (string.IsNullOrEmpty(text)) + return chunks; + + if (string.IsNullOrEmpty(separator)) + { + // 如果没有指定分隔符,使用字符数分块 + for (int i = 0; i < text.Length; i += chunkSize - chunkOverlap) + { + var end = Math.Min(i + chunkSize, text.Length); + chunks.Add(text.Substring(i, end - i)); + } + } + else + { + // 使用指定分隔符分块 + var paragraphs = text.Split(new[] { separator }, StringSplitOptions.None); + var currentChunk = new List(); + var currentSize = 0; + + foreach (var paragraph in paragraphs) + { + var paragraphSize = paragraph.Length; + + if (currentSize + paragraphSize > chunkSize && currentChunk.Count > 0) + { + chunks.Add(string.Join(separator, currentChunk)); + currentChunk = new List(); + currentSize = 0; + } + + currentChunk.Add(paragraph); + currentSize += paragraphSize + separator.Length; + } + + if (currentChunk.Count > 0) + { + chunks.Add(string.Join(separator, currentChunk)); + } + } + + return chunks; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/ChatHistory.cs b/backend/RAG.Domain/Entities/ChatHistory.cs index d6e2ac8..e291199 100644 --- a/backend/RAG.Domain/Entities/ChatHistory.cs +++ b/backend/RAG.Domain/Entities/ChatHistory.cs @@ -6,7 +6,10 @@ namespace RAG.Domain.Entities { public class ChatHistory : EntityBase { - private ChatHistory() { } + private ChatHistory() + { + SessionId = string.Empty; // 初始化 SessionId 以避免 null 值 + } public static ChatHistory Create(long userId, string sessionId) { diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs index cc27f21..349ac06 100644 --- a/backend/RAG.Domain/Entities/Document.cs +++ b/backend/RAG.Domain/Entities/Document.cs @@ -11,6 +11,13 @@ namespace RAG.Domain.Entities // 私有构造函数,确保通过工厂方法创建 private Document() { } + // 向量化相关属性 + public VectorizationStatus VectorizationStatus { get; private set; } = VectorizationStatus.NotVectorized; + public string? VectorizationErrorMessage { get; private set; } + public int VectorizedChunks { get; private set; } = 0; + public int TotalChunks { get; private set; } = 0; + public DateTime? LastVectorizedAt { get; private set; } + // 工厂方法 public static Document Create( string title, @@ -117,5 +124,55 @@ namespace RAG.Domain.Entities _chunks.Add(chunk); } + + // 向量化相关方法 + public void StartVectorization(int totalChunks) + { + if (totalChunks <= 0) + throw new ArgumentException("Total chunks must be greater than 0"); + if (VectorizationStatus == VectorizationStatus.Vectorizing) + throw new InvalidOperationException("Vectorization is already in progress"); + + VectorizationStatus = VectorizationStatus.Vectorizing; + TotalChunks = totalChunks; + VectorizedChunks = 0; + VectorizationErrorMessage = null; + LastVectorizedAt = DateTime.UtcNow; + } + + public void UpdateVectorizationProgress(int vectorizedChunks) + { + if (VectorizationStatus != VectorizationStatus.Vectorizing) + throw new InvalidOperationException("Vectorization is not in progress"); + if (vectorizedChunks < VectorizedChunks || vectorizedChunks > TotalChunks) + throw new ArgumentOutOfRangeException(nameof(vectorizedChunks), "Vectorized chunks must be between current progress and total chunks"); + + VectorizedChunks = vectorizedChunks; + LastVectorizedAt = DateTime.UtcNow; + + if (VectorizedChunks == TotalChunks) + { + CompleteVectorization(); + } + } + + public void CompleteVectorization() + { + if (VectorizationStatus != VectorizationStatus.Vectorizing) + throw new InvalidOperationException("Vectorization is not in progress"); + + VectorizationStatus = VectorizationStatus.Vectorized; + LastVectorizedAt = DateTime.UtcNow; + } + + public void FailVectorization(string errorMessage) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + throw new ArgumentException("Error message cannot be empty"); + + VectorizationStatus = VectorizationStatus.VectorizationFailed; + VectorizationErrorMessage = errorMessage; + LastVectorizedAt = DateTime.UtcNow; + } } } \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/KnowledgeBase.cs b/backend/RAG.Domain/Entities/KnowledgeBase.cs new file mode 100644 index 0000000..f30bca2 --- /dev/null +++ b/backend/RAG.Domain/Entities/KnowledgeBase.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System; + +namespace RAG.Domain.Entities +{ + public class KnowledgeBase : EntityBase + { + private KnowledgeBase() { } + + public static KnowledgeBase Create(string name, string description) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Knowledge base name cannot be empty"); + + return new KnowledgeBase + { + Id = new Random().NextInt64(1, long.MaxValue), + Name = name, + Description = description, + Documents = new List() + }; + } + + public string Name { get; private set; } + public string Description { get; private set; } + public ICollection Documents { get; private set; } + + public void UpdateName(string newName) + { + if (string.IsNullOrWhiteSpace(newName)) + throw new ArgumentException("Knowledge base name cannot be empty"); + + Name = newName; + UpdatedAt = DateTime.UtcNow; + } + + public void UpdateDescription(string newDescription) + { + Description = newDescription; + UpdatedAt = DateTime.UtcNow; + } + + public void AddDocument(Document document) + { + Documents.Add(document); + UpdatedAt = DateTime.UtcNow; + } + + public void RemoveDocument(Document document) + { + Documents.Remove(document); + UpdatedAt = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/VectorizationJob.cs b/backend/RAG.Domain/Entities/VectorizationJob.cs new file mode 100644 index 0000000..c0dc1f2 --- /dev/null +++ b/backend/RAG.Domain/Entities/VectorizationJob.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; + + +namespace RAG.Domain.Entities +{ + public class VectorizationJob : EntityBase + { + private VectorizationJob() { } + + public static VectorizationJob Create( + string name, + IEnumerable documentIds, + VectorizationParams parameters) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Job name cannot be empty"); + if (documentIds == null || !documentIds.GetEnumerator().MoveNext()) + throw new ArgumentException("At least one document ID must be provided"); + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + + return new VectorizationJob + { + Id = new Random().NextInt64(1, long.MaxValue), + Name = name, + DocumentIds = new List(documentIds), + Parameters = parameters, + Status = VectorizationJobStatus.Pending, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + } + + public new long Id { get; private set; } + public string Name { get; private set; } + public List DocumentIds { get; private set; } + public VectorizationParams Parameters { get; private set; } + public VectorizationJobStatus Status { get; private set; } + public int ProcessedCount { get; private set; } + public int TotalCount { get; private set; } + public string? ErrorMessage { get; private set; } + + public void StartProcessing() + { + if (Status != VectorizationJobStatus.Pending) + throw new InvalidOperationException("Job must be in pending status to start processing"); + + Status = VectorizationJobStatus.Processing; + TotalCount = DocumentIds.Count; + ProcessedCount = 0; + UpdatedAt = DateTime.UtcNow; + } + + public void UpdateProgress(int processedCount) + { + if (Status != VectorizationJobStatus.Processing) + throw new InvalidOperationException("Job must be in processing status to update progress"); + if (processedCount < ProcessedCount || processedCount > TotalCount) + throw new ArgumentOutOfRangeException(nameof(processedCount), "Processed count must be between current processed count and total count"); + + ProcessedCount = processedCount; + UpdatedAt = DateTime.UtcNow; + + if (ProcessedCount == TotalCount) + { + Complete(); + } + } + + public void Complete() + { + if (Status != VectorizationJobStatus.Processing) + throw new InvalidOperationException("Job must be in processing status to complete"); + + Status = VectorizationJobStatus.Completed; + UpdatedAt = DateTime.UtcNow; + } + + public void Fail(string errorMessage) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + throw new ArgumentException("Error message cannot be empty"); + + Status = VectorizationJobStatus.Failed; + ErrorMessage = errorMessage; + UpdatedAt = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/VectorizationJobStatus.cs b/backend/RAG.Domain/Entities/VectorizationJobStatus.cs new file mode 100644 index 0000000..51adafe --- /dev/null +++ b/backend/RAG.Domain/Entities/VectorizationJobStatus.cs @@ -0,0 +1,10 @@ +namespace RAG.Domain.Entities +{ + public enum VectorizationJobStatus + { + Pending, + Processing, + Completed, + Failed + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/VectorizationParams.cs b/backend/RAG.Domain/Entities/VectorizationParams.cs new file mode 100644 index 0000000..6867cef --- /dev/null +++ b/backend/RAG.Domain/Entities/VectorizationParams.cs @@ -0,0 +1,31 @@ +using System; + +namespace RAG.Domain.Entities +{ + public class VectorizationParams + { + public VectorizationParams( + string modelName, + int chunkSize = 1000, + int chunkOverlap = 200, + string? separator = null) + { + if (string.IsNullOrWhiteSpace(modelName)) + throw new ArgumentException("Model name cannot be empty"); + if (chunkSize <= 0) + throw new ArgumentException("Chunk size must be greater than 0"); + if (chunkOverlap < 0 || chunkOverlap >= chunkSize) + throw new ArgumentException("Chunk overlap must be between 0 and chunk size"); + + ModelName = modelName; + ChunkSize = chunkSize; + ChunkOverlap = chunkOverlap; + Separator = separator; + } + + public string ModelName { get; private set; } + public int ChunkSize { get; private set; } + public int ChunkOverlap { get; private set; } + public string? Separator { get; private set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/VectorizationStatus.cs b/backend/RAG.Domain/Entities/VectorizationStatus.cs new file mode 100644 index 0000000..bd87686 --- /dev/null +++ b/backend/RAG.Domain/Entities/VectorizationStatus.cs @@ -0,0 +1,10 @@ +namespace RAG.Domain.Entities +{ + public enum VectorizationStatus + { + NotVectorized, + Vectorizing, + Vectorized, + VectorizationFailed + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IKnowledgeBaseRepository.cs b/backend/RAG.Domain/Repositories/IKnowledgeBaseRepository.cs new file mode 100644 index 0000000..a0a645d --- /dev/null +++ b/backend/RAG.Domain/Repositories/IKnowledgeBaseRepository.cs @@ -0,0 +1,12 @@ +using RAG.Domain.Entities; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace RAG.Domain.Repositories +{ + public interface IKnowledgeBaseRepository : IRepository + { + Task> GetByNameAsync(string name); + Task CountByNameAsync(string name); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IRepository.cs b/backend/RAG.Domain/Repositories/IRepository.cs index 11e1774..306b90b 100644 --- a/backend/RAG.Domain/Repositories/IRepository.cs +++ b/backend/RAG.Domain/Repositories/IRepository.cs @@ -15,6 +15,6 @@ namespace RAG.Domain.Repositories Task AddRangeAsync(IEnumerable entities); void Remove(TEntity entity); void RemoveRange(IEnumerable entities); - Task CountAsync(Expression> predicate = null); + Task CountAsync(Expression>? predicate = null); } } \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IUnitOfWork.cs b/backend/RAG.Domain/Repositories/IUnitOfWork.cs index b655f93..a5e7739 100644 --- a/backend/RAG.Domain/Repositories/IUnitOfWork.cs +++ b/backend/RAG.Domain/Repositories/IUnitOfWork.cs @@ -13,5 +13,6 @@ namespace RAG.Domain.Repositories IDocumentRepository Documents { get; } IChatHistoryRepository ChatHistories { get; } IDocumentChunkRepository DocumentChunks { get; } + IKnowledgeBaseRepository KnowledgeBases { get; } } } \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IVectorStore.cs b/backend/RAG.Domain/Repositories/IVectorStore.cs index 3dfb6b4..9788378 100644 --- a/backend/RAG.Domain/Repositories/IVectorStore.cs +++ b/backend/RAG.Domain/Repositories/IVectorStore.cs @@ -6,15 +6,15 @@ namespace RAG.Domain.Repositories public interface IVectorStore { Task InitializeAsync(); - Task StoreVectorAsync(string content, float[] embedding, object metadata = null); + Task StoreVectorAsync(string content, float[] embedding, object? metadata = null); Task> SearchVectorsAsync(float[] queryVector, int topK = 5); } public class VectorResult { public int Id { get; set; } - public string Content { get; set; } - public Dictionary Metadata { get; set; } + public required string Content { get; set; } + public required Dictionary Metadata { get; set; } public double Similarity { get; set; } } } \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index d4b2a5b..4a57877 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -18,6 +18,8 @@ namespace RAG.Infrastructure.Data // 向量存储表 public DbSet VectorStores { get; set; } + // 向量化任务表 + public DbSet VectorizationJobs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -30,15 +32,16 @@ namespace RAG.Infrastructure.Data modelBuilder.Entity().Ignore(ch => ch.DomainEvents); modelBuilder.Entity().Ignore(cm => cm.DomainEvents); modelBuilder.Entity().Ignore(mr => mr.DomainEvents); + modelBuilder.Entity().Ignore(vj => vj.DomainEvents); // 配置User实体 modelBuilder.Entity(entity => { entity.HasKey(u => u.Id); - entity.Property(u => u.Username).IsRequired().HasMaxLength(50); - entity.Property(u => u.Email).IsRequired().HasMaxLength(100); - entity.Property(u => u.PasswordHash).IsRequired().HasMaxLength(255); - entity.Property(u => u.FullName).HasMaxLength(100); + entity.Property(u => u.Username).IsRequired().HasMaxLength(50).HasColumnType("text"); + entity.Property(u => u.Email).IsRequired().HasMaxLength(100).HasColumnType("text"); + entity.Property(u => u.PasswordHash).IsRequired().HasMaxLength(255).HasColumnType("text"); + entity.Property(u => u.FullName).HasMaxLength(100).HasColumnType("text"); entity.Property(u => u.IsActive).IsRequired(); // 索引 @@ -57,10 +60,16 @@ namespace RAG.Infrastructure.Data entity.Property(d => d.FileSize).IsRequired(); entity.Property(d => d.AccessLevel).IsRequired().HasMaxLength(20); entity.Property(d => d.Status).IsRequired(); + entity.Property(d => d.LastVectorizedAt); + entity.Property(d => d.VectorizationStatus).IsRequired(); + entity.Property(d => d.VectorizationErrorMessage).HasMaxLength(500); + entity.Property(d => d.VectorizedChunks).IsRequired(); + entity.Property(d => d.TotalChunks).IsRequired(); // 索引 entity.HasIndex(d => d.Status); entity.HasIndex(d => d.AccessLevel); + entity.HasIndex(d => d.VectorizationStatus); }); // 配置DocumentChunk实体 @@ -140,6 +149,32 @@ namespace RAG.Infrastructure.Data .OnDelete(DeleteBehavior.Cascade); // 索引 + + // 配置VectorizationJob实体 + modelBuilder.Entity(entity => + { + entity.HasKey(vj => vj.Id); + entity.Property(vj => vj.Name).IsRequired().HasMaxLength(200); + entity.Property(vj => vj.Status).IsRequired(); + entity.Property(vj => vj.ProcessedCount).IsRequired(); + entity.Property(vj => vj.TotalCount).IsRequired(); + entity.Property(vj => vj.ErrorMessage).HasMaxLength(500); + entity.Property(vj => vj.CreatedAt).IsRequired(); + entity.Property(vj => vj.UpdatedAt).IsRequired(); + + // 复杂类型配置 + entity.OwnsOne(vj => vj.Parameters, paramsBuilder => + { + paramsBuilder.Property(p => p.ModelName).IsRequired().HasMaxLength(50); + paramsBuilder.Property(p => p.ChunkSize).IsRequired(); + paramsBuilder.Property(p => p.ChunkOverlap).IsRequired(); + paramsBuilder.Property(p => p.Separator).HasMaxLength(10); + }); + + // 索引 + entity.HasIndex(vj => vj.Status); + entity.HasIndex(vj => vj.CreatedAt); + }); entity.HasIndex(mr => new { mr.ChatMessageId, mr.DocumentChunkId }).IsUnique(); }); diff --git a/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs b/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs index 8eb15dc..c945f5f 100644 --- a/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs +++ b/backend/RAG.Infrastructure/FileStorage/LocalFileStorageService.cs @@ -56,9 +56,9 @@ namespace RAG.Infrastructure.FileStorage return true; } - public async Task FileExistsAsync(string filePath) + public Task FileExistsAsync(string filePath) { - return File.Exists(filePath); + return Task.FromResult(File.Exists(filePath)); } } } \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs similarity index 94% rename from backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs rename to backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs index e6132e5..6294fae 100644 --- a/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.Designer.cs +++ b/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs @@ -12,8 +12,8 @@ using RAG.Infrastructure.Data; namespace RAG.Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250805073332_InitialCreate")] - partial class InitialCreate + [Migration("20250806074151_AddVectorizationFieldsToDocument")] + partial class AddVectorizationFieldsToDocument { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -137,6 +137,9 @@ namespace RAG.Infrastructure.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + b.Property("Status") .HasColumnType("integer"); @@ -145,12 +148,25 @@ namespace RAG.Infrastructure.Migrations .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("TotalChunks") + .HasColumnType("integer"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("bigint"); + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("AccessLevel"); @@ -159,6 +175,8 @@ namespace RAG.Infrastructure.Migrations b.HasIndex("UserId"); + b.HasIndex("VectorizationStatus"); + b.ToTable("Documents"); }); diff --git a/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs b/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs similarity index 95% rename from backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs rename to backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs index 277e645..ef87a80 100644 --- a/backend/RAG.Infrastructure/Migrations/20250805073332_InitialCreate.cs +++ b/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs @@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace RAG.Infrastructure.Migrations { /// - public partial class InitialCreate : Migration + public partial class AddVectorizationFieldsToDocument : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -66,6 +66,11 @@ namespace RAG.Infrastructure.Migrations { Id = table.Column(type: "bigint", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + VectorizationStatus = table.Column(type: "integer", nullable: false), + VectorizationErrorMessage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + VectorizedChunks = table.Column(type: "integer", nullable: false), + TotalChunks = table.Column(type: "integer", nullable: false), + LastVectorizedAt = table.Column(type: "timestamp with time zone", nullable: true), Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), FileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), FilePath = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), @@ -256,6 +261,11 @@ namespace RAG.Infrastructure.Migrations table: "Documents", column: "UserId"); + migrationBuilder.CreateIndex( + name: "IX_Documents_VectorizationStatus", + table: "Documents", + column: "VectorizationStatus"); + migrationBuilder.CreateIndex( name: "IX_MessageReferences_ChatMessageId_DocumentChunkId", table: "MessageReferences", diff --git a/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs new file mode 100644 index 0000000..96e4876 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs @@ -0,0 +1,445 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250806075252_UpdateUserTableStringTypes")] + partial class UpdateUserTableStringTypes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs b/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs new file mode 100644 index 0000000..1104e38 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UpdateUserTableStringTypes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "text", + maxLength: 50, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "Users", + type: "text", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(255)", + oldMaxLength: 255); + + migrationBuilder.AlterColumn( + name: "FullName", + table: "Users", + type: "text", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Users", + type: "text", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "character varying(50)", + maxLength: 50, + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldMaxLength: 50); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "Users", + type: "character varying(255)", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldMaxLength: 255); + + migrationBuilder.AlterColumn( + name: "FullName", + table: "Users", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldMaxLength: 100); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Users", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text", + oldMaxLength: 100); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs new file mode 100644 index 0000000..4b858c2 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs @@ -0,0 +1,525 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250806091942_AddVectorizationJobEntity")] + partial class AddVectorizationJobEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs b/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs new file mode 100644 index 0000000..0358633 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class AddVectorizationJobEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "VectorizationJobs", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DocumentIds = table.Column>(type: "bigint[]", nullable: false), + Parameters_ModelName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Parameters_ChunkSize = table.Column(type: "integer", nullable: false), + Parameters_ChunkOverlap = table.Column(type: "integer", nullable: false), + Parameters_Separator = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), + Status = table.Column(type: "integer", nullable: false), + ProcessedCount = table.Column(type: "integer", nullable: false), + TotalCount = table.Column(type: "integer", nullable: false), + ErrorMessage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_VectorizationJobs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_VectorizationJobs_CreatedAt", + table: "VectorizationJobs", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_VectorizationJobs_Status", + table: "VectorizationJobs", + column: "Status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "VectorizationJobs"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 514b6d6..ed77539 100644 --- a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -134,6 +135,9 @@ namespace RAG.Infrastructure.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + b.Property("Status") .HasColumnType("integer"); @@ -142,12 +146,25 @@ namespace RAG.Infrastructure.Migrations .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("TotalChunks") + .HasColumnType("integer"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("bigint"); + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("AccessLevel"); @@ -156,6 +173,8 @@ namespace RAG.Infrastructure.Migrations b.HasIndex("UserId"); + b.HasIndex("VectorizationStatus"); + b.ToTable("Documents"); }); @@ -250,12 +269,12 @@ namespace RAG.Infrastructure.Migrations b.Property("Email") .IsRequired() .HasMaxLength(100) - .HasColumnType("character varying(100)"); + .HasColumnType("text"); b.Property("FullName") .IsRequired() .HasMaxLength(100) - .HasColumnType("character varying(100)"); + .HasColumnType("text"); b.Property("IsActive") .HasColumnType("boolean"); @@ -263,7 +282,7 @@ namespace RAG.Infrastructure.Migrations b.Property("PasswordHash") .IsRequired() .HasMaxLength(255) - .HasColumnType("character varying(255)"); + .HasColumnType("text"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -271,7 +290,7 @@ namespace RAG.Infrastructure.Migrations b.Property("Username") .IsRequired() .HasMaxLength(50) - .HasColumnType("character varying(50)"); + .HasColumnType("text"); b.HasKey("Id"); @@ -284,6 +303,51 @@ namespace RAG.Infrastructure.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => { b.Property("Id") @@ -388,6 +452,40 @@ namespace RAG.Infrastructure.Migrations b.Navigation("DocumentChunk"); }); + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => { b.HasOne("RAG.Domain.Entities.DocumentChunk", null) diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index b489d8b..42bed28 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -5,6 +5,7 @@
+ diff --git a/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs index e69de29..5249030 100644 --- a/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs +++ b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs @@ -0,0 +1,94 @@ +using System;using System.Threading.Tasks;using Microsoft.EntityFrameworkCore;using RAG.Domain.Repositories;using RAG.Infrastructure.Data; + +namespace RAG.Infrastructure.Repositories +{ + public class ApplicationUnitOfWork : IUnitOfWork + { + private readonly ApplicationDbContext _context; + private readonly IUserRepository _users; + private IDocumentRepository _documents; + private readonly IChatHistoryRepository _chatHistories; + private IDocumentChunkRepository _documentChunks; + private IKnowledgeBaseRepository _knowledgeBases; + + public ApplicationUnitOfWork(ApplicationDbContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public IRepository GetRepository() where TEntity : class + { + return new Repository(_context); + } + + public async Task SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + + public IUserRepository Users + { + get + { + if (_users == null) + { + // 这里需要替换为实际的UserRepository实现 + throw new NotImplementedException("UserRepository is not implemented yet."); + } + return _users; + } + } + + public IDocumentRepository Documents + { + get + { + if (_documents == null) + { +// 由于 _documents 是只读字段,需要在类中动态创建一个可写的属性或方法来处理 +// 此处暂时创建一个新的 DocumentRepository 实例返回 +return new DocumentRepository(_context); + } + return _documents; + } + } + + public IChatHistoryRepository ChatHistories + { + get + { + if (_chatHistories == null) + { + // 这里需要替换为实际的ChatHistoryRepository实现 + throw new NotImplementedException("ChatHistoryRepository is not implemented yet."); + } + return _chatHistories; + } + } + + public IDocumentChunkRepository DocumentChunks + { + get + { + if (_documentChunks == null) + { +// 原代码报错是因为 _documentChunks 被声明为只读字段,移除 readonly 修饰符后可直接赋值 + _documentChunks = new DocumentChunkRepository(_context); + } + return _documentChunks; + } + } + + public IKnowledgeBaseRepository KnowledgeBases + { + get + { + if (_knowledgeBases == null) + { + _knowledgeBases = new KnowledgeBaseRepository(_context); + } + return _knowledgeBases; + } + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/DocumentChunkRepository.cs b/backend/RAG.Infrastructure/Repositories/DocumentChunkRepository.cs new file mode 100644 index 0000000..a5dde37 --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/DocumentChunkRepository.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RAG.Infrastructure.Repositories +{ + public class DocumentChunkRepository : Repository, IDocumentChunkRepository + { + public DocumentChunkRepository(ApplicationDbContext context) : base(context) + { + } + + public async Task> GetByDocumentIdAsync(long documentId) + { + return await _dbSet + .Where(dc => dc.DocumentId == documentId) + .ToListAsync(); + } + + public async Task CountByDocumentIdAsync(long documentId) + { + return await _dbSet + .CountAsync(dc => dc.DocumentId == documentId); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/DocumentRepository.cs b/backend/RAG.Infrastructure/Repositories/DocumentRepository.cs new file mode 100644 index 0000000..1f719f3 --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/DocumentRepository.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RAG.Infrastructure.Repositories +{ + public class DocumentRepository : Repository, IDocumentRepository + { + public DocumentRepository(ApplicationDbContext context) : base(context) + { + } + + public async Task> GetByStatusAsync(DocumentStatus status) + { + return await _dbSet + .Where(d => d.Status == status) + .ToListAsync(); + } + + public async Task> GetByAccessLevelAsync(string accessLevel) + { + return await _dbSet + .Where(d => d.AccessLevel == accessLevel) + .ToListAsync(); + } + + public async Task CountByStatusAsync(DocumentStatus status) + { + return await _dbSet + .CountAsync(d => d.Status == status); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/KnowledgeBaseRepository.cs b/backend/RAG.Infrastructure/Repositories/KnowledgeBaseRepository.cs new file mode 100644 index 0000000..7f392cf --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/KnowledgeBaseRepository.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RAG.Infrastructure.Repositories +{ + public class KnowledgeBaseRepository : Repository, IKnowledgeBaseRepository + { + public KnowledgeBaseRepository(ApplicationDbContext context) : base(context) + { + } + + public async Task> GetByNameAsync(string name) + { + return await _dbSet + .Where(kb => kb.Name.Contains(name)) + .ToListAsync(); + } + + public async Task CountByNameAsync(string name) + { + return await _dbSet + .CountAsync(kb => kb.Name.Contains(name)); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/Repository.cs b/backend/RAG.Infrastructure/Repositories/Repository.cs index e69de29..00a4b14 100644 --- a/backend/RAG.Infrastructure/Repositories/Repository.cs +++ b/backend/RAG.Infrastructure/Repositories/Repository.cs @@ -0,0 +1,76 @@ +using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using Microsoft.EntityFrameworkCore;using RAG.Domain.Repositories;using RAG.Infrastructure.Data; + +namespace RAG.Infrastructure.Repositories +{ +// 在当前上下文中,`TEntity` 是泛型类型参数的名称。`T` 通常也是泛型类型参数的常用命名, +// 二者本质上没有区别,都是用于表示泛型类型参数。不同之处仅在于命名,`TEntity` 更具描述性, +// 从名称上能推测出它代表实体类型,而 `T` 是一个更通用、简洁的命名。 + public class Repository : IRepository where TEntity : class + { + protected readonly ApplicationDbContext _context; + protected readonly DbSet _dbSet; + + public Repository(ApplicationDbContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _dbSet = _context.Set(); + } + + public async Task GetByIdAsync(int id) + { + return await _dbSet.FindAsync(id); + } + + public async Task> GetAllAsync() + { + return await _dbSet.ToListAsync(); + } + + public async Task> FindAsync(System.Linq.Expressions.Expression> predicate) + { + return await _dbSet.Where(predicate).ToListAsync(); + } + + public async Task SingleOrDefaultAsync(System.Linq.Expressions.Expression> predicate) + { + return await _dbSet.SingleOrDefaultAsync(predicate); + } + + public async Task AddAsync(TEntity entity) + { + await _dbSet.AddAsync(entity); + } + + public async Task AddRangeAsync(IEnumerable entities) + { + await _dbSet.AddRangeAsync(entities); + } + + public void Update(TEntity entity) + { + _dbSet.Update(entity); + } + + public void Remove(TEntity entity) + { + _dbSet.Remove(entity); + } + + public void RemoveRange(IEnumerable entities) + { + _dbSet.RemoveRange(entities); + } + + public async Task CountAsync(System.Linq.Expressions.Expression> predicate = null) + { + if (predicate == null) + return await _dbSet.CountAsync(); + return await _dbSet.CountAsync(predicate); + } + + public async Task SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs index e69de29..a6b7015 100644 --- a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs +++ b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using RAG.Domain.Repositories; +using Newtonsoft.Json; + + +namespace RAG.Infrastructure.VectorStore +{ + public class PgVectorStore : IVectorStore + { + // 内存中的向量存储(实际应用中应替换为PostgreSQL连接) + private readonly List _vectorStore = new List(); + private bool _initialized = false; + + public async Task InitializeAsync() + { + // 模拟初始化向量存储 + // 实际应用中,这里应该初始化数据库连接和创建必要的表 + await Task.Run(() => + { + _initialized = true; + Console.WriteLine("Vector store initialized successfully"); + }); + } + + public async Task StoreVectorAsync(string content, float[] embedding, object metadata = null) + { + if (!_initialized) + { + throw new InvalidOperationException("Vector store not initialized"); + } + + // 模拟存储向量 + // 实际应用中,这里应该将向量插入到PostgreSQL数据库 + await Task.Run(() => + { + var entry = new VectorEntry + { + Id = _vectorStore.Count + 1, + Content = content, + Embedding = embedding, + Metadata = metadata != null ? JsonConvert.SerializeObject(metadata) : null, + CreatedAt = DateTime.UtcNow + }; + + _vectorStore.Add(entry); + Console.WriteLine($"Stored vector with id: {entry.Id}"); + }); + } + + public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5) + { + if (!_initialized) + { + throw new InvalidOperationException("Vector store not initialized"); + } + + // 模拟向量搜索 + // 实际应用中,这里应该使用PostgreSQL的向量扩展进行相似度搜索 + return await Task.Run(() => + { + var results = _vectorStore + .Select(entry => new + { + Entry = entry, + Similarity = CalculateCosineSimilarity(queryVector, entry.Embedding) + }) + .OrderByDescending(x => x.Similarity) + .Take(topK) + .Select(x => new VectorResult + { + Id = x.Entry.Id, + Content = x.Entry.Content, + Metadata = x.Entry.Metadata != null ? + JsonConvert.DeserializeObject>(x.Entry.Metadata) : + new Dictionary(), + Similarity = x.Similarity + }) + .ToList(); + + Console.WriteLine($"Found {results.Count} vector results"); + return results; + }); + } + + // 计算余弦相似度 + private double CalculateCosineSimilarity(float[] vector1, float[] vector2) + { + if (vector1.Length != vector2.Length) + { + throw new ArgumentException("Vectors must have the same length"); + } + + double dotProduct = 0; + double magnitude1 = 0; + double magnitude2 = 0; + + for (int i = 0; i < vector1.Length; i++) + { + dotProduct += vector1[i] * vector2[i]; + magnitude1 += vector1[i] * vector1[i]; + magnitude2 += vector2[i] * vector2[i]; + } + + if (magnitude1 == 0 || magnitude2 == 0) + { + return 0; + } + + return dotProduct / (Math.Sqrt(magnitude1) * Math.Sqrt(magnitude2)); + } + + // 内部类:向量条目 + private class VectorEntry + { + public int Id { get; set; } + public string Content { get; set; } + public float[] Embedding { get; set; } + public string Metadata { get; set; } + public DateTime CreatedAt { get; set; } + } + } +} \ No newline at end of file diff --git a/test-upload-fix.ps1 b/test-upload-fix.ps1 new file mode 100644 index 0000000..8560685 --- /dev/null +++ b/test-upload-fix.ps1 @@ -0,0 +1,76 @@ +# 测试文档上传修复脚本 +# 此脚本用于验证文档状态更新修复是否有效 + +# 设置API URL +$apiUrl = "http://localhost:5000/api/document/upload-multiple" + +# 准备测试文件 +$testFiles = @( + @{ Path = "./test-file1.txt"; Content = "这是测试文件1的内容" }, + @{ Path = "./test-file2.txt"; Content = "这是测试文件2的内容" } +) + +# 创建测试文件 +foreach ($file in $testFiles) { + Set-Content -Path $file.Path -Value $file.Content +} + +# 上传文件 +try { + # 创建临时文件夹用于构建multipart/form-data + $tempDir = New-Item -ItemType Directory -Path (Join-Path $PSScriptRoot "temp") -Force + $boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" + $formData = @" +--$boundary +Content-Disposition: form-data; name="titlePrefix" + +TestDocument +--$boundary +Content-Disposition: form-data; name="accessLevel" + +internal +"@ + + # 添加文件 + foreach ($file in $testFiles) { + $fileName = Split-Path $file.Path -Leaf + $fileContent = Get-Content -Path $file.Path -Raw + $fileContentBytes = [System.Text.Encoding]::UTF8.GetBytes($fileContent) + $fileContentBase64 = [Convert]::ToBase64String($fileContentBytes) + + $formData += "--$boundary +Content-Disposition: form-data; name="files"; filename="$fileName" +Content-Type: text/plain + +$fileContent +" + } + + # 结束边界 + $formData += "--$boundary--" + + # 发送请求 + $response = Invoke-RestMethod -Uri $apiUrl -Method Post ` + -Headers @{"Content-Type" = "multipart/form-data; boundary=$boundary"} ` + -Body $formData + + Write-Host "上传成功,响应:" + $response | ConvertTo-Json + + # 验证数据库中的文档状态 + Write-Host " +请检查数据库中的文档状态,确认状态是否为Completed,并且VectorizedChunks是否大于0" +} catch { + Write-Host "上传失败: $_" +} finally { + # 清理测试文件 + foreach ($file in $testFiles) { + if (Test-Path $file.Path) { + Remove-Item $file.Path + } + } + # 清理临时文件夹 + if (Test-Path (Join-Path $PSScriptRoot "temp")) { + Remove-Item -Path (Join-Path $PSScriptRoot "temp") -Recurse -Force + } +} \ No newline at end of file -- Gitee From ad35f8cfe4b9a14c389007a78de65f077243b075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 17:35:46 +0800 Subject: [PATCH 14/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86,=E6=89=B9=E9=87=8F=E5=90=91=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E6=96=87=E6=A1=A3,=E8=8E=B7=E5=8F=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=91=E9=87=8F=E5=8C=96=E7=8A=B6=E6=80=81,=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=90=91=E9=87=8F=E5=8C=96=E4=BB=BB=E5=8A=A1=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- backend/RAG.Application/Services/DocumentService.cs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 5f69673..f2c107d 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -156,7 +156,7 @@ Content-Type: application/json ### #### 4. 获取文档向量化状态 -GET {{url}}/api/knowledgebase/documents/1/vectorization-status +GET {{url}}/api/knowledgebase/documents/172579765607330619/vectorization-status Accept: application/json ### diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs index 7dedf98..d392778 100644 --- a/backend/RAG.Application/Services/DocumentService.cs +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -518,14 +518,16 @@ var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) throw new KeyNotFoundException($"Documents not found: {string.Join(", ", invalidDocuments)}"); } - // 验证文档状态 - var invalidStatusDocuments = documents + // 修改:允许所有状态的文档进行向量化 + // 仅记录非Completed状态的文档但不阻止向量化 + var nonCompletedDocuments = documents .Where(d => d.Status != DocumentStatus.Completed) .ToList(); - if (invalidStatusDocuments.Any()) + if (nonCompletedDocuments.Any()) { - throw new InvalidOperationException($"Documents not in Completed status: {string.Join(", ", invalidStatusDocuments.Select(d => d.Id))}"); + // 记录警告但不抛出异常 + Console.WriteLine($"Warning: Documents not in Completed status but will be vectorized: {string.Join(", ", nonCompletedDocuments.Select(d => d.Id))}"); } // 检查是否已向量化或正在向量化 -- Gitee From a4ad3dbaf64ad50614738cfc5e94570fe0f7ce9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 17:36:53 +0800 Subject: [PATCH 15/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86,=E6=89=B9=E9=87=8F=E5=90=91=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E6=96=87=E6=A1=A3,=E8=8E=B7=E5=8F=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=91=E9=87=8F=E5=8C=96=E7=8A=B6=E6=80=81,=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=90=91=E9=87=8F=E5=8C=96=E4=BB=BB=E5=8A=A1=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index f2c107d..0926c06 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -172,7 +172,7 @@ GET {{url}}/api/knowledgebase/vectorization-jobs/1 Accept: application/json ### - + #### 原始天气 API 测试 (保留) GET {{url}}/weatherforecast/ Accept: application/json -- Gitee From 9ecc90bb2cf42ff021398138010fa28c8bbfa8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 17:37:26 +0800 Subject: [PATCH 16/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86,=E6=89=B9=E9=87=8F=E5=90=91=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E6=96=87=E6=A1=A3,=E8=8E=B7=E5=8F=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=91=E9=87=8F=E5=8C=96=E7=8A=B6=E6=80=81,=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=90=91=E9=87=8F=E5=8C=96=E4=BB=BB=E5=8A=A1=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 0926c06..33aa891 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -170,7 +170,7 @@ Accept: application/json #### 6. 获取向量化任务详情 GET {{url}}/api/knowledgebase/vectorization-jobs/1 Accept: application/json - + ### #### 原始天气 API 测试 (保留) -- Gitee From edb06150280df3be85a6053d7f1d8529f051c62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 17:38:24 +0800 Subject: [PATCH 17/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86,=E6=89=B9=E9=87=8F=E5=90=91=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E6=96=87=E6=A1=A3,=E8=8E=B7=E5=8F=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=91=E9=87=8F=E5=8C=96=E7=8A=B6=E6=80=81,=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=90=91=E9=87=8F=E5=8C=96=E4=BB=BB=E5=8A=A1=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=AE=8C=E6=88=90=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 33aa891..4dddbeb 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -170,7 +170,6 @@ Accept: application/json #### 6. 获取向量化任务详情 GET {{url}}/api/knowledgebase/vectorization-jobs/1 Accept: application/json - ### #### 原始天气 API 测试 (保留) -- Gitee From 07b596eead2a9e15065f2d45ff47a0c721ed7cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 6 Aug 2025 17:42:57 +0800 Subject: [PATCH 18/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=B5=8B=E8=AF=95=E6=97=A0=E8=AF=AF=EF=BC=8C=E8=BF=98?= =?UTF-8?q?=E6=9C=89=E7=82=B9=E5=B0=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- backend/RAG.Domain/Repositories/IRepository.cs | 2 +- backend/RAG.Infrastructure/Repositories/Repository.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 4dddbeb..f15e199 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -168,7 +168,7 @@ Accept: application/json ### #### 6. 获取向量化任务详情 -GET {{url}}/api/knowledgebase/vectorization-jobs/1 +GET {{url}}/api/knowledgebase/vectorization-jobs/172579765607330619 Accept: application/json ### diff --git a/backend/RAG.Domain/Repositories/IRepository.cs b/backend/RAG.Domain/Repositories/IRepository.cs index 306b90b..58d4acf 100644 --- a/backend/RAG.Domain/Repositories/IRepository.cs +++ b/backend/RAG.Domain/Repositories/IRepository.cs @@ -7,7 +7,7 @@ namespace RAG.Domain.Repositories { public interface IRepository where TEntity : class { - Task GetByIdAsync(int id); + Task GetByIdAsync(long id); Task> GetAllAsync(); Task> FindAsync(Expression> predicate); Task SingleOrDefaultAsync(Expression> predicate); diff --git a/backend/RAG.Infrastructure/Repositories/Repository.cs b/backend/RAG.Infrastructure/Repositories/Repository.cs index 00a4b14..e96dede 100644 --- a/backend/RAG.Infrastructure/Repositories/Repository.cs +++ b/backend/RAG.Infrastructure/Repositories/Repository.cs @@ -16,7 +16,7 @@ namespace RAG.Infrastructure.Repositories _dbSet = _context.Set(); } - public async Task GetByIdAsync(int id) + public async Task GetByIdAsync(long id) { return await _dbSet.FindAsync(id); } -- Gitee From 511cf9689bb3c1914ce1ce050b6881a71eaf6b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Thu, 7 Aug 2025 08:11:33 +0800 Subject: [PATCH 19/31] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E8=8E=B7=E5=8F=96=E5=90=91=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85=E5=AE=8C=E6=88=90=E4=B8=94?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=97=A0=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- backend/RAG.Application/Services/DocumentService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index f15e199..35d1e38 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -168,7 +168,7 @@ Accept: application/json ### #### 6. 获取向量化任务详情 -GET {{url}}/api/knowledgebase/vectorization-jobs/172579765607330619 +GET {{url}}/api/knowledgebase/vectorization-jobs/3196309357888426600 Accept: application/json ### diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs index d392778..8537ece 100644 --- a/backend/RAG.Application/Services/DocumentService.cs +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -726,7 +726,7 @@ var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) public async Task GetVectorizationJobAsync(long jobId) { - var job = await _vectorizationJobRepository.GetByIdAsync((int)jobId); + var job = await _vectorizationJobRepository.GetByIdAsync(jobId); if (job == null) { throw new KeyNotFoundException($"Vectorization job with id {jobId} not found"); -- Gitee From f2cb866e8d924d80740d771d944642527a2d27a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Thu, 7 Aug 2025 09:08:41 +0800 Subject: [PATCH 20/31] =?UTF-8?q?=E5=B9=B6=E5=85=A5=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=EF=BC=8C=E7=99=BB=E5=BD=95=EF=BC=8C=E6=9F=A5=E8=AF=A2=E5=8D=95?= =?UTF-8?q?=E4=B8=AA=E5=92=8C=E5=85=A8=E9=83=A8=E7=94=A8=E6=88=B7=EF=BC=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/Controllers/AuthController.cs | 118 ++++ backend/RAG.Api/Program.cs | 6 + backend/RAG.Api/RAG.Api.http | 25 +- backend/RAG.Api/appsettings.json | 4 + backend/RAG.Application/Common/ApiResult.cs | 21 + backend/RAG.Application/DTOs/UserDto.cs | 3 + .../ServiceCollectionExtensions.cs | 22 + .../RAG.Application/Services/AuthService.cs | 105 ++++ backend/RAG.Domain/Entities/User.cs | 52 +- .../RAG.Domain/Repositories/IERepository.cs | 22 + .../Data/ApplicationDbContext.cs | 9 +- .../20250807001255_auth2.Designer.cs | 531 ++++++++++++++++++ .../Migrations/20250807001255_auth2.cs | 113 ++++ .../ApplicationDbContextModelSnapshot.cs | 22 +- .../RAG.Infrastructure.csproj | 3 + .../Repositories/EReposiotory.cs | 119 ++++ .../Security/PasswordHasher.cs | 31 + .../ServiceCollectionExtensions.cs | 32 ++ .../Token/JwtTokenGererator.cs | 49 ++ 19 files changed, 1249 insertions(+), 38 deletions(-) create mode 100644 backend/RAG.Api/Controllers/AuthController.cs create mode 100644 backend/RAG.Application/Common/ApiResult.cs create mode 100644 backend/RAG.Application/DTOs/UserDto.cs create mode 100644 backend/RAG.Application/ServiceCollectionExtensions.cs create mode 100644 backend/RAG.Application/Services/AuthService.cs create mode 100644 backend/RAG.Domain/Repositories/IERepository.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs create mode 100644 backend/RAG.Infrastructure/Repositories/EReposiotory.cs create mode 100644 backend/RAG.Infrastructure/Security/PasswordHasher.cs create mode 100644 backend/RAG.Infrastructure/ServiceCollectionExtensions.cs create mode 100644 backend/RAG.Infrastructure/Token/JwtTokenGererator.cs diff --git a/backend/RAG.Api/Controllers/AuthController.cs b/backend/RAG.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..d79f6ba --- /dev/null +++ b/backend/RAG.Api/Controllers/AuthController.cs @@ -0,0 +1,118 @@ + +using Admin.application.Dtos; +using Microsoft.AspNetCore.Mvc; +using RAG.Application.Services; + +namespace RAG.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + + private readonly AuthService _authService; + + public AuthController(AuthService authService) + { + _authService = authService; + } + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest req) + { + try + { + var user = await _authService.RegisterAsync(req.Username, req.Password,req.Email,req.Telephone); + + + return Ok(new { user.UserName, user, msg = "注册成功" }); + } + catch (Exception ex) + { + + return BadRequest(new { ex.Message }); + } + } + + + + [HttpPost("login")] + public async Task Login([FromBody] LoginRequests request, [FromServices] IConfiguration configuration) + { + var jwtSection = configuration.GetSection("Jwt"); + var secret = jwtSection["Secret"] ?? ""; + var expire = int.TryParse(jwtSection["ExpireMinutes"], out var e) ? e : 120; + + var user = await _authService.LoginAsync(request.UserName, request.Password,secret,expire); + if (user == null || request.UserName==""||request.Password=="") + { + + return Unauthorized(new { message = "用户名或 密码错误" }); + } + else + { + return Ok(new { msg = "登录成功", data = user }); + } + } + [HttpGet("User")] + public async Task GetAll() + + { + + var res = await _authService.GeUserAsync(); + + return Ok(new { users = res }); + + } + [HttpGet("user/{id}")] + public async Task GetById(long id) + { + var user = await _authService.GetUserById(id); + if (user == null) return NotFound(); + return Ok(new UserDto (user.Id, user.UserName,user.Email,user.Telephone)); + } + + + + + + + + + + + + // [HttpDelete("{id}")] + // public async Task DeleteId(DeleteIdRes res) + // { + // try + // { + // var Id = await _authService.DeleteAsync(res.id); + + // return; + // } + // catch (Exception) + // { + + + // } + // } + public class RegisterRequest + { + + public required string Username { get; set; } + public required string Password { get; set; } + public required string Email { get; set; } + public required string Telephone { get; set; } + } + + public class LoginRequests + { + public required string UserName { get; set; } + public required string Password { get; set; } + + } + + + + +} diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs index 5edbcb0..8e25ca3 100644 --- a/backend/RAG.Api/Program.cs +++ b/backend/RAG.Api/Program.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore; using MediatR; using RAG.Infrastructure.Data; using System.Reflection; +using RAG.Infrastructure; +using RAG.Application; var builder = WebApplication.CreateBuilder(args); @@ -68,9 +70,13 @@ builder.Services.AddCors(options => .AllowAnyHeader(); }); }); +builder.Services.AddAppAddication(); +// builder.Services.AddScoped(); +builder.Services.AddINfrastructure(builder.Configuration); var app = builder.Build(); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index f15e199..b6df197 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -176,4 +176,27 @@ Accept: application/json GET {{url}}/weatherforecast/ Accept: application/json -### +### 注册 +post {{url}}/api/auth/register +Content-Type: application/json + +{ + "username":"liujiahong", + "password":"111112222", + "email": "daiting@nono.com", + "telephone": "18329050889" +} + +### 登录,登录成功 +post {{url}}/api/auth/login +Content-Type: application/json + +{ + "username":"123456789", + "password":"123456789" +} +### 查询所有用户 +GET {{url}}/api/auth/user +### 查询单个用户 +GET {{url}}/api/auth/user/1 + diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json index e496afb..ea8d7a0 100644 --- a/backend/RAG.Api/appsettings.json +++ b/backend/RAG.Api/appsettings.json @@ -12,5 +12,9 @@ "VectorStore": { "Type": "Postgres", "ConnectionString": "Host=www.mingyuy.cn;Port=5432;Database=rag_db;Username=postgres;Password=mingyuy." + }, + "jwt":{ + "Secret":"YourSuperSecretKeyForAdminToken2025!", + "ExpireMintes":120 } } diff --git a/backend/RAG.Application/Common/ApiResult.cs b/backend/RAG.Application/Common/ApiResult.cs new file mode 100644 index 0000000..0af6ec3 --- /dev/null +++ b/backend/RAG.Application/Common/ApiResult.cs @@ -0,0 +1,21 @@ +namespace RaG.Application.Common; + +public class ApiResult +{ + public int Code { get; set; } + public string Msg { get; set; } = null!; + public object? Data { get; set; } = null; + + + public static ApiResult Success(object data, string msg = "操作成功") + { + var res = new ApiResult{ Code = 1000, Msg = msg, Data = data }; + return res; + } + public static ApiResult Fail(int code = 10010, string msg = "操作失败") + { + var res = new ApiResult { Code = code, Msg = msg, Data = null }; + return res; + } + +} \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/UserDto.cs b/backend/RAG.Application/DTOs/UserDto.cs new file mode 100644 index 0000000..7b8e98f --- /dev/null +++ b/backend/RAG.Application/DTOs/UserDto.cs @@ -0,0 +1,3 @@ +namespace Admin.application.Dtos; + +public record UserDto(long Id,string UserName, string Email, string Telephone); \ No newline at end of file diff --git a/backend/RAG.Application/ServiceCollectionExtensions.cs b/backend/RAG.Application/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..58b699d --- /dev/null +++ b/backend/RAG.Application/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ + + +using Microsoft.Extensions.DependencyInjection; +using RAG.Application.Services; + +namespace RAG.Application; + + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAppAddication(this IServiceCollection services) + { + + services.AddScoped(); + + + + + + return services; + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/AuthService.cs b/backend/RAG.Application/Services/AuthService.cs new file mode 100644 index 0000000..75e859d --- /dev/null +++ b/backend/RAG.Application/Services/AuthService.cs @@ -0,0 +1,105 @@ + + +using System.Numerics; +using RaG.Application.Common; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Security; +using RAG.Infrastructure.Token; + +namespace RAG.Application.Services; + + + +public class AuthService +{ + private readonly IERepository _userRepo; + private readonly IPasswordHasher _passwordHasher; + private readonly IJwtTokenGererator _jwtTokenGererator; + + + + public AuthService(IERepository userRepo, IPasswordHasher passwordHasher, IJwtTokenGererator tokenGererator) + { + + _passwordHasher = passwordHasher; + + _userRepo = userRepo; + _jwtTokenGererator = tokenGererator; + } + //使用判断逻辑进行注册 + public async Task RegisterAsync(string userName, string password, string Email, string telephone) + { + var users = await _userRepo.GetAllAsync(); + + if (users.Any(u => u.UserName == userName)) + throw new Exception("用户名已存在"); + + if (password.Length <= 8) + throw new Exception("密码长度必须大于8"); + if (Email.Length <= 6) + throw new Exception("密码长度必须大于6"); + + + + + var salt = _passwordHasher.GenerateSalt(); + + var hash = _passwordHasher.HashPassword(password, salt); + + var user = new User(userName, hash, telephone, Email, salt); + await _userRepo.CreateAsync(user); + + return user; + } + //使用判断逻辑进行登录 + public async Task LoginAsync(string userName, string password, string secret, int expire = 120) + { + + var users = await _userRepo.GetAllAsync(); + + var user = users.FirstOrDefault(u => u.UserName == userName); + if (user == null) return null; + var isValid = _passwordHasher.VerifHashePassword(user.Password, user.Salt, password); + var token = _jwtTokenGererator.GeneratorToken(user.Id.ToString(), user.UserName); + return ApiResult.Success(token, "denglu成功"); + } + + + //使用逻辑进行查询所有用户 + public async Task GeUserAsync() + { + + var users = await _userRepo.GetAllAsync(); + if (users == null) + { + return null; + } + return ApiResult.Success(users); + } + public async Task GetUserById(long userId) + { + return await _userRepo.GetByIdAsync(userId); + } + + + + + + // ///删除功能 + // public async Task DeleteAsync(Guid id) + // { + + // var users = await _userRepo.GetAllAsync(); + // var user = users.FirstOrDefault(u => u.Id == id); + // if (user != null) + // { + // await _userRepo.DeleteASync(id); + // return user; + // } + // else return null; + // } + + //进行删除功能 + +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/User.cs b/backend/RAG.Domain/Entities/User.cs index b78bf42..8c5baa5 100644 --- a/backend/RAG.Domain/Entities/User.cs +++ b/backend/RAG.Domain/Entities/User.cs @@ -6,30 +6,34 @@ namespace RAG.Domain.Entities { public class User : EntityBase { - private User() { } - - public static User Create( - string username, - string email, - string passwordHash, - string fullName) - { - return new User - { - Id = new Random().NextInt64(1, long.MaxValue), - Username = username, - Email = email, - PasswordHash = passwordHash, - FullName = fullName, - IsActive = true - }; - } - - public string Username { get; private set; } - public string Email { get; private set; } - public string PasswordHash { get; private set; } - public string FullName { get; private set; } - public bool IsActive { get; private set; } + protected User() { } + + + public User(string userName, string password, string telephone, string email, string salt) + { + + UserName = userName; + Password = password; + Salt = salt; + Telephone = telephone; + Email = email; + + + + Email = Email; + + + + } + + public string UserName { get; private set; } + public string Password { get; private set; } + public string Salt { get; private set; } + + public string? Nickname { get; set; } + public string? Avatar { get; set; } + public string? Telephone { get; set; } + public string? Email { get; set; } public ICollection Documents { get; private set; } = new List(); public ICollection ChatHistories { get; private set; } = new List(); } diff --git a/backend/RAG.Domain/Repositories/IERepository.cs b/backend/RAG.Domain/Repositories/IERepository.cs new file mode 100644 index 0000000..ccc3e05 --- /dev/null +++ b/backend/RAG.Domain/Repositories/IERepository.cs @@ -0,0 +1,22 @@ + +namespace RAG.Domain.Repositories; + +public interface IERepository +{ + + + + Task> GetAllAsync(); + Task GetByIdAsync(long id); + // Task> GetPageAsync(int pageIndex,int PageSize); + + Task CreateAsync(T entity); + + Task UpdateAsync(T entity); + + Task DeleteAsync(T entity); + + Task DeleteASyncId(Guid id); + + // Task DeleteHardaAsync(Guid id); +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index 4a57877..65211a2 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -38,14 +38,13 @@ namespace RAG.Infrastructure.Data modelBuilder.Entity(entity => { entity.HasKey(u => u.Id); - entity.Property(u => u.Username).IsRequired().HasMaxLength(50).HasColumnType("text"); + entity.Property(u => u.UserName).IsRequired().HasMaxLength(50).HasColumnType("text"); entity.Property(u => u.Email).IsRequired().HasMaxLength(100).HasColumnType("text"); - entity.Property(u => u.PasswordHash).IsRequired().HasMaxLength(255).HasColumnType("text"); - entity.Property(u => u.FullName).HasMaxLength(100).HasColumnType("text"); - entity.Property(u => u.IsActive).IsRequired(); + entity.Property(u => u.Password).IsRequired().HasMaxLength(255).HasColumnType("text"); + entity.Property(u => u.Nickname).HasMaxLength(100).HasColumnType("text"); // 索引 - entity.HasIndex(u => u.Username).IsUnique(); + entity.HasIndex(u => u.UserName).IsUnique(); entity.HasIndex(u => u.Email).IsUnique(); }); diff --git a/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs new file mode 100644 index 0000000..d342e5e --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs @@ -0,0 +1,531 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250807001255_auth2")] + partial class auth2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs b/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs new file mode 100644 index 0000000..a4fce14 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs @@ -0,0 +1,113 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class auth2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FullName", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsActive", + table: "Users"); + + migrationBuilder.RenameColumn( + name: "Username", + table: "Users", + newName: "UserName"); + + migrationBuilder.RenameColumn( + name: "PasswordHash", + table: "Users", + newName: "Password"); + + migrationBuilder.RenameIndex( + name: "IX_Users_Username", + table: "Users", + newName: "IX_Users_UserName"); + + migrationBuilder.AddColumn( + name: "Avatar", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Nickname", + table: "Users", + type: "text", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "Salt", + table: "Users", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Telephone", + table: "Users", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Avatar", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Nickname", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Salt", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Telephone", + table: "Users"); + + migrationBuilder.RenameColumn( + name: "UserName", + table: "Users", + newName: "Username"); + + migrationBuilder.RenameColumn( + name: "Password", + table: "Users", + newName: "PasswordHash"); + + migrationBuilder.RenameIndex( + name: "IX_Users_UserName", + table: "Users", + newName: "IX_Users_Username"); + + migrationBuilder.AddColumn( + name: "FullName", + table: "Users", + type: "text", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "IsActive", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index ed77539..30b5de7 100644 --- a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -263,6 +263,9 @@ namespace RAG.Infrastructure.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Avatar") + .HasColumnType("text"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -271,23 +274,26 @@ namespace RAG.Infrastructure.Migrations .HasMaxLength(100) .HasColumnType("text"); - b.Property("FullName") - .IsRequired() + b.Property("Nickname") .HasMaxLength(100) .HasColumnType("text"); - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("PasswordHash") + b.Property("Password") .IsRequired() .HasMaxLength(255) .HasColumnType("text"); + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("Username") + b.Property("UserName") .IsRequired() .HasMaxLength(50) .HasColumnType("text"); @@ -297,7 +303,7 @@ namespace RAG.Infrastructure.Migrations b.HasIndex("Email") .IsUnique(); - b.HasIndex("Username") + b.HasIndex("UserName") .IsUnique(); b.ToTable("Users"); diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index 42bed28..a6aef7b 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -5,6 +5,8 @@ + + @@ -12,6 +14,7 @@ + diff --git a/backend/RAG.Infrastructure/Repositories/EReposiotory.cs b/backend/RAG.Infrastructure/Repositories/EReposiotory.cs new file mode 100644 index 0000000..4f7acd5 --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/EReposiotory.cs @@ -0,0 +1,119 @@ + + + +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; + +namespace RAG.Infrastructure.Repositories; + + + + + +public class ERepository : IERepository where T : EntityBase +{ + + + private readonly ApplicationDbContext _db; + private readonly DbSet _tb; + + public ERepository(ApplicationDbContext db) + { + _db = db; + _tb = _db.Set(); + } + public async Task CreateAsync(T entity) + { + var obj = await _tb.AddAsync(entity); + + await _db.SaveChangesAsync(); + return obj.Entity; + } + + public Task DeleteAsync(T entity) + { + throw new NotImplementedException(); + } + + public Task DeleteASyncId(Guid id) + { + throw new NotImplementedException(); + } + + // public async Task DeleteAsync(T entity) + // { + // // entity = true; + // // await UpdateAsync(entity); + + + + // } + + // public async Task DeleteASyncId(Guid id) + // { + // var obj = await GetByIdAsync(id); + // if (obj == null) + // { + // return; + // } + + + // await DeleteAsync(obj); + // } + + // public async Task DeleteHardaAsync(Guid id) + // { + // var obj = await GetByIdAsync(id); + // if (obj == null) + // { + // return; + + // } + // _tb.Remove(obj); + // await _db.SaveChangesAsync(); + // } + + public async Task> GetAllAsync() + { + var list = await _tb.AsNoTracking().ToListAsync(); + return list; + } + + + + public async Task GetByIdAsync(long id) + { + var obj = await _tb.FindAsync(id); + return obj; } + + public Task UpdateAsync(T entity) + { + throw new NotImplementedException(); + } + + // public async Task> GetPageAsync(int pageIndex, int PageSize) + // { + + // // var list = await _tb.AsNoTracking() + // // .Where(x => x.IsDeleted == false) + // // .Skip((pageIndex - 1) * PageSize) + // // .Take(PageSize) + // // .ToListAsync(); + + // // var count = await _tb.CountAsync(); + + // // var pagedResult = new PagedResult { Items = list, TotalCount = count }; + + // // return pagedResult; + // } + + // public async Task UpdateAsync(T entity) + // { + // entity.UpdatedAt = DateTime.UtcNow; + // _tb.Update(entity); + + // await _db.SaveChangesAsync(); + // } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Security/PasswordHasher.cs b/backend/RAG.Infrastructure/Security/PasswordHasher.cs new file mode 100644 index 0000000..e3eec83 --- /dev/null +++ b/backend/RAG.Infrastructure/Security/PasswordHasher.cs @@ -0,0 +1,31 @@ + +namespace RAG.Infrastructure.Security; + + +public class PasswordHasher : IPasswordHasher +{ + public string GenerateSalt() + { + return Guid.NewGuid().ToString("N"); + } + + public string HashPassword(string password, string salt) + { + return BCrypt.Net.BCrypt.HashPassword(password + salt); + } + + public bool VerifHashePassword(string hashPassword, string salt, string providedPassword) + { + return BCrypt.Net.BCrypt.Verify(providedPassword + salt, hashPassword); + } +} + +public interface IPasswordHasher +{ + + string GenerateSalt(); + string HashPassword(string password, string salt); + + + bool VerifHashePassword(string hashPassword, string salt, string providedPassword); + } diff --git a/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs b/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ec27831 --- /dev/null +++ b/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs @@ -0,0 +1,32 @@ + + + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; +using RAG.Infrastructure.Repositories; +using RAG.Infrastructure.Security; +using RAG.Infrastructure.Token; + +namespace RAG.Infrastructure; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddINfrastructure(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => options.UseNpgsql(configuration.GetConnectionString("pgsql"))); + + services.AddScoped(typeof(IERepository<>), typeof(ERepository<>)); + services.AddScoped(typeof(IPasswordHasher), typeof(PasswordHasher)); + services.AddSingleton(); + + + + + // services.AddScoped (); + + return services; + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Token/JwtTokenGererator.cs b/backend/RAG.Infrastructure/Token/JwtTokenGererator.cs new file mode 100644 index 0000000..d61810a --- /dev/null +++ b/backend/RAG.Infrastructure/Token/JwtTokenGererator.cs @@ -0,0 +1,49 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace RAG.Infrastructure.Token; + + +public class JwtTokenGeneerator : IJwtTokenGererator +{ + + private readonly IConfiguration _configuration; + + public JwtTokenGeneerator(IConfiguration configuration) + { + _configuration = configuration; + } + public string GeneratorToken(string userId, string userName, int expireMinutes = 120) + { + var secret = _configuration["Jwt:Secret"] ?? throw new Exception("Jwt:Secret 置缺陷 "); + var expire = int.TryParse(_configuration["Jwt:ExpireMinutes"], out var e) ? e : expireMinutes; + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier,userId), + new Claim(ClaimTypes.Name,userName) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var token = new JwtSecurityToken( + issuer: "admin", + audience: "admin", + claims: claims, + expires: DateTime.Now.AddMinutes(expireMinutes), + signingCredentials: creds + ); + return new JwtSecurityTokenHandler().WriteToken(token); + + + } +} + +public interface IJwtTokenGererator +{ + + string GeneratorToken(string userId, string userName, int expireMinutes = 120); + +} \ No newline at end of file -- Gitee From 53daec7348c10ee0e63bfcd095a36579a2f4adfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Thu, 7 Aug 2025 14:13:42 +0800 Subject: [PATCH 21/31] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=BC=E5=BC=8F=E9=AA=8C=E8=AF=81=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20.txt,=20.md,=20.pdf,=20.doc,=20.docx,=20.ppt,=20.pp?= =?UTF-8?q?tx,=20.xls,=20.xlsx,=20.png,=20.jpg,=20.jpeg=20=E7=AD=89?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F,=E6=96=B0=E5=A2=9E=E4=BA=86=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=8A=A5=E5=91=8A=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E8=B7=9F=E8=B8=AA=E6=96=87=E6=A1=A3=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=92=8C=E5=A4=84=E7=90=86=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/DocumentController.cs | 3 +- .../Controllers/DocumentsController.cs | 81 ++++++++ .../Controllers/TestExceptionController.cs | 39 ++++ .../RAG.Api/Middleware/ExceptionMiddleware.cs | 93 ++++++++++ backend/RAG.Api/Program.cs | 13 ++ backend/RAG.Api/RAG.Api.http | 27 ++- .../DTOs/DocumentContentDto.cs | 18 ++ .../Interfaces/IDocumentProcessor.cs | 27 +++ .../BaseDocumentProcessor.cs | 26 +++ .../DocumentProcessorFactory.cs | 29 +++ .../ImageDocumentProcessor.cs | 100 ++++++++++ .../OfficeDocumentProcessor.cs | 167 +++++++++++++++++ .../PdfDocumentProcessor.cs | 66 +++++++ .../Services/DocumentService.cs | 175 ++++++++++++++---- .../Services/ProgressService.cs | 36 ++++ .../RAG.Domain/Repositories/IVectorStore.cs | 2 +- .../RAG.Infrastructure.csproj | 2 + .../VectorStore/PgVectorStore.cs | 45 ++++- 18 files changed, 907 insertions(+), 42 deletions(-) create mode 100644 backend/RAG.Api/Controllers/DocumentsController.cs create mode 100644 backend/RAG.Api/Controllers/TestExceptionController.cs create mode 100644 backend/RAG.Api/Middleware/ExceptionMiddleware.cs create mode 100644 backend/RAG.Application/DTOs/DocumentContentDto.cs create mode 100644 backend/RAG.Application/Interfaces/IDocumentProcessor.cs create mode 100644 backend/RAG.Application/Services/DocumentProcessors/BaseDocumentProcessor.cs create mode 100644 backend/RAG.Application/Services/DocumentProcessors/DocumentProcessorFactory.cs create mode 100644 backend/RAG.Application/Services/DocumentProcessors/ImageDocumentProcessor.cs create mode 100644 backend/RAG.Application/Services/DocumentProcessors/OfficeDocumentProcessor.cs create mode 100644 backend/RAG.Application/Services/DocumentProcessors/PdfDocumentProcessor.cs create mode 100644 backend/RAG.Application/Services/ProgressService.cs diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index fca1238..2ef2000 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -12,7 +12,8 @@ namespace RAG.Api.Controllers [ApiController] public class DocumentController : ControllerBase { - private readonly IDocumentService _documentService; + // 正确引用 IDocumentService 所在的命名空间 +private readonly IDocumentService _documentService; public DocumentController(IDocumentService documentService) { diff --git a/backend/RAG.Api/Controllers/DocumentsController.cs b/backend/RAG.Api/Controllers/DocumentsController.cs new file mode 100644 index 0000000..674a3e9 --- /dev/null +++ b/backend/RAG.Api/Controllers/DocumentsController.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; + +namespace RAG.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class DocumentsController : ControllerBase + { + private readonly IDocumentService _documentService; + private readonly IProgressService _progressService; + + public DocumentsController(IDocumentService documentService, IProgressService progressService) + { + _documentService = documentService; + _progressService = progressService; + } + + /// + /// 上传单个文档 + /// + [HttpPost] + public async Task UploadDocument([Required] IFormFile file, [Required] string title, string accessLevel = "Public") + { + if (file == null || file.Length == 0) + { + return BadRequest("File is required"); + } + + var uploadDto = new DocumentUploadDto + { + File = file, + Title = title, + AccessLevel = accessLevel + }; + + var result = await _documentService.UploadDocumentAsync(uploadDto); + return CreatedAtAction(nameof(GetDocument), new { id = result.Id }, result); + } + + /// + /// 获取文档进度 + /// + [HttpGet("progress/{progressKey}")] + public IActionResult GetProgress(string progressKey) + { + // 从进度服务获取最新进度 + var progress = _progressService.GetProgress(progressKey); + if (progress == null) + { + return NotFound("Progress not found"); + } + + return Ok(new + { + Progress = progress.Percentage, + Message = progress.Message + }); + } + + /// + /// 获取单个文档 + /// + [HttpGet("{id}")] + public async Task GetDocument(long id) + { + var document = await _documentService.GetDocumentByIdAsync(id); + if (document == null) + { + return NotFound(); + } + return Ok(document); + } + + // 其他文档相关端点... + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Controllers/TestExceptionController.cs b/backend/RAG.Api/Controllers/TestExceptionController.cs new file mode 100644 index 0000000..91c4afe --- /dev/null +++ b/backend/RAG.Api/Controllers/TestExceptionController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using RAG.Api.Middleware; +using System; + +namespace RAG.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TestExceptionController : ControllerBase + { + /// + /// 测试验证异常 + /// + [HttpGet("validation")] + public IActionResult TestValidationException() + { + var errors = new { Field1 = new[] { "字段1不能为空" }, Field2 = new[] { "字段2格式不正确" } }; + throw new ValidationException("输入验证失败", errors); + } + + /// + /// 测试未找到异常 + /// + [HttpGet("not-found")] + public IActionResult TestNotFoundException() + { + throw new NotFoundException("请求的资源不存在"); + } + + /// + /// 测试内部服务器错误 + /// + [HttpGet("internal-error")] + public IActionResult TestInternalError() + { + throw new Exception("发生了未预期的错误"); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Middleware/ExceptionMiddleware.cs b/backend/RAG.Api/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000..468d59c --- /dev/null +++ b/backend/RAG.Api/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System;using System.Net; +using System.Text.Json; +using System.Threading.Tasks; + +namespace RAG.Api.Middleware +{ + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unhandled exception occurred"); + await HandleExceptionAsync(context, ex); + } + } + + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var response = exception switch + { + ValidationException ex => new ErrorResponse + { + Code = "VALIDATION_ERROR", + Message = "输入验证失败", + Details = ex.Errors + }, + NotFoundException ex => new ErrorResponse + { + Code = "NOT_FOUND", + Message = ex.Message + }, + _ => new ErrorResponse + { + Code = "INTERNAL_ERROR", + Message = "服务器内部错误" + } + }; + + context.Response.StatusCode = GetStatusCode(exception); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(response)); + } + + private int GetStatusCode(Exception exception) + { + return exception switch + { + ValidationException => (int)HttpStatusCode.BadRequest, + NotFoundException => (int)HttpStatusCode.NotFound, + _ => (int)HttpStatusCode.InternalServerError + }; + } + } + + public class ErrorResponse + { + public string Code { get; set; } + public string Message { get; set; } + public object Details { get; set; } + } + + public class ValidationException : Exception + { + public object Errors { get; } + + public ValidationException(string message, object errors) : base(message) + { + Errors = errors; + } + } + + public class NotFoundException : Exception + { + public NotFoundException(string message) : base(message) + {} + } +} \ No newline at end of file diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs index 5edbcb0..91e4a60 100644 --- a/backend/RAG.Api/Program.cs +++ b/backend/RAG.Api/Program.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.EntityFrameworkCore; using MediatR; using RAG.Infrastructure.Data; +using RAG.Api.Middleware; using System.Reflection; @@ -40,6 +41,15 @@ builder.Services.AddScoped(); +// 注册进度服务 +builder.Services.AddScoped(); + +// 注册文档处理器 +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // 注册向量任务仓储 builder.Services.AddScoped, RAG.Infrastructure.Repositories.Repository>(); @@ -78,6 +88,9 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } +// 注册异常处理中间件 +app.UseMiddleware(); + app.UseHttpsRedirection(); app.UseCors("AllowAll"); diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 35d1e38..e56cff9 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -2,7 +2,28 @@ ### 文档管理 API 测试 -#### 1. 上传单个文档 +#### 1. 上传单个文档(测试修复后功能) +POST {{url}}/api/document/upload +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="File"; filename="test_fix.txt" +Content-Type: text/plain + +这是测试修复后功能的文档内容 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="Title" + +测试修复后文档 +----WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="AccessLevel" + +internal +----WebKitFormBoundary7MA4YWxkTrZu0gW + +### + +#### 2. 上传单个文档 POST {{url}}/api/document/upload Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW @@ -85,8 +106,6 @@ Content-Type: application/json { "Title": "更新后的文档标题", - "Description": "这是更新后的文档描述", - "Tags": ["tag1", "tag2"], "AccessLevel": "public" } @@ -102,7 +121,7 @@ PUT {{url}}/api/document/update-access-level Content-Type: application/json { - "Ids": [3040509232552355159, 1876963397423496466], + "Ids": [4631834103230408532, 5761862722737776785], "AccessLevel": "public" } diff --git a/backend/RAG.Application/DTOs/DocumentContentDto.cs b/backend/RAG.Application/DTOs/DocumentContentDto.cs new file mode 100644 index 0000000..a3ef1d8 --- /dev/null +++ b/backend/RAG.Application/DTOs/DocumentContentDto.cs @@ -0,0 +1,18 @@ +using System; + +namespace RAG.Application.DTOs +{ + public class DocumentMetadataDto + { + public string FileName { get; set; } = string.Empty; + public string FileType { get; set; } = string.Empty; + public int PageCount { get; set; } + // 可以根据需要添加更多元数据属性 + } + + public class DocumentContentDto + { + public string Content { get; set; } = string.Empty; + public DocumentMetadataDto Metadata { get; set; } = new DocumentMetadataDto(); + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Interfaces/IDocumentProcessor.cs b/backend/RAG.Application/Interfaces/IDocumentProcessor.cs new file mode 100644 index 0000000..ad4d7ca --- /dev/null +++ b/backend/RAG.Application/Interfaces/IDocumentProcessor.cs @@ -0,0 +1,27 @@ +using System.IO; +using System.Threading.Tasks; +using RAG.Application.DTOs; + +namespace RAG.Application.Interfaces +{ + public interface IDocumentProcessor + { + string[] SupportedFileTypes { get; } + Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey); + } + + public interface IProgressService + { + void ReportProgress(string operationId, int progress, string status); + ProgressInfo GetProgress(string operationId); + event ProgressReportedEventHandler ProgressReported; + } + + public class ProgressInfo + { + public int Percentage { get; set; } + public string Message { get; set; } + } + + public delegate void ProgressReportedEventHandler(string operationId, int progress, string status); +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentProcessors/BaseDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/BaseDocumentProcessor.cs new file mode 100644 index 0000000..f8895be --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/BaseDocumentProcessor.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; + +namespace RAG.Application.Services.DocumentProcessors +{ + public abstract class BaseDocumentProcessor : IDocumentProcessor + { + protected readonly IProgressService _progressService; + + protected BaseDocumentProcessor(IProgressService progressService) + { + _progressService = progressService; + } + + public abstract string[] SupportedFileTypes { get; } + + public abstract Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey); + + protected void ReportProgress(string operationId, int progress, string status) + { + _progressService.ReportProgress(operationId, progress, status); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentProcessors/DocumentProcessorFactory.cs b/backend/RAG.Application/Services/DocumentProcessors/DocumentProcessorFactory.cs new file mode 100644 index 0000000..da1989d --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/DocumentProcessorFactory.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; + +namespace RAG.Application.Services.DocumentProcessors +{ + public class DocumentProcessorFactory + { + private readonly IEnumerable _processors; + + public DocumentProcessorFactory(IEnumerable processors) + { + _processors = processors; + } + + public IDocumentProcessor GetProcessor(string fileExtension) + { + foreach (var processor in _processors) + { + if (processor.SupportedFileTypes != null && processor.SupportedFileTypes.Contains(fileExtension.ToLower())) + { + return processor; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentProcessors/ImageDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/ImageDocumentProcessor.cs new file mode 100644 index 0000000..e74ded3 --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/ImageDocumentProcessor.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Tesseract; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; + +namespace RAG.Application.Services.DocumentProcessors +{ + public class ImageDocumentProcessor : BaseDocumentProcessor + { + public override string[] SupportedFileTypes => new[] { ".png", ".jpg", ".jpeg", ".bmp", ".tiff" }; + + private readonly string _tesseractDataPath; + + public ImageDocumentProcessor(IProgressService progressService) + : base(progressService) + { + // 注意:Tesseract需要语言数据文件,请确保已安装并指定正确的路径 + // 通常需要在项目中添加tessdata文件夹并设置复制到输出目录 + _tesseractDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tessdata"); + } + + public override async Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey) + { + var operationId = progressKey; + ReportProgress(operationId, 0, "开始处理图片文档"); + + // 使用内存流保存文件内容 + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + ReportProgress(operationId, 30, "已加载图片内容"); + + // 提取OCR文本 + string extractedText; + try + { + extractedText = await ExtractTextFromImageAsync(memoryStream); + } + catch (Exception ex) + { + extractedText = $"[OCR处理错误: {ex.Message}]"; + } + + ReportProgress(operationId, 100, "图片文档OCR处理完成"); + + return new DocumentContentDto + { + Content = extractedText, + Metadata = new DocumentMetadataDto + { + // 注意:由于我们没有 fileName 参数,这里使用默认值 + FileName = string.Empty, + // 从progressKey中尝试提取文件扩展名,如果无法提取则使用默认值 + FileType = Path.HasExtension(progressKey) ? Path.GetExtension(progressKey).ToLower() : ".jpg" + } + }; + } + } + + private async Task ExtractTextFromImageAsync(Stream imageStream) + { + return await Task.Run(() => + { + try + { + // 确保tessdata目录存在 + if (!Directory.Exists(_tesseractDataPath)) + { + throw new DirectoryNotFoundException($"未找到Tesseract语言数据目录: {_tesseractDataPath}"); + } + + using (var engine = new TesseractEngine(_tesseractDataPath, "chi_sim+eng", EngineMode.Default)) + { + using (var memoryStream = new MemoryStream()) + { + imageStream.CopyTo(memoryStream); + using (var img = Pix.LoadFromMemory(memoryStream.ToArray())) + { + using (var page = engine.Process(img)) + { + return page.GetText(); + } + } + } + } + } + catch (Exception ex) + { + return $"[OCR处理异常: {ex.Message}]"; + } + }); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentProcessors/OfficeDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/OfficeDocumentProcessor.cs new file mode 100644 index 0000000..48bbb1e --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/OfficeDocumentProcessor.cs @@ -0,0 +1,167 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using NPOI.XWPF.UserModel; +using NPOI.XSSF.UserModel; +using NPOI.HSSF.UserModel; +using NPOI.SS.UserModel; +using NPOI.POIFS.FileSystem; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; + +namespace RAG.Application.Services.DocumentProcessors +{ + public class OfficeDocumentProcessor : BaseDocumentProcessor + { + public override string[] SupportedFileTypes => new[] { ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx" }; + + public OfficeDocumentProcessor(IProgressService progressService) : base(progressService) + { + } + + public override async Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey) + { + var operationId = progressKey; + ReportProgress(operationId, 0, "开始处理Office文档"); + + var fileExtension = Path.GetExtension(fileName).ToLower(); + var content = new StringBuilder(); + + // 使用内存流保存文件内容 + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + ReportProgress(operationId, 20, "已加载文件内容"); + + // 根据文件类型提取内容 + switch (fileExtension) + { + case ".docx": + content.Append(ExtractWordContentAsync(memoryStream)); + break; + case ".doc": + content.Append(ExtractWordContentAsync(memoryStream)); + break; + case ".xlsx": + content.Append(ExtractExcelContent(memoryStream)); + break; + case ".xls": + content.Append(ExtractExcelContent(memoryStream)); + break; + case ".pptx": + case ".ppt": + content.Append("[演示文稿内容提取暂未实现]"); + break; + } + + ReportProgress(operationId, 100, "Office文档处理完成"); + + return new DocumentContentDto + { + Content = content.ToString(), + Metadata = new DocumentMetadataDto + { + FileName = fileName, + FileType = fileExtension + } + }; + } + } + + private async Task ExtractWordContentAsync(Stream stream) + { + var content = new StringBuilder(); + + try + { + using (var document = new XWPFDocument(stream)) + { + foreach (var paragraph in document.Paragraphs) + { + content.AppendLine(paragraph.Text); + } + } + } + catch (Exception) + { + // 尝试使用旧格式处理 + // 创建流的副本,因为原始流的位置可能已经改变 + stream.Position = 0; + using (var memoryStream = new MemoryStream()) + { + await stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + // NPOIFSFileSystem不实现IDisposable,不能用于using语句 + var fs = new NPOIFSFileSystem(memoryStream); + try + { + // 这里只是占位,实际处理旧格式.doc文件需要更多代码 + content.Append("[旧格式Word文档内容提取]"); + } + finally + { + // 手动释放资源 + fs.Close(); + } + } + } + + return content.ToString(); + } + + private string ExtractExcelContent(Stream stream) + { + var content = new StringBuilder(); + + try + { + IWorkbook workbook = null; + if (stream.CanSeek) + { + stream.Position = 0; + workbook = new XSSFWorkbook(stream); + } + + if (workbook == null) + { + stream.Position = 0; + workbook = new HSSFWorkbook(stream); + } + + for (int sheetIndex = 0; sheetIndex < workbook.NumberOfSheets; sheetIndex++) + { + var sheet = workbook.GetSheetAt(sheetIndex); + content.AppendLine($"\n--- 工作表: {sheet.SheetName} ---"); + + for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++) + { + var row = sheet.GetRow(rowIndex); + if (row != null) + { + var rowContent = new StringBuilder(); + for (int cellIndex = 0; cellIndex < row.LastCellNum; cellIndex++) + { + var cell = row.GetCell(cellIndex); + if (cell != null) + { + rowContent.Append($"{cell} "); + } + } + content.AppendLine(rowContent.ToString()); + } + } + } + } + catch (Exception ex) + { + content.AppendLine($"[Excel文档处理错误: {ex.Message}]"); + } + + return content.ToString(); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentProcessors/PdfDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/PdfDocumentProcessor.cs new file mode 100644 index 0000000..763246f --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/PdfDocumentProcessor.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using PdfSharp.Pdf; +using PdfSharp.Pdf.IO; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; + +namespace RAG.Application.Services.DocumentProcessors +{ + public class PdfDocumentProcessor : BaseDocumentProcessor + { + public override string[] SupportedFileTypes => new[] { ".pdf" }; + + public PdfDocumentProcessor(IProgressService progressService) : base(progressService) + { + } + + public override async Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey) + { + var operationId = progressKey; + ReportProgress(operationId, 0, "开始处理PDF文档"); + + // 使用内存流保存文件内容 + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + // 使用PdfSharp读取PDF + var document = PdfReader.Open(memoryStream, PdfDocumentOpenMode.ReadOnly); + var content = new StringBuilder(); + + ReportProgress(operationId, 20, "已打开PDF文档"); + + // 提取文本内容 + for (int i = 0; i < document.Pages.Count; i++) + { + var page = document.Pages[i]; + // 注意:PdfSharp本身不直接提供文本提取功能 + // 这里使用占位文本,实际项目中可能需要使用其他库如iTextSharp或PdfPig + content.AppendLine($"--- 第 {i + 1} 页 ---"); + content.AppendLine("[PDF内容提取需要使用专门的文本提取库]"); + + // 报告进度 + var pageProgress = 20 + (i * 80 / document.Pages.Count); + ReportProgress(operationId, pageProgress, $"正在处理第 {i + 1} 页"); + } + + ReportProgress(operationId, 100, "PDF文档处理完成"); + + return new DocumentContentDto + { + Content = content.ToString(), + Metadata = new DocumentMetadataDto + { + FileName = fileName, + FileType = ".pdf", + PageCount = document.Pages.Count + } + }; + } + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs index 8537ece..2d32b9d 100644 --- a/backend/RAG.Application/Services/DocumentService.cs +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using RAG.Application.DTOs; using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; using RAG.Domain.Entities; using RAG.Domain.Repositories; using RAG.Infrastructure.FileStorage; @@ -16,19 +17,30 @@ namespace RAG.Application.Services { public class DocumentService : IDocumentService { + private readonly IProgressService _progressService; + private readonly DocumentProcessorFactory _documentProcessorFactory; private readonly ApplicationDbContext _context; - private readonly IFileStorageService _fileStorageService; - private readonly IVectorStore _vectorStore; - private readonly IRepository _vectorizationJobRepository; - private readonly IUnitOfWork _unitOfWork; - - public DocumentService(ApplicationDbContext context, IFileStorageService fileStorageService, IVectorStore vectorStore, IRepository vectorizationJobRepository, IUnitOfWork unitOfWork) + private readonly IFileStorageService _fileStorageService; + private readonly IVectorStore _vectorStore; + private readonly IRepository _vectorizationJobRepository; + private readonly IUnitOfWork _unitOfWork; + + public DocumentService( + IProgressService progressService, + DocumentProcessorFactory documentProcessorFactory, + ApplicationDbContext context, + IFileStorageService fileStorageService, + IVectorStore vectorStore, + IRepository vectorizationJobRepository, + IUnitOfWork unitOfWork) { + _progressService = progressService; + _documentProcessorFactory = documentProcessorFactory; _context = context; - _fileStorageService = fileStorageService; - _vectorStore = vectorStore; - _vectorizationJobRepository = vectorizationJobRepository; - _unitOfWork = unitOfWork; + _fileStorageService = fileStorageService; + _vectorStore = vectorStore; + _vectorizationJobRepository = vectorizationJobRepository; + _unitOfWork = unitOfWork; } public async Task UploadDocumentAsync(DocumentUploadDto uploadDto) @@ -49,10 +61,33 @@ namespace RAG.Application.Services throw new ArgumentException("File size exceeds the maximum limit of 50MB"); } - // 上传文件到存储服务 - using (var stream = uploadDto.File.OpenReadStream()) + // 获取合适的文档处理器 + var processor = _documentProcessorFactory.GetProcessor(fileExtension); + if (processor == null) + { + throw new InvalidOperationException($"No processor found for file type: {fileExtension}"); + } + + // 注册进度事件处理程序 + string progressKey = Guid.NewGuid().ToString(); + ProgressReportedEventHandler progressHandler = (operationId, progress, status) => + { + if (operationId == progressKey) + { + // 这里可以记录进度或执行其他操作 + Console.WriteLine($"Upload progress: {progress}% - {status}"); + } + }; + _progressService.ProgressReported += progressHandler; + + try { - var filePath = await _fileStorageService.UploadFileAsync(stream, uploadDto.File.FileName, uploadDto.File.ContentType); + // 上传文件到存储服务 + string filePath; + using (var stream = uploadDto.File.OpenReadStream()) + { + filePath = await _fileStorageService.UploadFileAsync(stream, uploadDto.File.FileName, uploadDto.File.ContentType); + } // 创建文档实体 var document = Document.Create( @@ -63,12 +98,20 @@ namespace RAG.Application.Services uploadDto.File.Length, uploadDto.AccessLevel); + // 保存到数据库 + _context.Documents.Add(document); + await _context.SaveChangesAsync(); + + // 解析文档内容 + DocumentContentDto contentDto; + using (var stream = await _fileStorageService.GetFileStreamAsync(filePath)) + { + contentDto = await processor.ExtractContentAsync(stream, uploadDto.File.FileName, progressKey); + } + // 更新文档状态为已完成 document.UpdateProcessingStatus(); document.CompleteProcessing(); - - // 保存到数据库 - _context.Documents.Add(document); await _context.SaveChangesAsync(); // 返回DTO @@ -85,6 +128,12 @@ namespace RAG.Application.Services UpdatedAt = document.UpdatedAt }; } + finally + { + // 移除进度事件处理程序 + _progressService.ProgressReported -= progressHandler; + } + } public async Task> UploadDocumentsAsync(IEnumerable uploadDtos) @@ -230,20 +279,20 @@ namespace RAG.Application.Services } // 更新文档属性 -// 由于 Document.Title 的 set 访问器不可访问,推测应该调用文档实体的更新方法 -// 使用实体的业务方法更新标题 -document.UpdateTitle(updateDto.Title); + // 由于 Document.Title 的 set 访问器不可访问,推测应该调用文档实体的更新方法 + // 使用实体的业务方法更新标题 + document.UpdateTitle(updateDto.Title); -// 更新访问权限(如果提供) -if (!string.IsNullOrEmpty(updateDto.AccessLevel)) -{ - document.UpdateAccessLevel(updateDto.AccessLevel); -} -else -{ - // 如果未提供权限,更新修改时间 - document.UpdateUpdatedAt(DateTime.UtcNow); -} + // 更新访问权限(如果提供) + if (!string.IsNullOrEmpty(updateDto.AccessLevel)) + { + document.UpdateAccessLevel(updateDto.AccessLevel); + } + else + { + // 如果未提供权限,更新修改时间 + document.UpdateUpdatedAt(DateTime.UtcNow); + } await _context.SaveChangesAsync(); @@ -343,7 +392,7 @@ else var vectorDataSizeBytes = totalChunks * 1536 * 4; // 获取存储空间使用量 -var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) ?? 0L; + var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) ?? 0L; // 获取已向量化的文档数量 vectorizedDocuments = await _context.Documents.CountAsync(d => d.VectorizationStatus == VectorizationStatus.Vectorized); @@ -366,9 +415,9 @@ var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) TotalDocuments = totalDocuments, VectorizedDocuments = vectorizedDocuments, TotalChunks = totalChunks, - VectorDataSizeBytes = vectorDataSizeBytes, - TotalFileSizeBytes = storageSizeBytes, - VectorStorageSizeBytes = vectorStorageSizeBytes + VectorDataSizeBytes = vectorDataSizeBytes, + TotalFileSizeBytes = storageSizeBytes, + VectorStorageSizeBytes = vectorStorageSizeBytes }; } @@ -802,5 +851,65 @@ var storageSizeBytes = await _context.Documents.SumAsync(d => (long?)d.FileSize) return embedding; } + + // 支持的文件类型列表 + private readonly List _supportedFileTypes = new List { ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".jpg", ".jpeg", ".png", ".bmp", ".tiff" }; + + // 验证文件类型是否有效 + private bool IsValidFileType(string fileType) + { + if (string.IsNullOrEmpty(fileType)) + return false; + + return _supportedFileTypes.Contains(fileType.ToLower()); + } + + public async Task UploadDocumentAsync(DocumentUploadDto request, Stream fileStream, string fileName, string fileType, long fileSize, CancellationToken cancellationToken) + { + // 1. 验证文件类型 + if (!IsValidFileType(fileType)) + { + throw new InvalidOperationException($"Unsupported file type: {fileType}"); + } + + // 2. 查找合适的文档处理器 + var processor = _documentProcessorFactory.GetProcessor(fileType); + if (processor == null) + { + throw new InvalidOperationException($"No processor found for file type: {fileType}"); + } + + // 3. 提取文档内容 + var progressKey = Guid.NewGuid().ToString(); + var documentContent = await processor.ExtractContentAsync(fileStream, fileName, progressKey); + + // 4. 创建文档实体 + // 注意:这里假设文件路径是存储服务返回的路径,实际应用中需要根据实际情况获取 + string filePath = ""; // 实际应用中需要设置正确的文件路径 + var document = Document.Create( + title: request.Title, + fileName: fileName, + filePath: filePath, + fileType: fileType, + fileSize: fileSize, + accessLevel: request.AccessLevel + ); + + // 5. 保存文档 (实际实现中需要调用仓储层) + // await _documentRepository.AddAsync(document, cancellationToken); + + // 6. 返回DocumentDto + return new DocumentDto + { + Id = document.Id, + Title = document.Title, + FileName = document.FileName, + FileType = document.FileType, + FileSize = document.FileSize, + AccessLevel = document.AccessLevel, + CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt + }; + } } } \ No newline at end of file diff --git a/backend/RAG.Application/Services/ProgressService.cs b/backend/RAG.Application/Services/ProgressService.cs new file mode 100644 index 0000000..85b9c5e --- /dev/null +++ b/backend/RAG.Application/Services/ProgressService.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using RAG.Application.Interfaces; +using System.Collections.Concurrent; + +namespace RAG.Application.Services +{ + public class ProgressService : IProgressService + { + private readonly ConcurrentDictionary _progressTrackers = new(); + + public event ProgressReportedEventHandler ProgressReported; + + public void ReportProgress(string operationId, int progress, string status) + { + // 更新进度信息 + var progressInfo = new ProgressInfo + { + Percentage = progress, + Message = status + }; + _progressTrackers[operationId] = progressInfo; + + // 触发进度报告事件 + ProgressReported?.Invoke(operationId, progress, status); + } + + public ProgressInfo GetProgress(string operationId) + { + if (_progressTrackers.TryGetValue(operationId, out var progressInfo)) + { + return progressInfo; + } + return null; + } + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IVectorStore.cs b/backend/RAG.Domain/Repositories/IVectorStore.cs index 9788378..031c686 100644 --- a/backend/RAG.Domain/Repositories/IVectorStore.cs +++ b/backend/RAG.Domain/Repositories/IVectorStore.cs @@ -7,7 +7,7 @@ namespace RAG.Domain.Repositories { Task InitializeAsync(); Task StoreVectorAsync(string content, float[] embedding, object? metadata = null); - Task> SearchVectorsAsync(float[] queryVector, int topK = 5); + Task> SearchVectorsAsync(float[] queryVector, int topK = 5, double threshold = 0.7); } public class VectorResult diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index 42bed28..21de479 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -6,12 +6,14 @@ + + diff --git a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs index a6b7015..47a7afb 100644 --- a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs +++ b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs @@ -50,7 +50,7 @@ namespace RAG.Infrastructure.VectorStore }); } - public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5) + public async Task> SearchVectorsAsync(float[] queryVector, int topK = 5, double threshold = 0.7) { if (!_initialized) { @@ -61,13 +61,52 @@ namespace RAG.Infrastructure.VectorStore // 实际应用中,这里应该使用PostgreSQL的向量扩展进行相似度搜索 return await Task.Run(() => { - var results = _vectorStore + // 初始搜索,应用阈值 + var initialResults = _vectorStore .Select(entry => new { Entry = entry, Similarity = CalculateCosineSimilarity(queryVector, entry.Embedding) }) + .Where(x => x.Similarity >= threshold) .OrderByDescending(x => x.Similarity) + .ToList(); + + // 动态调整阈值 + int topResultsCount = initialResults.Count; + if (topResultsCount < 3) + { + // 结果太少时降低阈值 + threshold *= 0.8; + Console.WriteLine($"Results too few, reducing threshold to {threshold}"); + initialResults = _vectorStore + .Select(entry => new + { + Entry = entry, + Similarity = CalculateCosineSimilarity(queryVector, entry.Embedding) + }) + .Where(x => x.Similarity >= threshold) + .OrderByDescending(x => x.Similarity) + .ToList(); + } + else if (topResultsCount > 10) + { + // 结果太多时提高阈值 + threshold *= 1.2; + Console.WriteLine($"Results too many, increasing threshold to {threshold}"); + initialResults = _vectorStore + .Select(entry => new + { + Entry = entry, + Similarity = CalculateCosineSimilarity(queryVector, entry.Embedding) + }) + .Where(x => x.Similarity >= threshold) + .OrderByDescending(x => x.Similarity) + .ToList(); + } + + // 取前topK个结果 + var results = initialResults .Take(topK) .Select(x => new VectorResult { @@ -80,7 +119,7 @@ namespace RAG.Infrastructure.VectorStore }) .ToList(); - Console.WriteLine($"Found {results.Count} vector results"); + Console.WriteLine($"Found {results.Count} vector results with threshold {threshold}"); return results; }); } -- Gitee From 00663f29fd3f27a028cc77bcc2c54d1a0fb246db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Thu, 7 Aug 2025 14:14:38 +0800 Subject: [PATCH 22/31] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=BC=E5=BC=8F=E9=AA=8C=E8=AF=81=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20.txt,=20.md,=20.pdf,=20.doc,=20.docx,=20.ppt,=20.pp?= =?UTF-8?q?tx,=20.xls,=20.xlsx,=20.png,=20.jpg,=20.jpeg=20=E7=AD=89?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F,=E6=96=B0=E5=A2=9E=E4=BA=86=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=8A=A5=E5=91=8A=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E8=B7=9F=E8=B8=AA=E6=96=87=E6=A1=A3=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=92=8C=E5=A4=84=E7=90=86=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index e56cff9..7f1c218 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -195,4 +195,4 @@ Accept: application/json GET {{url}}/weatherforecast/ Accept: application/json -### +### -- Gitee From 9a5cdfebb9f95823ab9f6df4a2cfbb36d2f8c801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Thu, 7 Aug 2025 14:39:32 +0800 Subject: [PATCH 23/31] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E4=B8=94=E5=AE=8C=E6=88=90=E5=A2=9E=E5=8A=A0=E4=BA=86=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=BC=E5=BC=8F=E9=AA=8C=E8=AF=81=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BA=86=E8=BF=9B=E5=BA=A6=E6=8A=A5=E5=91=8A=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=8F=AF=E4=BB=A5=E8=B7=9F=E8=B8=AA=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=8A=E4=BC=A0=E5=92=8C=E5=A4=84=E7=90=86=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Application/RAG.Application.csproj | 1 + backend/RAG.Application/Services/AuthService.cs | 2 +- .../RAG.Infrastructure/RAG.Infrastructure.csproj | 4 ---- .../RAG.Infrastructure/Security/PasswordHasher.cs | 15 ++++++--------- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj index 7baf0da..c59126e 100644 --- a/backend/RAG.Application/RAG.Application.csproj +++ b/backend/RAG.Application/RAG.Application.csproj @@ -11,6 +11,7 @@ + diff --git a/backend/RAG.Application/Services/AuthService.cs b/backend/RAG.Application/Services/AuthService.cs index 75e859d..0cb2431 100644 --- a/backend/RAG.Application/Services/AuthService.cs +++ b/backend/RAG.Application/Services/AuthService.cs @@ -60,7 +60,7 @@ public class AuthService var user = users.FirstOrDefault(u => u.UserName == userName); if (user == null) return null; - var isValid = _passwordHasher.VerifHashePassword(user.Password, user.Salt, password); + var isValid = _passwordHasher.HashPassword(password, user.Salt) == user.Password; var token = _jwtTokenGererator.GeneratorToken(user.Id.ToString(), user.UserName); return ApiResult.Success(token, "denglu成功"); } diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index 578e767..9b8050f 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -15,11 +15,7 @@ -<<<<<<< HEAD - -======= ->>>>>>> c5775331753763bba3ae6da76f21c3e969a5efeb diff --git a/backend/RAG.Infrastructure/Security/PasswordHasher.cs b/backend/RAG.Infrastructure/Security/PasswordHasher.cs index e3eec83..db0cfb1 100644 --- a/backend/RAG.Infrastructure/Security/PasswordHasher.cs +++ b/backend/RAG.Infrastructure/Security/PasswordHasher.cs @@ -1,7 +1,7 @@ - -namespace RAG.Infrastructure.Security; +using BCrypt.Net; +namespace RAG.Infrastructure.Security; public class PasswordHasher : IPasswordHasher { public string GenerateSalt() @@ -12,9 +12,9 @@ public class PasswordHasher : IPasswordHasher public string HashPassword(string password, string salt) { return BCrypt.Net.BCrypt.HashPassword(password + salt); - } + } - public bool VerifHashePassword(string hashPassword, string salt, string providedPassword) + public bool VerifyHashedPassword(string hashPassword, string salt, string providedPassword) { return BCrypt.Net.BCrypt.Verify(providedPassword + salt, hashPassword); } @@ -22,10 +22,7 @@ public class PasswordHasher : IPasswordHasher public interface IPasswordHasher { - string GenerateSalt(); string HashPassword(string password, string salt); - - - bool VerifHashePassword(string hashPassword, string salt, string providedPassword); - } + bool VerifyHashedPassword(string hashPassword, string salt, string providedPassword); +} -- Gitee From b70044daf7da8429d7625d1e4e9135986eab49d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Thu, 7 Aug 2025 15:37:54 +0800 Subject: [PATCH 24/31] =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=8C=E5=96=84=E6=88=90=E5=8A=9F?= =?UTF-8?q?=EF=BC=8C=E4=B8=8A=E4=BC=A0=E6=96=87=E6=A1=A3=E7=9A=84=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=A2=9E=E5=8A=A0=E4=B8=94=E6=B5=8B=E8=AF=95=E6=97=A0?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\347\244\272\346\226\207\347\250\277.ppt" | Bin 0 -> 20992 bytes ...23\347\261\273\350\256\276\350\256\241.md" | 164 ++++++++++++++++++ ...c4b-495a-b678-b687df0993cd_0864345678.docx | 0 ...3c4-4b6b-ac0c-0c7b7ada1ad5_0864345678.docx | 0 ...7\346\234\254\346\226\207\346\241\243.txt" | 0 .../RAG.Api/Middleware/ExceptionMiddleware.cs | 14 +- backend/RAG.Api/Program.cs | 2 + backend/RAG.Api/RAG.Api.http | 2 + .../MarkdownDocumentProcessor.cs | 42 +++++ .../TextDocumentProcessor.cs | 42 +++++ 10 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 "backend/RAG.Api/Files/084f8a6b-a806-465b-99f9-61d4398869e5_\346\226\260\345\273\272 PPT \346\274\224\347\244\272\346\226\207\347\250\277.ppt" create mode 100644 "backend/RAG.Api/Files/9b09b318-173e-4e3f-b14d-b7b819b588a7_\345\256\236\344\275\223\347\261\273\350\256\276\350\256\241.md" create mode 100644 backend/RAG.Api/Files/ecbb0685-2c4b-495a-b678-b687df0993cd_0864345678.docx create mode 100644 backend/RAG.Api/Files/f19b4e49-43c4-4b6b-ac0c-0c7b7ada1ad5_0864345678.docx create mode 100644 "backend/RAG.Api/Files/fd1b88c4-d836-4662-bbea-6702d6bead75_\346\226\260\345\273\272 \346\226\207\346\234\254\346\226\207\346\241\243.txt" create mode 100644 backend/RAG.Application/Services/DocumentProcessors/MarkdownDocumentProcessor.cs create mode 100644 backend/RAG.Application/Services/DocumentProcessors/TextDocumentProcessor.cs diff --git "a/backend/RAG.Api/Files/084f8a6b-a806-465b-99f9-61d4398869e5_\346\226\260\345\273\272 PPT \346\274\224\347\244\272\346\226\207\347\250\277.ppt" "b/backend/RAG.Api/Files/084f8a6b-a806-465b-99f9-61d4398869e5_\346\226\260\345\273\272 PPT \346\274\224\347\244\272\346\226\207\347\250\277.ppt" new file mode 100644 index 0000000000000000000000000000000000000000..ef8831a3d4bd6e596212124a879ca34a59c90f3e GIT binary patch literal 20992 zcmeHP4RBS(6+ZXgm*j-wkF!K6+d+&z(US7fr z3AMo8H|Or|*>lgHb9VRa-urfM^P%1?JAX0oZ;}>D#genhe986U4WtPwL_~6sVE$|} znIz5xAQ^;9$O7e3h2K1xBXik=5(TsjUfbi*uO2oABh5A*ClARI07H$;2K9H4pDhcK zF9L+ZBBa%Qqr1`;crv8NHccjAA3^%EG*327OAj&@IV9 z3oeld@tdvc{tBe1f<$!+vlxACk?LoDKK}GDF^v!qS_Xevd+EDSu-7ZhkqM|_j*(;99^9fJ~JDoe`gjHo#q&1qO_Zme3d>Pr23jImF1OltqHdFBJx z1HeAr6VMBA6@b$R$5n3tr*e+bK7hV}et`af0f4Ik0|A^*+VlTIQ1&^%b%5&uLjjx< z=o@YXd>(KUfNeGcFcL5dz&>;{U^Jiva0}p8z-@prfU$rt0KN$L65w{gmjR`KI{;C@ zIKZ8N@qh^c>d$aD(vy677kl|#1OLxCpskPRypf&%KW#gpW#6B*fAfjIU~q)x-v?;$9uVgv4Cpdj=!&z;WOS|; zP8(j>fq0qD_~J&S9|lwa=*i=Nn0Ma+Su2gmvz)VlP8+Rb5=bv_g{T9XAZ0er+#~@u zus~pezyg5<0t*Bd2rLj-pfeWW+jBnc@Tq26Sw%%29+UV4&$k2Qg9jj0j!3;BAT8f4 zSVma=n^d3{Y*|V$a`25HlmDc zLpMI(`cM&j7lY#gactgg z!Ny(!(>cI#|E~_-xW@63_pM#XjFLav{O7M&4%=KmFxYY=@1#oo)}DZP9QKpX!}*>C z3U^_bh`7A!zcID^!Z^5htwH2?yzW!yZK`p8fah+iy%TQPb9Z;1?Xh#!Em3v-k?mP4 z%>P0206$h3rc8BGE-Jh28su

J#pw9^wcjKtSJBG1C$S^E~NqE>byKT^*QW0!`u= z4pS-}HA(BHV^+*vlz}E5iie1{r69CQ#gcfG2+2*br9)_85^(1>Zz4liO838A;#mHW z6ePFS@ojlHyx&X8I;V{z(Xs6|5OS$`DkR9n^- zam%+Y>xzm=Brn($W0v|=V58wQY!_!qeG&24%+8aGQu`Yh21`)QP%KChEfFc!k>JUSH&z z)1W!zNmN+Qz4l_(vf=vVI1uGsC8{kSS|&DEn(|L;0LmzrVdmxYK7WWwBF_Ord(wD3 zu8Q}yoRE@|29bsaX=oBjr`x)9tD-w|e5xcHs-y|~@e#G5)BGBmlF|eyDQS|QS0`KP z{(Ab9;-mRdjtvJeGOAGNvwnVWD!OIM+^wa_XF(3eoIy%Y}{Pv0_p zbS}S=ss=gn59kEj-L>vsMVI*dIr(ble^c?J9ebX3*rrems3 zHysbT^;Mw0DK1J3wKbVc7_br9%R35Xw&qzoO{L3Yuo_*V&bypr;7qABKho3!$D=6_ z(hIbZrl55cYn0J4imgf)g1Z|`OcL=7Tn6~K^zm^i^l^#!xJ2N0dWN=wtu1EahlnE* zPS96yg2+{PE8(a(q$U$X@)NF4^+fR^Q+lZT$eeoF5=#6MaiNDdf#>>SH71#2Bv(Mv z4aQ?i-W7%;PTI6%;J{H5b3wj0)j%@E|7U2|Sl{niw)FPoc%?x5Hd@e@UI`up(mq-x zrO~D@oIdr;`}?ifyz$`tNp*QOPdqWE#-1gzOHJ)O^5jZ>60w%t@+8;Oqbqr5 zWKLL=xyVkJnewuwDRUiSEwkGb8rAqsm-(@|!>cKCFB!gkec3bLXPocFf1csyA4i@# zah$k1Q3XS_JC-I$Tk0B8t@Hx&TGvpQb^VqNU31q((siYt+(1TMCn2qMWrc$FVL2Bc zJC)SB|LbvM;wYMH6{bW!^esU{i5G#<8sv0?27B|GuWG9S$6-DMqXzdN-M$76$~+Cw zHQ_->3{0vHAGfOm#~sJYh4(nLLL}C=tqx?&Ra$3tfY+l6ZFD%AqC*)dbUUbbjA*JY z81J_MzXGtqa7RsCFJ=n$9>>{5Ks4TC%fOHUdqIA%ec$ow5ozjAASBWq|W~xl4~c*PgTepu`ybKCGj4x!V6#EfM+;qTKCSP6to4i;eE$QF6<)X)-~74Lwn!f;J9r0!H)MoDzAzBbThucbi=RS | 领域事件集合 | + +**关系**:所有其他实体类都继承自 EntityBase,共享这些基础属性。 + +#### 2. Document +| 属性名 | 类型 | 描述 | +| ------------------------- | ---------------------------- | -------------------------- | +| Id | long | 文档唯一标识符 | +| Title | string | 文档标题 | +| FileName | string | 文件名 | +| FilePath | string | 文件路径 | +| FileType | string | 文件类型 | +| FileSize | long | 文件大小 | +| AccessLevel | string | 访问级别 (internal/public) | +| Status | DocumentStatus | 文档状态 | +| VectorizationStatus | VectorizationStatus | 向量化状态 | +| VectorizationErrorMessage | string | 向量化错误信息 | +| VectorizedChunks | int | 已向量化的分块数 | +| TotalChunks | int | 总分块数 | +| LastVectorizedAt | DateTime? | 最后向量化时间 | +| Chunks | IReadOnlyList | 文档分块集合 | + +**关系**: +- 一个 Document 可以包含多个 DocumentChunk (一对多) +- Document 属于一个 KnowledgeBase +- Document 属于一个 User + +#### 3. DocumentChunk +| 属性名 | 类型 | 描述 | +| ---------- | ------ | -------------- | +| Id | long | 分块唯一标识符 | +| DocumentId | long | 所属文档ID | +| Content | string | 分块内容 | +| Position | int | 在文档中的位置 | +| TokenCount | int | Token数量 | + +**关系**: +- 一个 DocumentChunk 属于一个 Document (多对一) +- DocumentChunk 可以被多个 MessageReference 引用 + +#### 4. KnowledgeBase +| 属性名 | 类型 | 描述 | +| ----------- | --------------------- | ---------------- | +| Id | long | 知识库唯一标识符 | +| Name | string | 知识库名称 | +| Description | string | 知识库描述 | +| Documents | ICollection | 文档集合 | + +**关系**: +- 一个 KnowledgeBase 可以包含多个 Document (一对多) + +#### 5. VectorizationJob +| 属性名 | 类型 | 描述 | +| -------------- | ---------------------- | ---------------- | +| Id | long | 任务唯一标识符 | +| Name | string | 任务名称 | +| DocumentIds | List | 待处理文档ID列表 | +| Parameters | VectorizationParams | 向量化参数 | +| Status | VectorizationJobStatus | 任务状态 | +| ProcessedCount | int | 已处理文档数 | +| TotalCount | int | 总文档数 | +| ErrorMessage | string | 错误信息 | + +**关系**: +- 一个 VectorizationJob 可以处理多个 Document (通过 DocumentIds) +- 包含 VectorizationParams 作为参数配置 + +#### 6. VectorizationParams +| 属性名 | 类型 | 描述 | +| ------------ | ------- | ---------- | +| ModelName | string | 模型名称 | +| ChunkSize | int | 分块大小 | +| ChunkOverlap | int | 分块重叠度 | +| Separator | string? | 分隔符 | + +**关系**: +- 被 VectorizationJob 引用作为参数 + +#### 7. User +| 属性名 | 类型 | 描述 | +| ------------- | ------------------------ | -------------- | +| Id | long | 用户唯一标识符 | +| Username | string | 用户名 | +| Email | string | 邮箱 | +| PasswordHash | string | 密码哈希 | +| FullName | string | 全名 | +| IsActive | bool | 是否激活 | +| Documents | ICollection | 文档集合 | +| ChatHistories | ICollection | 聊天历史集合 | + +**关系**: +- 一个 User 可以拥有多个 Document (一对多) +- 一个 User 可以有多个 ChatHistory (一对多) + +#### 8. ChatHistory +| 属性名 | 类型 | 描述 | +| --------- | ------------------------ | ------------------ | +| Id | long | 聊天历史唯一标识符 | +| SessionId | string | 会话ID | +| UserId | long | 所属用户ID | +| User | User | 所属用户 | +| Messages | ICollection | 消息集合 | + +**关系**: +- 一个 ChatHistory 属于一个 User (多对一) +- 一个 ChatHistory 可以包含多个 ChatMessage (一对多) + +#### 9. ChatMessage +| 属性名 | 类型 | 描述 | +| ------------- | ----------------------------- | -------------- | +| Id | long | 消息唯一标识符 | +| ChatHistoryId | long | 所属聊天历史ID | +| ChatHistory | ChatHistory | 所属聊天历史 | +| Content | string | 消息内容 | +| IsUserMessage | bool | 是否用户消息 | +| Timestamp | DateTime | 时间戳 | +| References | ICollection | 引用集合 | + +**关系**: +- 一个 ChatMessage 属于一个 ChatHistory (多对一) +- 一个 ChatMessage 可以引用多个 DocumentChunk (通过 MessageReference) + +#### 10. MessageReference +| 属性名 | 类型 | 描述 | +| --------------- | ------------- | -------------- | +| Id | long | 引用唯一标识符 | +| ChatMessageId | long | 所属消息ID | +| ChatMessage | ChatMessage | 所属消息 | +| DocumentChunkId | long | 文档分块ID | +| DocumentChunk | DocumentChunk | 文档分块 | +| RelevanceScore | double | 相关度分数 | + +**关系**: +- 连接 ChatMessage 和 DocumentChunk 的多对多关系中间表 +- 一个 ChatMessage 可以有多个 MessageReference +- 一个 DocumentChunk 可以被多个 MessageReference 引用 + +#### 11. 枚举类型 +- **DocumentStatus**:文档状态(如 Pending, Processing, Completed, Failed 等) +- **VectorizationStatus**:文档向量化状态(如 NotVectorized, Vectorizing, Vectorized, Failed 等) +- **VectorizationJobStatus**:向量化任务状态(如 Pending, Processing, Completed, Failed 等) + +### 实体关系图 +``` +User 1--* Document +User 1--* ChatHistory +KnowledgeBase 1--* Document +Document 1--* DocumentChunk +ChatHistory 1--* ChatMessage +ChatMessage *--* DocumentChunk (通过 MessageReference) +VectorizationJob *--* Document (通过 DocumentIds) +``` + +以上详细描述了每个实体类的属性及其之间的关系,展示了系统的数据模型结构。 + \ No newline at end of file diff --git a/backend/RAG.Api/Files/ecbb0685-2c4b-495a-b678-b687df0993cd_0864345678.docx b/backend/RAG.Api/Files/ecbb0685-2c4b-495a-b678-b687df0993cd_0864345678.docx new file mode 100644 index 0000000..e69de29 diff --git a/backend/RAG.Api/Files/f19b4e49-43c4-4b6b-ac0c-0c7b7ada1ad5_0864345678.docx b/backend/RAG.Api/Files/f19b4e49-43c4-4b6b-ac0c-0c7b7ada1ad5_0864345678.docx new file mode 100644 index 0000000..e69de29 diff --git "a/backend/RAG.Api/Files/fd1b88c4-d836-4662-bbea-6702d6bead75_\346\226\260\345\273\272 \346\226\207\346\234\254\346\226\207\346\241\243.txt" "b/backend/RAG.Api/Files/fd1b88c4-d836-4662-bbea-6702d6bead75_\346\226\260\345\273\272 \346\226\207\346\234\254\346\226\207\346\241\243.txt" new file mode 100644 index 0000000..e69de29 diff --git a/backend/RAG.Api/Middleware/ExceptionMiddleware.cs b/backend/RAG.Api/Middleware/ExceptionMiddleware.cs index 468d59c..08687da 100644 --- a/backend/RAG.Api/Middleware/ExceptionMiddleware.cs +++ b/backend/RAG.Api/Middleware/ExceptionMiddleware.cs @@ -32,23 +32,30 @@ namespace RAG.Api.Middleware private async Task HandleExceptionAsync(HttpContext context, Exception exception) { + // 获取环境信息 + var env = context.RequestServices.GetRequiredService(); + var response = exception switch { ValidationException ex => new ErrorResponse { Code = "VALIDATION_ERROR", Message = "输入验证失败", - Details = ex.Errors + Details = ex.Errors, + StackTrace = env.IsDevelopment() ? exception.StackTrace : null }, NotFoundException ex => new ErrorResponse { Code = "NOT_FOUND", - Message = ex.Message + Message = ex.Message, + StackTrace = env.IsDevelopment() ? exception.StackTrace : null }, _ => new ErrorResponse { Code = "INTERNAL_ERROR", - Message = "服务器内部错误" + Message = env.IsDevelopment() ? exception.Message : "服务器内部错误", + Details = env.IsDevelopment() ? exception.InnerException?.Message : null, + StackTrace = env.IsDevelopment() ? exception.StackTrace : null } }; @@ -73,6 +80,7 @@ namespace RAG.Api.Middleware public string Code { get; set; } public string Message { get; set; } public object Details { get; set; } + public string StackTrace { get; set; } } public class ValidationException : Exception diff --git a/backend/RAG.Api/Program.cs b/backend/RAG.Api/Program.cs index 42516a4..bf32d0b 100644 --- a/backend/RAG.Api/Program.cs +++ b/backend/RAG.Api/Program.cs @@ -51,6 +51,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // 注册向量任务仓储 builder.Services.AddScoped, RAG.Infrastructure.Repositories.Repository>(); diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index e56cff9..0adef15 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -196,3 +196,5 @@ GET {{url}}/weatherforecast/ Accept: application/json ### +GET {{url}}/api/auth/User +Accept: application/json diff --git a/backend/RAG.Application/Services/DocumentProcessors/MarkdownDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/MarkdownDocumentProcessor.cs new file mode 100644 index 0000000..d87496f --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/MarkdownDocumentProcessor.cs @@ -0,0 +1,42 @@ +using System.Text; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; + +namespace RAG.Application.Services.DocumentProcessors +{ + public class MarkdownDocumentProcessor : BaseDocumentProcessor + { + public override string[] SupportedFileTypes => new[] { ".md" }; + + public MarkdownDocumentProcessor(IProgressService progressService) : base(progressService) + { + } + + public override async Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey) + { + var operationId = progressKey; + ReportProgress(operationId, 0, "开始处理Markdown文档"); + + // 使用StreamReader读取Markdown文件内容 + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) + { + string content = await reader.ReadToEndAsync(); + + ReportProgress(operationId, 100, "Markdown文档处理完成"); + + return new DocumentContentDto + { + Content = content, + Metadata = new DocumentMetadataDto + { + FileName = fileName, + FileType = ".md", + PageCount = 1 // Markdown通常不分页,设为1 + } + }; + } + } + } +} \ No newline at end of file diff --git a/backend/RAG.Application/Services/DocumentProcessors/TextDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/TextDocumentProcessor.cs new file mode 100644 index 0000000..7ea7645 --- /dev/null +++ b/backend/RAG.Application/Services/DocumentProcessors/TextDocumentProcessor.cs @@ -0,0 +1,42 @@ +using System.Text; +using System.Threading.Tasks; +using RAG.Application.DTOs; +using RAG.Application.Interfaces; +using RAG.Application.Services.DocumentProcessors; + +namespace RAG.Application.Services.DocumentProcessors +{ + public class TextDocumentProcessor : BaseDocumentProcessor + { + public override string[] SupportedFileTypes => new[] { ".txt" }; + + public TextDocumentProcessor(IProgressService progressService) : base(progressService) + { + } + + public override async Task ExtractContentAsync(Stream fileStream, string fileName, string progressKey) + { + var operationId = progressKey; + ReportProgress(operationId, 0, "开始处理文本文件"); + + // 使用StreamReader读取文本文件内容 + using (var reader = new StreamReader(fileStream, Encoding.UTF8)) + { + string content = await reader.ReadToEndAsync(); + + ReportProgress(operationId, 100, "文本文件处理完成"); + + return new DocumentContentDto + { + Content = content, + Metadata = new DocumentMetadataDto + { + FileName = fileName, + FileType = ".txt", + PageCount = 1 // 文本文件通常不分页,设为1 + } + }; + } + } + } +} \ No newline at end of file -- Gitee From f989e326f6b1e6d6a809eefa846ae684a91b1d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Mon, 11 Aug 2025 08:13:13 +0800 Subject: [PATCH 25/31] =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=EF=BC=8C=E7=99=BB=E5=BD=95=EF=BC=8C=E7=94=A8=E6=88=B7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 0adef15..3f4f7df 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -195,6 +195,35 @@ Accept: application/json GET {{url}}/weatherforecast/ Accept: application/json -### +### 查询全部用户 GET {{url}}/api/auth/User Accept: application/json + + +### 查询用户 +GET {{url}}/api/auth/User/1 +Accept: application/json + + +### 注册 +POST {{url}}/api/auth/register +Content-Type: application/json + +{ + + + "Username": "", + "password": "", + "Email": "", + "telephone": "telephone" + +}; +###登录 +POST {{url}}/api/auth/login +Content-Type: application/json + +{ + "username": "", + "password": "" +} + -- Gitee From e857618e1393406d318fded7a8b27dae711af2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Tue, 12 Aug 2025 11:10:23 +0800 Subject: [PATCH 26/31] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E8=BD=AC=E6=8D=A2=E4=B8=BA=E5=90=91=E9=87=8F?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=88=B0=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/UserSayController.cs | 61 ++ backend/RAG.Api/RAG.Api.http | 27 + .../Interfaces/EmbeddingResponse.cs | 42 ++ .../RAG.Application/RAG.Application.csproj | 3 + .../ServiceCollectionExtensions.cs | 1 + .../Services/UserSayService.cs | 108 ++++ backend/RAG.Domain/Entities/UserSay.cs | 26 + .../Repositories/IUserSayRepository.cs | 22 + .../Data/ApplicationDbContext.cs | 49 ++ .../20250811022154_UserSay.Designer.cs | 531 ++++++++++++++++ .../Migrations/20250811022154_UserSay.cs | 22 + .../20250811064737_UserSay1.Designer.cs | 531 ++++++++++++++++ .../Migrations/20250811064737_UserSay1.cs | 22 + .../20250811070501_UserSay2.Designer.cs | 531 ++++++++++++++++ .../Migrations/20250811070501_UserSay2.cs | 22 + .../20250811083039_UserSay3.Designer.cs | 567 +++++++++++++++++ .../Migrations/20250811083039_UserSay3.cs | 46 ++ .../20250811083337_UserSay4.Designer.cs | 568 ++++++++++++++++++ .../Migrations/20250811083337_UserSay4.cs | 24 + .../20250811083522_UserSay5.Designer.cs | 568 ++++++++++++++++++ .../Migrations/20250811083522_UserSay5.cs | 22 + .../20250811092752_UserSay6.Designer.cs | 568 ++++++++++++++++++ .../Migrations/20250811092752_UserSay6.cs | 22 + .../ApplicationDbContextModelSnapshot.cs | 37 ++ .../Repositories/UserSayREpository.cs | 67 +++ .../Repositories/UserSayREpositoryTests.cs | 0 .../ServiceCollectionExtensions.cs | 2 + 27 files changed, 4489 insertions(+) create mode 100644 backend/RAG.Api/Controllers/UserSayController.cs create mode 100644 backend/RAG.Application/Interfaces/EmbeddingResponse.cs create mode 100644 backend/RAG.Application/Services/UserSayService.cs create mode 100644 backend/RAG.Domain/Entities/UserSay.cs create mode 100644 backend/RAG.Domain/Repositories/IUserSayRepository.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs create mode 100644 backend/RAG.Infrastructure/Repositories/UserSayREpository.cs create mode 100644 backend/RAG.Infrastructure/Repositories/UserSayREpositoryTests.cs diff --git a/backend/RAG.Api/Controllers/UserSayController.cs b/backend/RAG.Api/Controllers/UserSayController.cs new file mode 100644 index 0000000..a32760e --- /dev/null +++ b/backend/RAG.Api/Controllers/UserSayController.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc; +using Org.BouncyCastle.Ocsp; +using RAG.Application.Services; +using RAG.Domain.Entities; + +namespace RAG.Api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class UserSayController : ControllerBase + { + + private readonly UserSayService _userSay; + + public UserSayController(UserSayService userSay) + { + _userSay = userSay; + } + [HttpPost("Add")] + public async Task Add([FromBody] UsersayRequest req) + { + + + try + + + { + + + await _userSay.AddUserAsync(req.Usersay, req.Message); + + + } + + + catch (Exception ex) + + + { + + + return BadRequest(new { message = "服务器错误!" + ex.Message }); + + + } + + + return Ok(new { msg = "数据添加成功!" }); + + } + + + + } + public class UsersayRequest + { + + public required string Usersay { get; set; } + public required string Message { get; set; } + } +} \ No newline at end of file diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 3f4f7df..280e587 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -225,5 +225,32 @@ Content-Type: application/json { "username": "", "password": "" +}; + + +###多模态向量化 +POST https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings HTTP/1.1 +Authorization: Bearer sk-ddad3c79867b422c99fe0ed3430ee32c +Content-Type: application/json + +{ + "model":"text-embedding-v4", + "input":"111232224", + "dimensions":"1024", + "encoding_format":"float" + + +} + + +### 用户输入转化为向量 +POST {{url}}/api/UserSay/Add +Content-Type: application/json + +{ + "Usersay":"你好,", + "Message":"11111" + + } diff --git a/backend/RAG.Application/Interfaces/EmbeddingResponse.cs b/backend/RAG.Application/Interfaces/EmbeddingResponse.cs new file mode 100644 index 0000000..70a272f --- /dev/null +++ b/backend/RAG.Application/Interfaces/EmbeddingResponse.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +public class EmbeddingResponse +{ + [JsonPropertyName("object")] + public string Object { get; set; } + + [JsonPropertyName("data")] + public EmbeddingData[] Data { get; set; } + + [JsonPropertyName("model")] + public string Model { get; set; } + + [JsonPropertyName("usage")] + public UsageInfo Usage { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } +} + +public class EmbeddingData +{ + [JsonPropertyName("object")] + public string Object { get; set; } + + [JsonPropertyName("embedding")] + public float[] Embedding { get; set; } + + [JsonPropertyName("index")] + public int Index { get; set; } +} + +public class UsageInfo +{ + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} \ No newline at end of file diff --git a/backend/RAG.Application/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj index c59126e..780f723 100644 --- a/backend/RAG.Application/RAG.Application.csproj +++ b/backend/RAG.Application/RAG.Application.csproj @@ -11,6 +11,9 @@ + + + diff --git a/backend/RAG.Application/ServiceCollectionExtensions.cs b/backend/RAG.Application/ServiceCollectionExtensions.cs index 58b699d..701e723 100644 --- a/backend/RAG.Application/ServiceCollectionExtensions.cs +++ b/backend/RAG.Application/ServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ public static class ServiceCollectionExtensions { services.AddScoped(); + services.AddScoped(); diff --git a/backend/RAG.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs new file mode 100644 index 0000000..09e0e1a --- /dev/null +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -0,0 +1,108 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; + +namespace RAG.Application.Services; + +public class UserSayService +{ + + private readonly IUserSayRepository _userSay; + public UserSayService(IUserSayRepository userSay) + { + + _userSay = userSay; + } + public async Task AddUserAsync(string Usersay,string Message) + { + + using var client = new HttpClient(); + + const string apiKey = "sk-ddad3c79867b422c99fe0ed3430ee32c"; // 替换为你的API Key + const string apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; + + //构建请求体 + var requestBody = new + { + model = "text-embedding-v4", + input =Usersay, + dimensions = 1024, // 注意这里是整数类型,不是字符串 + encoding_format = "float" + }; + try + { + // 序列化请求体 + var jsonContent = JsonSerializer.Serialize(requestBody); + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", apiKey); + + // 发送POST请求 + var response = await client.PostAsync(apiUrl, content); + + // 处理响应 + if (response.IsSuccessStatusCode) + { + var responseJson = await response.Content.ReadAsStringAsync(); + Console.WriteLine("请求成功!响应结果:"); + Console.WriteLine(responseJson); + // 假设jsonResponse是API返回的JSON字符串 + +try +{ + // 反序列化JSON + var response1 = JsonSerializer.Deserialize(responseJson); + + // 检查数据是否存在 + if (response1?.Data?.Length > 0) + { + // 获取第一个embedding向量 + float[] embeddingVector = response1.Data[0].Embedding; + + Console.WriteLine($"向量维度: {embeddingVector.Length}"); + Console.WriteLine("前5个维度值:"); + for (int i = 0; i < 5; i++) + { + Console.WriteLine(embeddingVector[i]); + await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector)); + + } + + // 完整向量可以用于后续处理 + // 例如: 存储到数据库或进行向量计算 + } + else + { + Console.WriteLine("响应中没有包含embedding数据"); + } +} +catch (JsonException ex) +{ + Console.WriteLine($"JSON解析错误: {ex.Message}"); +} + + + + + } + else + { + Console.WriteLine($"请求失败,状态码:{(int)response.StatusCode} ({response.ReasonPhrase})"); + var errorDetails = await response.Content.ReadAsStringAsync(); + Console.WriteLine("错误详情:" + errorDetails); + } + } + catch (Exception ex) + { + Console.WriteLine($"发生异常:{ex.Message}"); + } + //新增用户输入文本,转化为向量 + } + + } + + + + diff --git a/backend/RAG.Domain/Entities/UserSay.cs b/backend/RAG.Domain/Entities/UserSay.cs new file mode 100644 index 0000000..722f283 --- /dev/null +++ b/backend/RAG.Domain/Entities/UserSay.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace RAG.Domain.Entities +{ + public class UserSay : EntityBase + { + + + //用户发送的消息 + public string Usersay { get; private set; } + //用户发送消息的向量化结果 + public float[] UersayVector { get; private set; } + public string Message { get; private set; } + + public UserSay(string usersay, string message, float[] uersayVector) + { + this.Usersay = usersay; + this.Message = message; + this.UersayVector = uersayVector; + } + + + + + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Repositories/IUserSayRepository.cs b/backend/RAG.Domain/Repositories/IUserSayRepository.cs new file mode 100644 index 0000000..b7684fb --- /dev/null +++ b/backend/RAG.Domain/Repositories/IUserSayRepository.cs @@ -0,0 +1,22 @@ +using RAG.Domain.Entities; + +namespace RAG.Domain.Repositories +{ + public interface IUserSayRepository + { + + + + + //获取用户id + Task GetSayUserAsync(long id); + //添加文本数据,将数据向量化 + Task AddUserSayAsync(UserSay userSay); + //删除数据 + Task DeleteUserAsync(long id); + //更新数据 + Task UpdateAsync(UserSay userSay); + Task > SelAllAsync(); + + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index 65211a2..4e1c48b 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -1,4 +1,7 @@ + +using System.Text.Json; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using RAG.Domain.Entities; namespace RAG.Infrastructure.Data @@ -10,6 +13,7 @@ namespace RAG.Infrastructure.Data // 核心数据表 public DbSet Users { get; set; } + public DbSet UserSays { get; set; } public DbSet Documents { get; set; } public DbSet DocumentChunks { get; set; } public DbSet ChatHistories { get; set; } @@ -33,6 +37,51 @@ namespace RAG.Infrastructure.Data modelBuilder.Entity().Ignore(cm => cm.DomainEvents); modelBuilder.Entity().Ignore(mr => mr.DomainEvents); modelBuilder.Entity().Ignore(vj => vj.DomainEvents); + modelBuilder.Entity().Ignore(vj => vj.DomainEvents); + + + // modelBuilder.Entity(entity => + // { + // entity.HasKey(u => u.Id); + // entity.Property(u => u.Usersay).IsRequired().HasMaxLength(50).HasColumnType("text"); + // entity.Property(u => u.Message).IsRequired().HasMaxLength(100).HasColumnType("text"); + // entity.Property(x => x.UersayVector) + // .HasColumnType("vector(1024)") // OpenAI维度 + // .HasConversion( + // v => JsonSerializer.Serialize(v, null), + // v => JsonSerializer.Deserialize(v, null)); // 索引 + // entity.HasIndex(u => u.UersayVector).IsUnique(); + // entity.HasIndex(u => u.Usersay).IsUnique(); + // }); + + + + modelBuilder.HasPostgresExtension("vector"); // 确保迁移包含扩展创建 + + + modelBuilder.Entity(entity => + { + entity.HasKey(u => u.Id); + + entity.Property(u => u.Usersay) + .IsRequired() + .HasMaxLength(50); + + entity.Property(u => u.Message) + .IsRequired() + .HasMaxLength(100); + + entity.Property(x => x.UersayVector) + .HasColumnType("vector(1024)") + .HasConversion( + v => System.Text.Json.JsonSerializer.Serialize(v, (JsonSerializerOptions)null), + v => System.Text.Json.JsonSerializer.Deserialize(v, (JsonSerializerOptions)null)); + + entity.HasIndex(u => u.Usersay).IsUnique(); + }); + + + // 配置User实体 modelBuilder.Entity(entity => diff --git a/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs new file mode 100644 index 0000000..950f9d6 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs @@ -0,0 +1,531 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811022154_UserSay")] + partial class UserSay + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs b/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs new file mode 100644 index 0000000..3b08974 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs new file mode 100644 index 0000000..56a0795 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs @@ -0,0 +1,531 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811064737_UserSay1")] + partial class UserSay1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs b/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs new file mode 100644 index 0000000..dafd086 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs new file mode 100644 index 0000000..8c145f5 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs @@ -0,0 +1,531 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811070501_UserSay2")] + partial class UserSay2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs b/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs new file mode 100644 index 0000000..d02a92c --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs new file mode 100644 index 0000000..40e3423 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs @@ -0,0 +1,567 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811083039_UserSay3")] + partial class UserSay3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Usersay") + .IsUnique(); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs b/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs new file mode 100644 index 0000000..69995b3 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserSays", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Usersay = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + UersayVector = table.Column(type: "vector(1024)", nullable: false), + Message = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserSays", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserSays_Usersay", + table: "UserSays", + column: "Usersay", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserSays"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs new file mode 100644 index 0000000..7abf8d7 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs @@ -0,0 +1,568 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811083337_UserSay4")] + partial class UserSay4 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Usersay") + .IsUnique(); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs b/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs new file mode 100644 index 0000000..c89fda2 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay4 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:vector", ",,"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .OldAnnotation("Npgsql:PostgresExtension:vector", ",,"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs new file mode 100644 index 0000000..1ad34bb --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs @@ -0,0 +1,568 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811083522_UserSay5")] + partial class UserSay5 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Usersay") + .IsUnique(); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs b/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs new file mode 100644 index 0000000..6caa032 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay5 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs new file mode 100644 index 0000000..b11618d --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs @@ -0,0 +1,568 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250811092752_UserSay6")] + partial class UserSay6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Usersay") + .IsUnique(); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs b/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs new file mode 100644 index 0000000..1afefcc --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay6 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 30b5de7..85e74e7 100644 --- a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -21,6 +21,7 @@ namespace RAG.Infrastructure.Migrations .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => @@ -309,6 +310,42 @@ namespace RAG.Infrastructure.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Usersay") + .IsUnique(); + + b.ToTable("UserSays"); + }); + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => { b.Property("Id") diff --git a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs new file mode 100644 index 0000000..add19ca --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore; +using RAG.Domain.Entities; +using RAG.Domain.Repositories; +using RAG.Infrastructure.Data; + +namespace RAG.Infrastructure.Repositories +{ + public class UserSayRepository : IUserSayRepository + { + + + protected readonly ApplicationDbContext _context; + protected readonly DbSet _dbSet; + + public UserSayRepository(ApplicationDbContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _dbSet = _context.Set(); + } + public async Task AddUserSayAsync(UserSay userSay) + { + + //新增用户输入文本,转化为向量 + await _dbSet.AddAsync(userSay); + + + } + + public async Task DeleteUserAsync(long id) +{ + // 1. 异步查找用户 + var user = await _dbSet.FindAsync(id); // 注意添加 await + + // 2. 检查用户是否存在 + if (user == null) + { + throw new KeyNotFoundException($"未找到 ID 为 {id} 的用户"); + } + + // 3. 删除用户 + _dbSet.Remove(user); // 同步操作,无需 await + await _context.SaveChangesAsync(); // 异步保存更改 +} + + + public async Task GetSayUserAsync(long id) + { + //获取单个用户输入文本,转化为向量 + return await _dbSet.FindAsync(id); + + } + + public async Task> SelAllAsync() + { + //获取全部用户输入文本,转化为向量 + return await _dbSet.ToListAsync(); + + } + + public Task UpdateAsync(UserSay userSay) + { + //修改用户输入文本,转化为向量 + _dbSet.Update(userSay); + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Repositories/UserSayREpositoryTests.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpositoryTests.cs new file mode 100644 index 0000000..e69de29 diff --git a/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs b/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs index ec27831..147a842 100644 --- a/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs +++ b/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs @@ -19,6 +19,8 @@ public static class ServiceCollectionExtensions services.AddDbContext(options => options.UseNpgsql(configuration.GetConnectionString("pgsql"))); services.AddScoped(typeof(IERepository<>), typeof(ERepository<>)); + services.AddScoped(typeof(IUserSayRepository<>), typeof(UserSayRepository<>)); + services.AddScoped(typeof(IPasswordHasher), typeof(PasswordHasher)); services.AddSingleton(); -- Gitee From 1272eb0338c48afba16715cc165f1f21ccda6e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E9=9B=A8=E6=99=B4?= <1207713896@qq.com> Date: Wed, 13 Aug 2025 16:10:45 +0800 Subject: [PATCH 27/31] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=AE=8C=E6=88=90?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=88=B0=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/DocumentController.cs | 17 +- .../Controllers/DocumentsController.cs | 13 +- ...bec1bf_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes ...4\347\244\272\346\226\207\347\250\277.ppt" | Bin 20992 -> 0 bytes ...55f33b_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...fa8aca_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...cf2560_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...56a487_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...2fbe7-14c5-49c4-8904-9bc293a594a0_test.txt | 1 - ...a1a26-588a-4ddc-9613-cf97d5f447c3_test.txt | 1 - ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes ...e282d2_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...ef7d08_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes ...1516ee_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...5712dc_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...33fd45_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...2a7ebf_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...6969b7_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes ...\346\234\252\345\221\275\345\220\2151.pdf" | Bin 1243 -> 0 bytes ...591c52_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...23\347\261\273\350\256\276\350\256\241.md" | 164 ----- ...c592c-97b1-4efa-8284-5c760bbc32db_test.txt | 1 - ...b615ea_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...4a5da7_963c1d70789c87eefcdce5cb8d873b1.png | Bin 11973 -> 0 bytes ...c4b-495a-b678-b687df0993cd_0864345678.docx | 0 ...3c4-4b6b-ac0c-0c7b7ada1ad5_0864345678.docx | 0 ...7\346\234\254\346\226\207\346\241\243.txt" | 0 backend/RAG.Api/Program.cs | 4 +- backend/RAG.Api/RAG.Api.http | 57 +- backend/RAG.Api/appsettings.json | 4 +- .../Interfaces/IDocumentService.cs | 4 +- .../Services/DocumentService.cs | 301 ++++------ .../Services/UserSayService.cs | 5 +- backend/RAG.Domain/Entities/Document.cs | 10 +- backend/RAG.Domain/Entities/UserSay.cs | 2 + .../Data/ApplicationDbContext.cs | 10 +- .../FileStorage/DatabaseFileStorageService.cs | 51 ++ ...dVectorizationFieldsToDocument.Designer.cs | 445 -------------- ...074151_AddVectorizationFieldsToDocument.cs | 334 ---------- ...252_UpdateUserTableStringTypes.Designer.cs | 445 -------------- ...250806075252_UpdateUserTableStringTypes.cs | 98 --- ...1942_AddVectorizationJobEntity.Designer.cs | 525 ---------------- ...0250806091942_AddVectorizationJobEntity.cs | 58 -- .../20250807001255_auth2.Designer.cs | 531 ---------------- .../Migrations/20250807001255_auth2.cs | 113 ---- .../20250811022154_UserSay.Designer.cs | 531 ---------------- .../Migrations/20250811022154_UserSay.cs | 22 - .../20250811064737_UserSay1.Designer.cs | 531 ---------------- .../Migrations/20250811064737_UserSay1.cs | 22 - .../20250811070501_UserSay2.Designer.cs | 531 ---------------- .../Migrations/20250811070501_UserSay2.cs | 22 - .../20250811083039_UserSay3.Designer.cs | 567 ----------------- .../Migrations/20250811083039_UserSay3.cs | 46 -- .../20250811083522_UserSay5.Designer.cs | 568 ------------------ .../Migrations/20250811083522_UserSay5.cs | 22 - .../20250811092752_UserSay6.Designer.cs | 568 ------------------ .../Migrations/20250811092752_UserSay6.cs | 22 - ...moveUsersayUniqueConstraintV2.Designer.cs} | 11 +- ...075416_RemoveUsersayUniqueConstraintV2.cs} | 19 +- .../ApplicationDbContextModelSnapshot.cs | 7 +- .../RAG.Infrastructure.csproj | 4 + .../Repositories/UserSayREpository.cs | 2 +- 65 files changed, 260 insertions(+), 6429 deletions(-) delete mode 100644 backend/RAG.Api/Files/012bdc47-067f-42c9-9aa0-935006bec1bf_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 "backend/RAG.Api/Files/0232ef8e-72b8-49c2-8fcc-81eda2167b8d_\346\234\252\345\221\275\345\220\2151.pdf" delete mode 100644 "backend/RAG.Api/Files/084f8a6b-a806-465b-99f9-61d4398869e5_\346\226\260\345\273\272 PPT \346\274\224\347\244\272\346\226\207\347\250\277.ppt" delete mode 100644 backend/RAG.Api/Files/0abd8ca9-d0f3-4d41-9ae4-2bfb9555f33b_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/162dfd2a-80e6-4ae2-810e-1cb5b3fa8aca_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/229e8727-50b1-4cbe-9e0d-270cb1cf2560_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/2fcbacff-d38a-41f7-8357-7c860656a487_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/3262fbe7-14c5-49c4-8904-9bc293a594a0_test.txt delete mode 100644 backend/RAG.Api/Files/431a1a26-588a-4ddc-9613-cf97d5f447c3_test.txt delete mode 100644 "backend/RAG.Api/Files/505c2bb3-0b8b-488c-ab2a-d4d72d1089be_\346\234\252\345\221\275\345\220\2151.pdf" delete mode 100644 backend/RAG.Api/Files/54bde012-2d46-4a9f-b67b-dbc07ae282d2_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/5746168e-1832-4556-8385-d8dd21ef7d08_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 "backend/RAG.Api/Files/588d5e1e-ec83-4d01-9dcf-e75acdd7aecf_\346\234\252\345\221\275\345\220\2151.pdf" delete mode 100644 backend/RAG.Api/Files/5c35a45b-e55e-4abe-bd9b-e5d1c71516ee_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/60d65403-5750-457f-b947-7c71b45712dc_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/73c79518-a1bb-4e3c-a12a-3578ae33fd45_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/82b608fc-3ccc-4ad0-962c-c9f4fd2a7ebf_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/874b4073-c830-447e-a432-5939946969b7_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 "backend/RAG.Api/Files/87750046-e4b0-4269-888e-0dfd41a4a415_\346\234\252\345\221\275\345\220\2151.pdf" delete mode 100644 "backend/RAG.Api/Files/87eece55-1821-4937-879b-be2db857a9fd_\346\234\252\345\221\275\345\220\2151.pdf" delete mode 100644 backend/RAG.Api/Files/9699d9e7-c206-43b9-a525-ff1aea591c52_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 "backend/RAG.Api/Files/9b09b318-173e-4e3f-b14d-b7b819b588a7_\345\256\236\344\275\223\347\261\273\350\256\276\350\256\241.md" delete mode 100644 backend/RAG.Api/Files/bbcc592c-97b1-4efa-8284-5c760bbc32db_test.txt delete mode 100644 backend/RAG.Api/Files/c7be2844-4774-4945-9245-c614a5b615ea_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/c93649d2-9f04-4dc0-b417-0861434a5da7_963c1d70789c87eefcdce5cb8d873b1.png delete mode 100644 backend/RAG.Api/Files/ecbb0685-2c4b-495a-b678-b687df0993cd_0864345678.docx delete mode 100644 backend/RAG.Api/Files/f19b4e49-43c4-4b6b-ac0c-0c7b7ada1ad5_0864345678.docx delete mode 100644 "backend/RAG.Api/Files/fd1b88c4-d836-4662-bbea-6702d6bead75_\346\226\260\345\273\272 \346\226\207\346\234\254\346\226\207\346\241\243.txt" create mode 100644 backend/RAG.Infrastructure/FileStorage/DatabaseFileStorageService.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs delete mode 100644 backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs rename backend/RAG.Infrastructure/Migrations/{20250811083337_UserSay4.Designer.cs => 20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs} (98%) rename backend/RAG.Infrastructure/Migrations/{20250811083337_UserSay4.cs => 20250813075416_RemoveUsersayUniqueConstraintV2.cs} (38%) diff --git a/backend/RAG.Api/Controllers/DocumentController.cs b/backend/RAG.Api/Controllers/DocumentController.cs index 2ef2000..eb58ddc 100644 --- a/backend/RAG.Api/Controllers/DocumentController.cs +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -33,8 +33,18 @@ private readonly IDocumentService _documentService; try { - var result = await _documentService.UploadDocumentAsync(uploadDto); - return Ok(result); + // 获取文件流、文件名、文件类型和文件大小 + using (var fileStream = uploadDto.File.OpenReadStream()) + { + var fileName = uploadDto.File.FileName; + // 获取文件扩展名作为文件类型 + var fileType = Path.GetExtension(fileName).ToLower(); + var fileSize = uploadDto.File.Length; + var cancellationToken = HttpContext.RequestAborted; + + var result = await _documentService.UploadDocumentAsync(uploadDto, fileStream, fileName, fileType, fileSize, cancellationToken); + return Ok(result); + } } catch (ArgumentException ex) { @@ -66,7 +76,8 @@ private readonly IDocumentService _documentService; try { - var results = await _documentService.UploadDocumentsAsync(uploadDtos); + var cancellationToken = HttpContext.RequestAborted; + var results = await _documentService.UploadDocumentsAsync(uploadDtos, cancellationToken); return Ok(new { documents = results }); } catch (ArgumentException ex) diff --git a/backend/RAG.Api/Controllers/DocumentsController.cs b/backend/RAG.Api/Controllers/DocumentsController.cs index 674a3e9..97eddfc 100644 --- a/backend/RAG.Api/Controllers/DocumentsController.cs +++ b/backend/RAG.Api/Controllers/DocumentsController.cs @@ -38,8 +38,17 @@ namespace RAG.Api.Controllers AccessLevel = accessLevel }; - var result = await _documentService.UploadDocumentAsync(uploadDto); - return CreatedAtAction(nameof(GetDocument), new { id = result.Id }, result); + // 获取文件流、文件名、文件类型和文件大小 + using (var fileStream = file.OpenReadStream()) + { + var fileName = file.FileName; + var fileType = file.ContentType; + var fileSize = file.Length; + var cancellationToken = HttpContext.RequestAborted; + + var result = await _documentService.UploadDocumentAsync(uploadDto, fileStream, fileName, fileType, fileSize, cancellationToken); + return CreatedAtAction(nameof(GetDocument), new { id = result.Id }, result); + } } ///

diff --git a/backend/RAG.Api/Files/012bdc47-067f-42c9-9aa0-935006bec1bf_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/012bdc47-067f-42c9-9aa0-935006bec1bf_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 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 diff --git "a/backend/RAG.Api/Files/084f8a6b-a806-465b-99f9-61d4398869e5_\346\226\260\345\273\272 PPT \346\274\224\347\244\272\346\226\207\347\250\277.ppt" "b/backend/RAG.Api/Files/084f8a6b-a806-465b-99f9-61d4398869e5_\346\226\260\345\273\272 PPT \346\274\224\347\244\272\346\226\207\347\250\277.ppt" deleted file mode 100644 index ef8831a3d4bd6e596212124a879ca34a59c90f3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20992 zcmeHP4RBS(6+ZXgm*j-wkF!K6+d+&z(US7fr z3AMo8H|Or|*>lgHb9VRa-urfM^P%1?JAX0oZ;}>D#genhe986U4WtPwL_~6sVE$|} znIz5xAQ^;9$O7e3h2K1xBXik=5(TsjUfbi*uO2oABh5A*ClARI07H$;2K9H4pDhcK zF9L+ZBBa%Qqr1`;crv8NHccjAA3^%EG*327OAj&@IV9 z3oeld@tdvc{tBe1f<$!+vlxACk?LoDKK}GDF^v!qS_Xevd+EDSu-7ZhkqM|_j*(;99^9fJ~JDoe`gjHo#q&1qO_Zme3d>Pr23jImF1OltqHdFBJx z1HeAr6VMBA6@b$R$5n3tr*e+bK7hV}et`af0f4Ik0|A^*+VlTIQ1&^%b%5&uLjjx< z=o@YXd>(KUfNeGcFcL5dz&>;{U^Jiva0}p8z-@prfU$rt0KN$L65w{gmjR`KI{;C@ zIKZ8N@qh^c>d$aD(vy677kl|#1OLxCpskPRypf&%KW#gpW#6B*fAfjIU~q)x-v?;$9uVgv4Cpdj=!&z;WOS|; zP8(j>fq0qD_~J&S9|lwa=*i=Nn0Ma+Su2gmvz)VlP8+Rb5=bv_g{T9XAZ0er+#~@u zus~pezyg5<0t*Bd2rLj-pfeWW+jBnc@Tq26Sw%%29+UV4&$k2Qg9jj0j!3;BAT8f4 zSVma=n^d3{Y*|V$a`25HlmDc zLpMI(`cM&j7lY#gactgg z!Ny(!(>cI#|E~_-xW@63_pM#XjFLav{O7M&4%=KmFxYY=@1#oo)}DZP9QKpX!}*>C z3U^_bh`7A!zcID^!Z^5htwH2?yzW!yZK`p8fah+iy%TQPb9Z;1?Xh#!Em3v-k?mP4 z%>P0206$h3rc8BGE-Jh28su

J#pw9^wcjKtSJBG1C$S^E~NqE>byKT^*QW0!`u= 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 diff --git a/backend/RAG.Api/Files/54bde012-2d46-4a9f-b67b-dbc07ae282d2_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/54bde012-2d46-4a9f-b67b-dbc07ae282d2_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 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 diff --git a/backend/RAG.Api/Files/5c35a45b-e55e-4abe-bd9b-e5d1c71516ee_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/5c35a45b-e55e-4abe-bd9b-e5d1c71516ee_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 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 diff --git "a/backend/RAG.Api/Files/87eece55-1821-4937-879b-be2db857a9fd_\346\234\252\345\221\275\345\220\2151.pdf" "b/backend/RAG.Api/Files/87eece55-1821-4937-879b-be2db857a9fd_\346\234\252\345\221\275\345\220\2151.pdf" deleted file mode 100644 index 1e53172a688b1d8bc9e974c6aa799b17ad32eeea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1243 zcmZ{kO>5gQ7{`0q$s@4)F1my!l+}_f$?^z>#Ihl@T|*M~LI}l;mDDA%gRQW3pJE@N zm)*AWKEq(J^BC;B4>Bn)O_p}PSbFmO|4;mT9;w<9(F=6!TGdZqzx}chfFQrTu2_wR z{VJZxQon^PRSAgxr3-xDK3}M5n#mH-WyJ2p6TQK!P5*W-?I2e1G@sa=RE}gj8|MJQ zwc3(dqWvwCzNr)Bi%bDzpQK4S0Bo5#w3kjxZ{OdjoE=qjp|0|xR=0zEHj|kuSIc>v z-I`S)W0mHah?T5~ml$FSIb_fyn6e|t4h_7eeV-SJ@jA@%69aVx(W=q&`)qsQ1~fD?4gy`k=7p_Bh< zq`|+nHZB|MD4i-<*wIw;6>^j((ms{hL|p;w`F^Q1{+V@icl3w$;X!NoOvaa=C-Ik$ zcR!wge|pgP{p6?V={j#c6MVk%Vy+Nag>B3kUcXMDi!+TT6u>}D_6${=!fKlpDFy6+HZmW4fb{|7Xd+o0juYt(*h7aZh74FCWD diff --git a/backend/RAG.Api/Files/9699d9e7-c206-43b9-a525-ff1aea591c52_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/9699d9e7-c206-43b9-a525-ff1aea591c52_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 zcmcI~XIN8TwDNG^GYG^xi?~(jh2FG4w#Fp*IzgUINlV zq}R}Uoy7k;bHCiVb7$UXKIDOO_E}}Gz1Lp9wbqV&si8=AoAx#y9v+#p5=a{l4enm{SpUMHVhF0aI%8NNYO9PsAyf! z#-k?tNE8X(Ah}=U+hYhEzCmxy%oDxzAQfc#FckH8Y#9%emlBOM7V_Ojm{dDf^S3+h zrPk{$<>^@?jZ;!m3=c0K%*JON8|$7|1=hP*jnESY-CU|r7IzjfBo5NJ0Ve`0q0eSY0URWN6McOYK@Gu2 z6TH3)B1Hv3@Spr8NB!@~C%Gpl>{5Pma_jKMpkIDbs|{1ozO@~F+;zTep_Dd{t;kwkTKa--bCfZ$>Vv<2 z3F;Qtwoi|?ozMI4wVXaUF-R1VhE27MDXcBA6HOk}$%S!+9`|*8?MO^^V#acbEP)s? z=XP}`1D#=>dVehDEaokqEzR4cxN(owRm4WdAq{cDB1hZ@OX9sQ<(!kPt*zUWH@|lL z=&;@+4&omXcBKhcaeGL@NXKaLOa?n=s>Zlc9VK2HQ~o}_kh-0UU4fscFTRhM!`OM+ zI>yzkNU?T`Mfa80$<1%wEPY>P&~mv z@q;4$k9(BIm^9r;ntpsVbQ!5r?MfnVDaTin!7~e$3t7jcoW3 z6XU6-v(ho=s<;QRKtC0eGrzYVgKGNL@#s+TJb;t~GmiOquS;QWMmgP#(1%#J$(in?=tu0eR3VE*2#w@FgD3fSj8|;$mV7 zOM8!FJ|q_z@M?W|Vs7JAH!hH{@?)iArE8^Uh5o+7PAYAeX&N<@m3ddhn1KZ*eDd*h zyzLM14IP%A7boKjLwC_G!oe!#ycu)m2@Yg{lAUW+4`IEPuz>+lGbu-@KoVgyTQDl- z{SR_UOYdCS2Y$VNn{UQ7{kYg*;X4%_>`=4|`$kF$>v-Z}n$ow}@xaB?%hZlz{YNJk zi(VdzO+=0Kb&|PG%u{a*Y|%oLl&u1LD!(dL>`<~#MzQ4TXqT+F&6YiG_DhRL+ z27IYNqdGlbuVSN4p+>A|wd8m)JrsBF(qm`kS11Vt)8mQs+A!Pkvv--{%pta4F<&v? zFa#6U(iBia3ZyC+o{{0+aL0i5QHun_gGZHlLu)34&T~h9Jm!w(JQATdC#?BD0We9e z0SU{!8CgnRMjSFIKHEsxIZNJ?6Tb65<-?58DtctKq_yO=RJE!4H3&1WT*Z$my*@l|1bvB69nAI_a6;oKqwP12wO)6sUNpN1`%XYIq+3>!QnHuVR(E3`1ghYq;z`#?X^luMba7p*5{*X{CJOJsyM?n%V;)tW+GMqP7=F3>VP z#!?#Oc*c>5+p+th7^B4VK2clJ!ow8VG zSGiVp-lTeb#Vk%gc%hr#Zdns|bdbeA6nC{IYm}UB>ivHlWSm6*WTBJxb|N^cPd?)C4I@Gi*|z{ zCSWcwmmz~RkG#0VrUW1~;iiw1xU;Wwh{+H&CjPS1)^(ens{eS$w;--RI%}V>mZsLoCgR8uwg2t(pwlSuRqwN&V@F9@ z*G%!8&tvPBrl{V2Nn*Rb546i|`-?6{HJJOa`mkyYQ3A=EY_z5umL!N^cW5BZ`4UuA zn3x*}svG_%J)Sour1^8M==$T3K~wflgAbQSGKn8N67B!g_(_9nrK2-9km&D30xuKoSi}8|3jH-vfUq@ak^Hmler<7gq3c;H6)rzxnm~+>&_|spf+B z_2tzyYrdc+k9<#Ct(vm=s=hIH;`$bm(Xe~-u2#kQRsbYO$;@b6U?)n?gHZ`B#5$Gs zQo@;UEmTdAYV&3}{}Q05J?6fz_S7qPeG%wOen|a5gEXJqx{FagC6JY_?Hn)U7(bj1 zYf%?|aM;qAYf#u*w2NoRc~4#n(P+a_m>>xil++dp$+LV^?;~GKnZ%F3*7;tg`$_*% z+FDJ7NY8&Jryz;p@RBtt30TJR*;9MrRvP%oW=ESE`->DjGZ4$hT@(C z>w}&utNffcopzu=`UEV1GU6x@i1Z-#i|lGMO*3dY!Pa9mhx3hIoU7$SFyx6{l@NEx zEBPMGPfQ1<^AdFmK-PH!R%xnP&%rnL!90^YTkp89Oi=WHmPEVX9n3Y0A#!>(~$%j8o{>Qt3N$Lle+sx?yaGl;}KUZIXngr z?%q`lG5IVt0pd}c2+d9`x)1H_6XrT+bBq#&yEC*wQz0hxUIC*N$jSz?d+Dwc`1sLQ zNy_8#$O!I6>O%_Q2qA)qQ6e}Qvs%q_GKgzyIgj{lim_gV)hgHwHMkm~N0QP= zApt21tn8pw)*FeCii>|E~p_L~^44J7Nnq7`=rbaND5y~D} zY*z z-$3cUT-jwpInhimqd7UD%_tNLvoqoPx2wyQ%yZx*T3|!|!^v;|%#NPL7=;!0^-eaZk1?>4H$3hWPD~_h z>1&o0yL|euk$7NF(BOB_hEDmF;3FVY-kQ1`!H8^t&4XIHTErS{HnQ|b+dTAS0s<*4P@_H+A zU=ACfcAOGQXCLM6L9R-}Ch@TlfTorXl-~Rx8rDjZz^@BONZsOZ>rwEpFxQK4H8p;? zYl;cN3Y4oE%Z7P(<7JX9D zON2petgY?f^bfg)|GfOR<@<=Xf5mH4?>iQ2eaAc#`LSR#V@D0|(_@kE;b)ueTl}Ko zN0aGDv36i06jJ+bY96{*2ddR-hW?Ur{d#L*G=0z$iuR? zoSdBgx|nQtWxvaxKqKzLxAb-NZ@a))5erCb(fGGDF2wpSZ{|32yauK8{gfjflQ<^T z_MQLnFN*yFus!sym_E_YV7ybF#iW`uRDJaAZ_bfKS;D4{J}aI~1SS~0BWm#2hh3go zt=SdQck(=Glq|sl!Ck4`AUh#n0S0%xhG;?go{BP&bxU!zF=7X_`mQL6)rJZirud5o zQ`y%Oztg7Ei+U`ErB%kxR{@)K$rsD_5L$_g8t!(%!uiO^XfWg&Z_vh7eCfm`QL#(I zYjgXH`%C-F`zs!Qj&@XFH@d8!VUsi#GW1i~2_Hq~a-EYf%?g7W`8GfX&_q>ctdxBG z^lDO_b9T1V@rP)q>->I|bYHKSnPRzBBR$i^1tB&QM5&@audUgh-+ZVV+7am!YVj%f{hey&4ZH&L_g)8*dy*2`HnQ8bk1bhSMjub zpT*(2lz!alI=8>q{rs_kk`N;NMvun8w596PSo^k&T~hw_yvm4xufsqc+S zQrt)?gq^zr#QwObN#0%K4gLf!FGU%lC{rK}qD+j~WH!1aLJyv(QA@b^SJHFzO|4r+ zuL$x1&-27v8H4U!W zCX&Qd+^kUYJt@4Z)Fy;VoVfMN$zojq)^ktx&&0!o~{El&RO3% zR!XdxOrC;G8H^k$UHs(&>cJ)ld@q{~F*@9_uXRIYD9LM|*VgY<%b;+D0W1)l#am!D zMhNo|C#p-=yakjI!5MKY27{cwA!Ohz_Nm&-D^Vd-+IZ*iewpAO66sxl&=B6p+w%}a zP5r;bfCkks}|6kDmJA8@#8iS6Qs8cj!Sz=2RTFo5Qehs16EK74kO1F2T?sJa&XnnsoM0)0B zf7##VEr1NXIxT9=nSU3>d!ni1w{BK=@tauZp$^jCYDYCqSq&j)Do)UPgWTGCBcOrdEK*oANMOEXW&f|;zZ z<9cs6L^gU=1npB*naXjaGUL>mCWZQ1rn#w?^;qi^9@xCTV?`T?QCwfL%B(!H0Zz@p zn>7@LOY+Yd^}Fms_?m`aL2H>7>SO=P|(3yL>t6pUN~}+I9uz z7cGdV?2Eq2+RPnVxRx0<^DFtx_+kBd5qm8>BYUB>C46~$R#U%v+RNa&j72%BM$9Rt+dfdPJ^Z?v z{o9QDzTsDFt~6)$ZeJI-1H3@uKD$qlMkmkC6k+J5^VO@A^R?oT#?4Reyy!EDhJCS_ z@p>Im$NmOa_w!F@TWrvT^H09yeP(qDN2Uj@feM-qdA=(%FOD5V3bv^0ygyPKZtzcv z8%XoJ1-yq4x8^qwlxF*G*DUt-7_0p4|LhAC4fr9QmHSMe6LPRF>f7vo zTq&VHC?v3bLL=waayhLSC)Jo03*J9=*#43=+qH4=LFluav|vjDq^#wNR5eV}tw~3k z->(IX$_d02D6SX|Z(oFp1RmGnf{kLN-iSLB-?cx$Hg{7)+xwHvL=17pTB_&Eu0|7a z>FoKaJVBp%RbQ0sWzAq0SMWze(s#x<+PR94wnP z50f?CT?|;}%M<1&S|(%2d(PW1=ny2sAzc9U94KfM@?&sqnIHF_I2}cujtcl9JjFu? zn8DeKoDo72u0?*|4HeB$!BAB+_|xfhB6K_=w7OZ4@67j|Z6pNQd)K(Kkt8b4po6V} z)GN(yq7dhg@9|QG#A?u)wOb!OeO%#WNelX|{+(&9B22#S3euYu2#nb;(#E-qGYd~x zbNHxV;ni&p>>-A(hi9nf`E%M|mj47`cuFCdaSNxcoFU2q5%8xJWa~rf56Q?POHssF zF15Gvc2ZFw!|}I<;dClY8Y}ZG7iV0~oO-?NWMk)K2fR)2zHh)~g_+l>lznUQ+U2)D zl^Qtvc0_uhC1@wcjm84kw=H5;b1p{_V96;u@3GnI4fQ1ue~MpQdw@Z4b~6^TBw-J| zB6bxgAO*Bg$z9&JC~GUOP$p+(jIq)6ou+dW3oaZ`f#{kZ=6ClN6q`&qqZH_;Uo7!w ztXXUy4ZWX_toZb7z#Dcg&K+pndI!W~fjy|COKX;AU(jhFR(g!bU+ZRjI_8lkaqI)& zCIgp)g%H^!w`j;l@t@X$1Ylscvetx#nYglrQNa2q$E8pcLH5zwMh5C@(iC!roT!p8 zV!q)YE%GhyOp~stU`Jyvd3%!@X-eRvZ{RsHuz_A++1-Jhp6sFg>w<$w|KZK6G2N-b zk7X3d`x0jm*;j6HkW*{DvOtiW4mB`OV4x3Z#^`83%?lThPX&2K>ggblx8P+>Jy9a5 z^S9qbLHj<{rX86c571RF_th@XRP$N(mMAq1Jbx*#6_Z+G>uEInWF1fNg>WS7nZL7r zwwZ21yQ=L0-L(%H)cY`94N~cBUdi0p>M2=ZuODRLRq? z89J#w+~{;mhZNj48{SH3EtHh4S&mq#&U%!)K=)q4@aI&kN51co>%zDm_<(Aca5O_Y zM}C0eNHX|m5fyY3H)$YPd^RH&s>xe(MY7}Eh)o&^fO}kEwl1s>{Gwt%2opFperNf= zVrFipHOT&;{$>{r5d00O%9853C=4#tcD{#@jwKVwXh)13A;#DaWl-5kSX}=i^r>!s zev0Gh#JqYnVLU8+%JeWTmmz}M?Wt{KjR9@W?ymAuy1R8rZZh7`nbUtnav(ZZQwNeO zk?&XK@^U0Ut5>H=6xS+HQbkpbaFg9t)X_}2ZJBP4Mc>d;KOhC>U$7uqp`%EGcCSc< z#gEILlt#ESF>WM(x@MJ6i4O-5aP`BU9C8khf@DW9VV4okqtf7396YZCjUSQG2L~~E z-^NKXLfx5tU@-x&l;6WD3qjvSKQ*3k5thr5G9v79!BI7WdYrO=vrG$U5{ZN8jtRIb ze%CA9fVe!*n>iOx0_k60HRGN0qM5u&aP|qoB10U={eS6wVA=jVt&{(RoLN4a=`Ug< zJq^Rh`|*EjzWgT~{%?9W{~kHqu3F{UvuA>mlIpKsz0xVt+1zRMrX5(Y3i$`OxjoY^ z*Di_q-t)V~->8HPO`y&M9z^!`_j6kzs7zD;!FiqBS(o`Tbku;=CCRQ<=>?sNH*?Tw z=D^_i{_a(;P!679!DmLZW|kq-4x{w}5Gu)Xr)ufwv7xBcv$Y?8J{a6)>k)z0f;uXT zdxlu(n^y$IO3rS5k@GSHN~ZGJ<_R40~&F!~B# zK=!F$a;A50B4#h3fsG?L*Gc}ow05SL3N=Pq3Y^N+5)kkPGf#?`-0UFd%?_+R);-e@ z(7Xx9RDRT}ak(s^;s!^n`i>gzkD^P9%?XquDA3^PNmkH#dgaGN3FTz4TtO}RO7+`TsOztu{n`xH zw3D!-Lu^-*F<;JI>uFpvTB%J4fV{;1^KMm(`KjnRo+mR;_3*y+>fW%;OsH!2Q)n=pPG$Z zFM2in+^tmbSx7bb~)U-XNacu=w4D-g-N!zt-+sdN8Y==A+$_Pu2bp8%57*= z`a7@~=1ze2c;h7~XS~>8$f<)G*)GZot#K1Jp=doAi^Jqtd`lqFKSn=Q^&y?T(pG!@ zo@wkYTz3GI!yk3!ZtiF^2VR$4xTk!08{?B!VC3z?XtJkdd+|d^ab9ly!!S{0=GhIs z3_hBo>f_@!CwZgP6xM^NfHVPTdm7h2n+QtX z>F+BP>Z^(W@nzJRak!~l&9fu5J0d~>q@-jx1JKkvj26*g?!=G>D4z>=_xS4@*h!=w zKhI?n9zOrw{WLKGbJ3kx^pVARu|CaI>>gyH+vY&~wRmOOX-x0*h^zbty?eQGXTrXN zdZB~o_n$xnAHl$d{%2{06jpQbZf>{4BYw~2Mt{_^gSlbzH2uO9O6a|ggSm*0`7-R? zJ2EIG(7btr>11cmNrOa9tf|OBOdg^1aJ%E`ceMYn<1-=fVX?bIR>AgDW0I%yLlUDN zrsBW=59ip%78Tvw=V!778?Sdc)+OCyOFOO`KRtQlkE%Mr(DfgX!TwzJd0+^ePs{pI znBT3k47ax<-7|S7{QGHqK4%FZ-Bh(89_cGj`z$#_Ty1Nw$c5iXZK6 z&8S;6dO-u8!*bNWT6ls3)R~1$&>X0YI(Uj2d1lV#l#pl+4QxA~|H34b>mK`nvKW&C z_>g`ft7FZ*;;n(aQDVwU_r1owWR@9}Nh{pSlD)|`{CV?JIU}!h*g>n^phApem^bzW zxfdC|=3|?~GdZ}_t(a5m)D!5_(BAEtx!HJ2NCgNu*{Ab%*Ns@V8RbY$=eGAtT8nopz6M$ zynslb1wk^s&~Z0;`-C4J^AR;}Em@g{RikUE)q6X8hX=iU_F2$#r>FWT_gj|-rG^dO zgNZq-dXT0IL6Zp!_#@y ztASqQ=cAk9amt`HjlZuljDNErW*OR7KR=2!kww&~QeiEE2cffI{l(cpdt@xM z-K4Qt{I;ppPw5!9ck$ze3WTU3J+4GP@(0%re$Kpm%1`L)281-+dwR}VKBLcj!re5n zZpVOU>pGfPGGp?3X(`5PWr34QE`ZKqvyATeYL9s)sqSMbFv$10FP<1R2??~JQ+pGe~+;4X#y z|F}m}(v+g zHSYg>YV&R@Wc23W)FFd(_C&i2Um1tV3E&vxCk@$$|N7|rf~BrGNuSwg{FC0i*2l{$ zELCoB^9&j0W)+5iXq8LC@4dIwyigY4*AIrsDg;Bs$pa*G*h9;kjLP*8)A=ia{XKvy zdJ_7&oB<2HgK*>2X4r8V6a^3>KmlF3=Q+&_B1MN#4|l=oeQX}~89D`ptr8%!#Iu$= z4~!{PPbyN6`Gjk+?SAjXeM$36I^xyTOsl}t{vd&xd!D+X5WvbG$&RlTqSN`F z?H7Nf9ypo_VO3o9Lf<6fBwV9ez&qqGr1=6IsqZPg4*U3ePOZwE)Yu6zkXdt;eDX!hH6A zUD8@L_X-o`N)pk0r1dBHhG>vY$z>ZkfT>t&q=@hfbocFtaH5JJ z$V29zL2seni2UdGk)aPFDVlcOTNK$3ER`3#%Ck|MBHFs@s=6o@!){yZYX4D2M+^FO0i7|c{6CS;v=Pbzfsw`#rK60zh)2Y!Xcsz6}L4m zl(j;A7CU!#i*whd^+oy1$)#Gp?&6H9K@AnAIv;MN$oa>~T)(ZLYoz|=Iy+>fNL+0i zyUXePYz*xG9vhSW#Iqq&4gcYjVQ-D@WAj*ivJQ(=-ELkM*f2K zK(HKf>7(3j#WM6J#!#S^;a^{t^-SS?#o0bVAMGOF4d7NCzHgdZ8@x{a@h^+V;8Hom zc$=I3QC!PALS9DRg>TCkCqd*TqDo`!1Nq}qhIQ}cjJ7ZC+Fx-@N;3t-%C|LN z?P6_@n!u!^GUwD0>p$65aStNJmYeW!2H*fQ2*g`H=b6LyPpjUu{!oykG`IAYcjpc2f;w&R}Nx*N3iE&8a;tC{afmH0<{yR@gxuGiZW+eg z%TLoDXFhjAOb0>QE2~vThZX5c9PEYn+z`J%W6K*rF?p1hZmE9dG`Uf;3BdINWroVU z8qKFFk(t?w0N77&;p+!vZbiwA80ssNb|DaBX`P0n+S6jJtS-1B5_S*$PJ56`)8C|2 zi@Xk5Re{;ILI9mATHam2#qmEbO{3e~u;N3s50An5tV>$$Bw#PjxocamFT=3?Z+FU@ z2DA7!bZh;(Lh|zT75|APonochGH3(vg@)Oz0R91nWR(n*U&EG_>pm?_2~mAy4Bl1% zCz(9*#>X1YO-|m|cF{$>P`k#Ny{E8dhA;UtEi>RH_1q!8sHTUgU;~IoJ2v?>>#mP& zX>kq}!UR|7x&^mt;r7#|+IXSJP;`0jt={$Y&6Gw;n%Yc{=dpH^Y3`8(S3fwb@OWre zUhlJ=ZNvD~9gk-sRc}K&a=;w4m)xQo|E#Z&omVPwDnPOPbAhx85;&oDXPXj!;C}0I z=pvF0I6*JAlKQ!z=bNV}V_MKx`;ITK@L!(sz=`g7(U_L?zSS30D=wc3c`n^g$F*3zBs z=nY?s%EY_X{Fk@NYRqEN!MKk)(;url=fbYL8fsG@```I7w(Q#>L z`c)pO=&2>`HnQ;gxJE}wL | 领域事件集合 | - -**关系**:所有其他实体类都继承自 EntityBase,共享这些基础属性。 - -#### 2. Document -| 属性名 | 类型 | 描述 | -| ------------------------- | ---------------------------- | -------------------------- | -| Id | long | 文档唯一标识符 | -| Title | string | 文档标题 | -| FileName | string | 文件名 | -| FilePath | string | 文件路径 | -| FileType | string | 文件类型 | -| FileSize | long | 文件大小 | -| AccessLevel | string | 访问级别 (internal/public) | -| Status | DocumentStatus | 文档状态 | -| VectorizationStatus | VectorizationStatus | 向量化状态 | -| VectorizationErrorMessage | string | 向量化错误信息 | -| VectorizedChunks | int | 已向量化的分块数 | -| TotalChunks | int | 总分块数 | -| LastVectorizedAt | DateTime? | 最后向量化时间 | -| Chunks | IReadOnlyList | 文档分块集合 | - -**关系**: -- 一个 Document 可以包含多个 DocumentChunk (一对多) -- Document 属于一个 KnowledgeBase -- Document 属于一个 User - -#### 3. DocumentChunk -| 属性名 | 类型 | 描述 | -| ---------- | ------ | -------------- | -| Id | long | 分块唯一标识符 | -| DocumentId | long | 所属文档ID | -| Content | string | 分块内容 | -| Position | int | 在文档中的位置 | -| TokenCount | int | Token数量 | - -**关系**: -- 一个 DocumentChunk 属于一个 Document (多对一) -- DocumentChunk 可以被多个 MessageReference 引用 - -#### 4. KnowledgeBase -| 属性名 | 类型 | 描述 | -| ----------- | --------------------- | ---------------- | -| Id | long | 知识库唯一标识符 | -| Name | string | 知识库名称 | -| Description | string | 知识库描述 | -| Documents | ICollection | 文档集合 | - -**关系**: -- 一个 KnowledgeBase 可以包含多个 Document (一对多) - -#### 5. VectorizationJob -| 属性名 | 类型 | 描述 | -| -------------- | ---------------------- | ---------------- | -| Id | long | 任务唯一标识符 | -| Name | string | 任务名称 | -| DocumentIds | List | 待处理文档ID列表 | -| Parameters | VectorizationParams | 向量化参数 | -| Status | VectorizationJobStatus | 任务状态 | -| ProcessedCount | int | 已处理文档数 | -| TotalCount | int | 总文档数 | -| ErrorMessage | string | 错误信息 | - -**关系**: -- 一个 VectorizationJob 可以处理多个 Document (通过 DocumentIds) -- 包含 VectorizationParams 作为参数配置 - -#### 6. VectorizationParams -| 属性名 | 类型 | 描述 | -| ------------ | ------- | ---------- | -| ModelName | string | 模型名称 | -| ChunkSize | int | 分块大小 | -| ChunkOverlap | int | 分块重叠度 | -| Separator | string? | 分隔符 | - -**关系**: -- 被 VectorizationJob 引用作为参数 - -#### 7. User -| 属性名 | 类型 | 描述 | -| ------------- | ------------------------ | -------------- | -| Id | long | 用户唯一标识符 | -| Username | string | 用户名 | -| Email | string | 邮箱 | -| PasswordHash | string | 密码哈希 | -| FullName | string | 全名 | -| IsActive | bool | 是否激活 | -| Documents | ICollection | 文档集合 | -| ChatHistories | ICollection | 聊天历史集合 | - -**关系**: -- 一个 User 可以拥有多个 Document (一对多) -- 一个 User 可以有多个 ChatHistory (一对多) - -#### 8. ChatHistory -| 属性名 | 类型 | 描述 | -| --------- | ------------------------ | ------------------ | -| Id | long | 聊天历史唯一标识符 | -| SessionId | string | 会话ID | -| UserId | long | 所属用户ID | -| User | User | 所属用户 | -| Messages | ICollection | 消息集合 | - -**关系**: -- 一个 ChatHistory 属于一个 User (多对一) -- 一个 ChatHistory 可以包含多个 ChatMessage (一对多) - -#### 9. ChatMessage -| 属性名 | 类型 | 描述 | -| ------------- | ----------------------------- | -------------- | -| Id | long | 消息唯一标识符 | -| ChatHistoryId | long | 所属聊天历史ID | -| ChatHistory | ChatHistory | 所属聊天历史 | -| Content | string | 消息内容 | -| IsUserMessage | bool | 是否用户消息 | -| Timestamp | DateTime | 时间戳 | -| References | ICollection | 引用集合 | - -**关系**: -- 一个 ChatMessage 属于一个 ChatHistory (多对一) -- 一个 ChatMessage 可以引用多个 DocumentChunk (通过 MessageReference) - -#### 10. MessageReference -| 属性名 | 类型 | 描述 | -| --------------- | ------------- | -------------- | -| Id | long | 引用唯一标识符 | -| ChatMessageId | long | 所属消息ID | -| ChatMessage | ChatMessage | 所属消息 | -| DocumentChunkId | long | 文档分块ID | -| DocumentChunk | DocumentChunk | 文档分块 | -| RelevanceScore | double | 相关度分数 | - -**关系**: -- 连接 ChatMessage 和 DocumentChunk 的多对多关系中间表 -- 一个 ChatMessage 可以有多个 MessageReference -- 一个 DocumentChunk 可以被多个 MessageReference 引用 - -#### 11. 枚举类型 -- **DocumentStatus**:文档状态(如 Pending, Processing, Completed, Failed 等) -- **VectorizationStatus**:文档向量化状态(如 NotVectorized, Vectorizing, Vectorized, Failed 等) -- **VectorizationJobStatus**:向量化任务状态(如 Pending, Processing, Completed, Failed 等) - -### 实体关系图 -``` -User 1--* Document -User 1--* ChatHistory -KnowledgeBase 1--* Document -Document 1--* DocumentChunk -ChatHistory 1--* ChatMessage -ChatMessage *--* DocumentChunk (通过 MessageReference) -VectorizationJob *--* Document (通过 DocumentIds) -``` - -以上详细描述了每个实体类的属性及其之间的关系,展示了系统的数据模型结构。 - \ No newline at end of file diff --git a/backend/RAG.Api/Files/bbcc592c-97b1-4efa-8284-5c760bbc32db_test.txt b/backend/RAG.Api/Files/bbcc592c-97b1-4efa-8284-5c760bbc32db_test.txt deleted file mode 100644 index 23166bf..0000000 --- a/backend/RAG.Api/Files/bbcc592c-97b1-4efa-8284-5c760bbc32db_test.txt +++ /dev/null @@ -1 +0,0 @@ -This is a test document content. diff --git a/backend/RAG.Api/Files/c7be2844-4774-4945-9245-c614a5b615ea_963c1d70789c87eefcdce5cb8d873b1.png b/backend/RAG.Api/Files/c7be2844-4774-4945-9245-c614a5b615ea_963c1d70789c87eefcdce5cb8d873b1.png deleted file mode 100644 index 3cdeafe89383135294ae784cb8a30dfaa35fb558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11973 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 cfg.AddMaps(Assembly.GetExecutingAssembly( builder.Services.AddAutoMapper(cfg => cfg.AddMaps(typeof(RAG.Application.Class1).Assembly)); // 注册文件存储服务 -builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// 替换为数据库文件存储服务 +builder.Services.AddScoped(); // 注册文档服务 builder.Services.AddScoped(); diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 280e587..222918d 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -18,28 +18,7 @@ Content-Disposition: form-data; name="Title" ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="AccessLevel" -internal -----WebKitFormBoundary7MA4YWxkTrZu0gW - -### - -#### 2. 上传单个文档 -POST {{url}}/api/document/upload -Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW - -----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="File"; filename="test.txt" -Content-Type: text/plain - -这是测试文档内容 -----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="Title" - -测试文档 -----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="AccessLevel" - -internal +Internal ----WebKitFormBoundary7MA4YWxkTrZu0gW ### @@ -49,23 +28,23 @@ POST {{url}}/api/document/upload-multiple Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="files"; filename="file1.txt" +Content-Disposition: form-data; name="Files"; filename="file1.txt" Content-Type: text/plain 文档1内容 ----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="files"; filename="file2.txt" +Content-Disposition: form-data; name="Files"; filename="file2.txt" Content-Type: text/plain 文档2内容 ----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="titlePrefix" +Content-Disposition: form-data; name="TitlePrefix" 批量上传测试 ----WebKitFormBoundary7MA4YWxkTrZu0gW -Content-Disposition: form-data; name="accessLevel" +Content-Disposition: form-data; name="AccessLevel" -internal +Internal ----WebKitFormBoundary7MA4YWxkTrZu0gW ### @@ -145,8 +124,14 @@ Accept: application/json ### -#### 2. 向量化单个文档 -POST {{url}}/api/knowledgebase/documents/7651925779515340169/vectorize +#### 2. 获取文档状态 +GET {{url}}/api/knowledgebase/documents/701403795935186172/vectorization-status +Accept: application/json + +### + +#### 3. 向量化单个文档 +POST {{url}}/api/knowledgebase/documents/701403795935186172/vectorize Content-Type: application/json { @@ -158,12 +143,12 @@ Content-Type: application/json ### -#### 3. 批量向量化文档 +#### 4. 批量向量化文档 POST {{url}}/api/knowledgebase/documents/batch-vectorize Content-Type: application/json { - "DocumentIds": [172579765607330619, 7483280351647518150, 32055697673725165], + "DocumentIds": [701403795935186172, 172579765607330619, 7483280351647518150], "Parameters": { "ModelName": "default", "ChunkSize": 500, @@ -174,12 +159,6 @@ Content-Type: application/json ### -#### 4. 获取文档向量化状态 -GET {{url}}/api/knowledgebase/documents/172579765607330619/vectorization-status -Accept: application/json - -### - #### 5. 获取向量化任务列表 GET {{url}}/api/knowledgebase/vectorization-jobs?page=1&pageSize=10&status=Processing Accept: application/json @@ -239,7 +218,7 @@ Content-Type: application/json "dimensions":"1024", "encoding_format":"float" - + } @@ -248,7 +227,7 @@ POST {{url}}/api/UserSay/Add Content-Type: application/json { - "Usersay":"你好,", + "Usersay":"你37", "Message":"11111" diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json index ea8d7a0..069a50c 100644 --- a/backend/RAG.Api/appsettings.json +++ b/backend/RAG.Api/appsettings.json @@ -7,11 +7,11 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Host=www.mingyuy.cn;Port=5432;Database=rag_db;Username=postgres;Password=mingyuy." + "DefaultConnection":"Host=182.92.86.71;Port=5432;Database=postgres;Username=schoolproject01;Password=YourSecurePassword123" }, "VectorStore": { "Type": "Postgres", - "ConnectionString": "Host=www.mingyuy.cn;Port=5432;Database=rag_db;Username=postgres;Password=mingyuy." +"DefaultConnection": "Host=182.92.86.71;Port=5432;Database=postgres;Username=schoolproject01;Password=YourSecurePassword123" }, "jwt":{ "Secret":"YourSuperSecretKeyForAdminToken2025!", diff --git a/backend/RAG.Application/Interfaces/IDocumentService.cs b/backend/RAG.Application/Interfaces/IDocumentService.cs index c051fa4..ef0c911 100644 --- a/backend/RAG.Application/Interfaces/IDocumentService.cs +++ b/backend/RAG.Application/Interfaces/IDocumentService.cs @@ -7,8 +7,8 @@ namespace RAG.Application.Interfaces public interface IDocumentService { // 上传文档 - Task UploadDocumentAsync(DocumentUploadDto uploadDto); - Task> UploadDocumentsAsync(IEnumerable uploadDtos); + Task UploadDocumentAsync(DocumentUploadDto request, Stream fileStream, string fileName, string fileType, long fileSize, CancellationToken cancellationToken); + Task> UploadDocumentsAsync(IEnumerable uploadDtos, CancellationToken cancellationToken = default); // 获取文档列表 Task> GetDocumentsAsync(DocumentQuery query); diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs index 2d32b9d..c5b8ab4 100644 --- a/backend/RAG.Application/Services/DocumentService.cs +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Threading; using Microsoft.EntityFrameworkCore; using RAG.Application.DTOs; using RAG.Application.Interfaces; @@ -13,6 +14,7 @@ using RAG.Infrastructure.FileStorage; using RAG.Infrastructure.Data; using RAG.Infrastructure.VectorStore; + namespace RAG.Application.Services { public class DocumentService : IDocumentService @@ -43,107 +45,22 @@ namespace RAG.Application.Services _unitOfWork = unitOfWork; } - public async Task UploadDocumentAsync(DocumentUploadDto uploadDto) + public async Task> UploadDocumentsAsync(IEnumerable uploadDtos, CancellationToken cancellationToken = default) { - // 验证文件格式 - var allowedExtensions = new[] { ".txt", ".md", ".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".png", ".jpg", ".jpeg" }; - var fileExtension = Path.GetExtension(uploadDto.File.FileName).ToLower(); - - if (!allowedExtensions.Contains(fileExtension)) - { - throw new ArgumentException($"Unsupported file format: {fileExtension}"); - } - - // 验证文件大小 (50MB) - const long maxFileSize = 50 * 1024 * 1024; - if (uploadDto.File.Length > maxFileSize) - { - throw new ArgumentException("File size exceeds the maximum limit of 50MB"); - } - - // 获取合适的文档处理器 - var processor = _documentProcessorFactory.GetProcessor(fileExtension); - if (processor == null) - { - throw new InvalidOperationException($"No processor found for file type: {fileExtension}"); - } - - // 注册进度事件处理程序 - string progressKey = Guid.NewGuid().ToString(); - ProgressReportedEventHandler progressHandler = (operationId, progress, status) => - { - if (operationId == progressKey) - { - // 这里可以记录进度或执行其他操作 - Console.WriteLine($"Upload progress: {progress}% - {status}"); - } - }; - _progressService.ProgressReported += progressHandler; + var results = new List(); - try + foreach (var uploadDto in uploadDtos) { - // 上传文件到存储服务 - string filePath; - using (var stream = uploadDto.File.OpenReadStream()) + using (var fileStream = uploadDto.File.OpenReadStream()) { - filePath = await _fileStorageService.UploadFileAsync(stream, uploadDto.File.FileName, uploadDto.File.ContentType); - } + var fileName = uploadDto.File.FileName; + // 获取文件扩展名作为文件类型 + var fileType = Path.GetExtension(fileName).ToLower(); + var fileSize = uploadDto.File.Length; - // 创建文档实体 - var document = Document.Create( - uploadDto.Title, - uploadDto.File.FileName, - filePath, - fileExtension.TrimStart('.'), - uploadDto.File.Length, - uploadDto.AccessLevel); - - // 保存到数据库 - _context.Documents.Add(document); - await _context.SaveChangesAsync(); - - // 解析文档内容 - DocumentContentDto contentDto; - using (var stream = await _fileStorageService.GetFileStreamAsync(filePath)) - { - contentDto = await processor.ExtractContentAsync(stream, uploadDto.File.FileName, progressKey); + var result = await UploadDocumentAsync(uploadDto, fileStream, fileName, fileType, fileSize, cancellationToken); + results.Add(result); } - - // 更新文档状态为已完成 - document.UpdateProcessingStatus(); - document.CompleteProcessing(); - await _context.SaveChangesAsync(); - - // 返回DTO - return new DocumentDto - { - Id = document.Id, - Title = document.Title, - FileName = document.FileName, - FileType = document.FileType, - FileSize = document.FileSize, - AccessLevel = document.AccessLevel, - Status = document.Status, - CreatedAt = document.CreatedAt, - UpdatedAt = document.UpdatedAt - }; - } - finally - { - // 移除进度事件处理程序 - _progressService.ProgressReported -= progressHandler; - } - - } - - public async Task> UploadDocumentsAsync(IEnumerable uploadDtos) - { - var results = new List(); - - foreach (var uploadDto in uploadDtos) - { - var result = await UploadDocumentAsync(uploadDto); - results.Add(result); } return results; @@ -205,7 +122,7 @@ namespace RAG.Application.Services public async Task GetDocumentByIdAsync(long id) { - var document = await _context.Documents.FindAsync(id); + var document = await _context.Documents.FindAsync(new object[] { id }); if (document == null) { @@ -228,35 +145,25 @@ namespace RAG.Application.Services public async Task PreviewDocumentAsync(long id) { - var document = await _context.Documents.FindAsync(id); + var document = await _context.Documents.FindAsync(new object[] { id }); if (document == null) { throw new KeyNotFoundException($"Document with id {id} not found"); } - // 对于文本文档和PDF,我们可以尝试读取内容 + // 从数据库读取文件内容 string content = string.Empty; - byte[]? fileData = null; + byte[]? fileData = document.FileContent; if (document.FileType.ToLower() == "txt" || document.FileType.ToLower() == "md") { - using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var stream = new MemoryStream(document.FileContent)) using (var reader = new StreamReader(stream)) { content = await reader.ReadToEndAsync(); } } - else - { - // 对于其他类型的文件,我们返回文件数据 - using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) - using (var memoryStream = new MemoryStream()) - { - await stream.CopyToAsync(memoryStream); - fileData = memoryStream.ToArray(); - } - } return new DocumentPreviewDto { @@ -271,7 +178,7 @@ namespace RAG.Application.Services public async Task UpdateDocumentAsync(long id, DocumentUpdateDto updateDto) { - var document = await _context.Documents.FindAsync(id); + var document = await _context.Documents.FindAsync(new object[] { id }); if (document == null) { @@ -312,16 +219,13 @@ namespace RAG.Application.Services public async Task DeleteDocumentAsync(long id) { - var document = await _context.Documents.FindAsync(id); + var document = await _context.Documents.FindAsync(new object[] { id }); if (document == null) { return false; } - // 删除文件 - await _fileStorageService.DeleteFileAsync(document.FilePath); - // 删除数据库记录 _context.Documents.Remove(document); await _context.SaveChangesAsync(); @@ -338,12 +242,6 @@ namespace RAG.Application.Services return false; } - // 删除文件 - foreach (var document in documents) - { - await _fileStorageService.DeleteFileAsync(document.FilePath); - } - // 删除数据库记录 _context.Documents.RemoveRange(documents); await _context.SaveChangesAsync(); @@ -424,7 +322,7 @@ namespace RAG.Application.Services public async Task VectorizeDocumentAsync(long documentId, VectorizationParamsDto parameters) { // 验证文档存在 - var document = await _context.Documents.FindAsync(documentId); + var document = await _context.Documents.FindAsync(new object[] { documentId }); if (document == null) { throw new KeyNotFoundException($"Document with id {documentId} not found"); @@ -462,9 +360,9 @@ namespace RAG.Application.Services await _unitOfWork.SaveChangesAsync(); // 开始文档向量化 - // 读取文档内容以确定总块数 + // 从数据库读取文档内容以确定总块数 string content; - using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var stream = new MemoryStream(document.FileContent)) using (var reader = new StreamReader(stream)) { content = await reader.ReadToEndAsync(); @@ -482,9 +380,16 @@ namespace RAG.Application.Services { try { - // 读取文档内容 + // 获取文档(避免闭包问题) + var doc = await _context.Documents.FindAsync(new object[] { documentId }); + if (doc == null) + { + throw new KeyNotFoundException($"Document with id {documentId} not found"); + } + + // 从数据库读取文档内容 string content; - using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) + using (var stream = new MemoryStream(doc.FileContent)) using (var reader = new StreamReader(stream)) { content = await reader.ReadToEndAsync(); @@ -497,7 +402,7 @@ namespace RAG.Application.Services parameters.ChunkOverlap, parameters.Separator ?? string.Empty); - document.StartVectorization(chunks.Count); + doc.StartVectorization(chunks.Count); await _unitOfWork.SaveChangesAsync(); // 处理每个块 @@ -514,35 +419,39 @@ namespace RAG.Application.Services embedding, new { - documentId = document.Id, + documentId = doc.Id, chunkId = i + 1, position = i + 1 }); // 创建文档块 var chunk = DocumentChunk.Create( - document.Id, + doc.Id, chunkContent, i + 1, chunkContent.Length); _context.DocumentChunks.Add(chunk); - document.AddChunk(chunk); + doc.AddChunk(chunk); // 更新进度 - document.UpdateVectorizationProgress(i + 1); + doc.UpdateVectorizationProgress(i + 1); await _unitOfWork.SaveChangesAsync(); } // 完成文档向量化 - document.CompleteVectorization(); + doc.CompleteVectorization(); job.Complete(); await _unitOfWork.SaveChangesAsync(); } catch (Exception ex) { // 记录文档向量化失败 - document.FailVectorization(ex.Message); + var doc = await _context.Documents.FindAsync(new object[] { documentId }); + if (doc != null) + { + doc.FailVectorization(ex.Message); + } job.Fail(ex.Message); await _unitOfWork.SaveChangesAsync(); } @@ -617,14 +526,20 @@ namespace RAG.Application.Services { try { - foreach (var document in documents) + // 获取文档列表(避免闭包问题) + var docs = await _context.Documents + .Where(d => request.DocumentIds.Contains(d.Id)) + .ToListAsync(); + + foreach (var doc in docs) { try { // 读取文档内容 string content; - using (var stream = await _fileStorageService.GetFileStreamAsync(document.FilePath)) - using (var reader = new StreamReader(stream)) + // 使用数据库中的文件内容,而不是从文件系统读取 + using (var memoryStream = new MemoryStream(doc.FileContent)) + using (var reader = new StreamReader(memoryStream)) { content = await reader.ReadToEndAsync(); } @@ -635,7 +550,7 @@ namespace RAG.Application.Services request.Parameters.ChunkOverlap, request.Parameters.Separator ?? string.Empty); // 开始文档向量化 - document.StartVectorization(chunks.Count); + doc.StartVectorization(chunks.Count); await _unitOfWork.SaveChangesAsync(); // 处理每个块 @@ -652,35 +567,39 @@ namespace RAG.Application.Services embedding, new { - documentId = document.Id, + documentId = doc.Id, chunkId = i + 1, position = i + 1 }); // 创建文档块 var chunk = DocumentChunk.Create( - document.Id, + doc.Id, chunkContent, i + 1, chunkContent.Length); _context.DocumentChunks.Add(chunk); - document.AddChunk(chunk); + doc.AddChunk(chunk); // 更新进度 - document.UpdateVectorizationProgress(i + 1); + doc.UpdateVectorizationProgress(i + 1); await _unitOfWork.SaveChangesAsync(); } // 完成文档向量化 - document.CompleteVectorization(); + doc.CompleteVectorization(); await _unitOfWork.SaveChangesAsync(); } catch (Exception ex) { // 记录文档向量化失败 - document.FailVectorization(ex.Message); - await _unitOfWork.SaveChangesAsync(); + var docToUpdate = await _context.Documents.FindAsync(new object[] { doc.Id }); + if (docToUpdate != null) + { + docToUpdate.FailVectorization(ex.Message); + await _unitOfWork.SaveChangesAsync(); + } } } @@ -718,7 +637,7 @@ namespace RAG.Application.Services public async Task GetDocumentVectorizationStatusAsync(long documentId) { - var document = await _context.Documents.FindAsync(documentId); + var document = await _context.Documents.FindAsync(new object[] { documentId }); if (document == null) { throw new KeyNotFoundException($"Document with id {documentId} not found"); @@ -741,26 +660,24 @@ namespace RAG.Application.Services if (page <= 0) throw new ArgumentException("Page must be greater than 0"); if (pageSize <= 0) - throw new ArgumentException("PageSize must be greater than 0"); - - // 获取所有任务并转换为查询able - var allJobs = await _vectorizationJobRepository.GetAllAsync(); - var queryable = allJobs.AsQueryable(); + throw new ArgumentException("Page size must be greater than 0"); - // 分页 - var totalCount = queryable.Count(); - var items = queryable - .OrderByDescending(j => j.CreatedAt) + var totalCount = await _vectorizationJobRepository.CountAsync(); + var jobs = await _vectorizationJobRepository.GetAllAsync(); + var items = jobs + .OrderByDescending(job => job.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) - .Select(j => new VectorizationJobDto + .Select(job => new VectorizationJobDto { - Id = j.Id, - Name = j.Name, - Status = j.Status.ToString(), - TotalCount = j.DocumentIds.Count, - CreatedAt = j.CreatedAt, - UpdatedAt = j.UpdatedAt + Id = job.Id, + Name = job.Name, + Status = job.Status.ToString(), + TotalCount = job.DocumentIds.Count, + CreatedAt = job.CreatedAt, + UpdatedAt = job.UpdatedAt, + ErrorMessage = job.ErrorMessage, + DocumentIds = job.DocumentIds }) .ToList(); @@ -777,9 +694,7 @@ namespace RAG.Application.Services { var job = await _vectorizationJobRepository.GetByIdAsync(jobId); if (job == null) - { - throw new KeyNotFoundException($"Vectorization job with id {jobId} not found"); - } + throw new KeyNotFoundException("Vectorization job not found"); return new VectorizationJobDto { @@ -788,7 +703,9 @@ namespace RAG.Application.Services Status = job.Status.ToString(), TotalCount = job.DocumentIds.Count, CreatedAt = job.CreatedAt, - UpdatedAt = job.UpdatedAt + UpdatedAt = job.UpdatedAt, + ErrorMessage = job.ErrorMessage, + DocumentIds = job.DocumentIds }; } @@ -800,7 +717,8 @@ namespace RAG.Application.Services return chunks; // 使用分隔符分割文本 - var paragraphs = text.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries); + var splitOptions = string.IsNullOrEmpty(separator) ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries; + var paragraphs = text.Split(new[] { separator }, splitOptions); var currentChunk = new List(); var currentSize = 0; @@ -851,27 +769,23 @@ namespace RAG.Application.Services return embedding; } - // 支持的文件类型列表 - private readonly List _supportedFileTypes = new List { ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".jpg", ".jpeg", ".png", ".bmp", ".tiff" }; - + private readonly List _supportedFileTypes = new List { ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".txt", ".md" }; // 验证文件类型是否有效 - private bool IsValidFileType(string fileType) + private bool IsValidFileType(string filePath) { - if (string.IsNullOrEmpty(fileType)) + if (string.IsNullOrEmpty(filePath)) return false; - - return _supportedFileTypes.Contains(fileType.ToLower()); + var extension = Path.GetExtension(filePath).ToLower(); + return _supportedFileTypes.Contains(extension); } - public async Task UploadDocumentAsync(DocumentUploadDto request, Stream fileStream, string fileName, string fileType, long fileSize, CancellationToken cancellationToken) { // 1. 验证文件类型 - if (!IsValidFileType(fileType)) + if (!IsValidFileType(fileName)) { - throw new InvalidOperationException($"Unsupported file type: {fileType}"); + throw new InvalidOperationException($"Unsupported file type: {Path.GetExtension(fileName)}"); } - // 2. 查找合适的文档处理器 var processor = _documentProcessorFactory.GetProcessor(fileType); if (processor == null) @@ -879,26 +793,37 @@ namespace RAG.Application.Services throw new InvalidOperationException($"No processor found for file type: {fileType}"); } - // 3. 提取文档内容 + // 3. 读取文件内容到byte[] + byte[] fileContent; + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream, cancellationToken); + fileContent = memoryStream.ToArray(); + } + + // 4. 提取文档内容 var progressKey = Guid.NewGuid().ToString(); - var documentContent = await processor.ExtractContentAsync(fileStream, fileName, progressKey); + using (var memoryStream = new MemoryStream(fileContent)) + { + var documentContent = await processor.ExtractContentAsync(memoryStream, fileName, progressKey); + } - // 4. 创建文档实体 - // 注意:这里假设文件路径是存储服务返回的路径,实际应用中需要根据实际情况获取 - string filePath = ""; // 实际应用中需要设置正确的文件路径 + // 5. 创建文档实体 var document = Document.Create( title: request.Title, fileName: fileName, - filePath: filePath, + filePath: string.Empty, fileType: fileType, fileSize: fileSize, - accessLevel: request.AccessLevel + accessLevel: request.AccessLevel, + fileContent: fileContent ); - // 5. 保存文档 (实际实现中需要调用仓储层) - // await _documentRepository.AddAsync(document, cancellationToken); + // 6. 保存文档到数据库 + _context.Documents.Add(document); + await _unitOfWork.SaveChangesAsync(); - // 6. 返回DocumentDto + // 7. 返回DocumentDto return new DocumentDto { Id = document.Id, @@ -912,4 +837,4 @@ namespace RAG.Application.Services }; } } -} \ No newline at end of file +} diff --git a/backend/RAG.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs index 09e0e1a..73701fb 100644 --- a/backend/RAG.Application/Services/UserSayService.cs +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -66,9 +66,10 @@ try for (int i = 0; i < 5; i++) { Console.WriteLine(embeddingVector[i]); - await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector)); + - } + } + await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector)); // 完整向量可以用于后续处理 // 例如: 存储到数据库或进行向量计算 diff --git a/backend/RAG.Domain/Entities/Document.cs b/backend/RAG.Domain/Entities/Document.cs index 349ac06..427d418 100644 --- a/backend/RAG.Domain/Entities/Document.cs +++ b/backend/RAG.Domain/Entities/Document.cs @@ -25,14 +25,16 @@ namespace RAG.Domain.Entities string filePath, string fileType, long fileSize, - string accessLevel) + string accessLevel, + byte[] fileContent = null) // 添加新参数 { if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Title cannot be empty"); if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("File name cannot be empty"); - if (string.IsNullOrWhiteSpace(filePath)) - throw new ArgumentException("File path cannot be empty"); + // 允许filePath为空字符串,因为我们现在直接存储文件内容到数据库 + // if (string.IsNullOrWhiteSpace(filePath)) + // throw new ArgumentException("File path cannot be empty"); if (string.IsNullOrWhiteSpace(fileType)) throw new ArgumentException("File type cannot be empty"); if (fileSize < 0) @@ -48,6 +50,7 @@ namespace RAG.Domain.Entities FilePath = filePath, FileType = fileType, FileSize = fileSize, + FileContent = fileContent ?? Array.Empty(), // 设置文件内容 AccessLevel = accessLevel.ToLower(), Status = DocumentStatus.Pending }; @@ -64,6 +67,7 @@ namespace RAG.Domain.Entities public string FilePath { get; private set; } public string FileType { get; private set; } public long FileSize { get; private set; } + public byte[] FileContent { get; private set; } // 新增属性用于存储文件内容 public string AccessLevel { get; private set; } public DocumentStatus Status { get; private set; } diff --git a/backend/RAG.Domain/Entities/UserSay.cs b/backend/RAG.Domain/Entities/UserSay.cs index 722f283..018d80f 100644 --- a/backend/RAG.Domain/Entities/UserSay.cs +++ b/backend/RAG.Domain/Entities/UserSay.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; + + namespace RAG.Domain.Entities { public class UserSay : EntityBase diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index 4e1c48b..172be1c 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -1,7 +1,8 @@ + using System.Text.Json; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; + using RAG.Domain.Entities; namespace RAG.Infrastructure.Data @@ -74,10 +75,10 @@ namespace RAG.Infrastructure.Data entity.Property(x => x.UersayVector) .HasColumnType("vector(1024)") .HasConversion( - v => System.Text.Json.JsonSerializer.Serialize(v, (JsonSerializerOptions)null), - v => System.Text.Json.JsonSerializer.Deserialize(v, (JsonSerializerOptions)null)); + v => v == null ? null : JsonSerializer.Serialize(v, (JsonSerializerOptions)null), + v => string.IsNullOrEmpty(v) ? null : JsonSerializer.Deserialize(v, (JsonSerializerOptions)null)); - entity.HasIndex(u => u.Usersay).IsUnique(); + // entity.HasIndex(u => u.Usersay).IsUnique(); }); @@ -106,6 +107,7 @@ namespace RAG.Infrastructure.Data entity.Property(d => d.FilePath).IsRequired().HasMaxLength(500); entity.Property(d => d.FileType).IsRequired().HasMaxLength(50); entity.Property(d => d.FileSize).IsRequired(); + entity.Property(d => d.FileContent).IsRequired(); entity.Property(d => d.AccessLevel).IsRequired().HasMaxLength(20); entity.Property(d => d.Status).IsRequired(); entity.Property(d => d.LastVectorizedAt); diff --git a/backend/RAG.Infrastructure/FileStorage/DatabaseFileStorageService.cs b/backend/RAG.Infrastructure/FileStorage/DatabaseFileStorageService.cs new file mode 100644 index 0000000..45ddaf0 --- /dev/null +++ b/backend/RAG.Infrastructure/FileStorage/DatabaseFileStorageService.cs @@ -0,0 +1,51 @@ +using System.IO; +using System.Threading.Tasks; +using RAG.Infrastructure.Data; + +namespace RAG.Infrastructure.FileStorage +{ + public class DatabaseFileStorageService : IFileStorageService + { + private readonly ApplicationDbContext _context; + + public DatabaseFileStorageService(ApplicationDbContext context) + { + _context = context; + } + + public async Task UploadFileAsync(Stream fileStream, string fileName, string contentType) + { + // 读取文件流到字节数组 + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream); + var fileContent = memoryStream.ToArray(); + + // 在这个实现中,我们不实际存储文件路径,而是返回一个标识符 + // 实际使用时,Document实体的FileContent属性会存储文件内容 + return $"db://{fileName}"; + } + + public async Task GetFileStreamAsync(string filePath) + { + // 从数据库中获取文件内容 + // 注意:这里简化了实现,实际应用中需要根据filePath找到对应的Document + // 并返回其FileContent属性的流 + var memoryStream = new MemoryStream(); + return memoryStream; + } + + public async Task DeleteFileAsync(string filePath) + { + // 在数据库存储实现中,删除文件实际上是删除Document实体 + // 这里简化了实现 + return true; + } + + public Task FileExistsAsync(string filePath) + { + // 检查数据库中是否存在对应的文件 + // 这里简化了实现 + return Task.FromResult(true); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs deleted file mode 100644 index 6294fae..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.Designer.cs +++ /dev/null @@ -1,445 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250806074151_AddVectorizationFieldsToDocument")] - partial class AddVectorizationFieldsToDocument - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("FullName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs b/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs deleted file mode 100644 index ef87a80..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250806074151_AddVectorizationFieldsToDocument.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class AddVectorizationFieldsToDocument : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Username = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - FullName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ChatHistories", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - SessionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - UserId = table.Column(type: "bigint", nullable: false), - UserId1 = table.Column(type: "bigint", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ChatHistories", x => x.Id); - table.ForeignKey( - name: "FK_ChatHistories_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ChatHistories_Users_UserId1", - column: x => x.UserId1, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Documents", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - VectorizationStatus = table.Column(type: "integer", nullable: false), - VectorizationErrorMessage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - VectorizedChunks = table.Column(type: "integer", nullable: false), - TotalChunks = table.Column(type: "integer", nullable: false), - LastVectorizedAt = table.Column(type: "timestamp with time zone", nullable: true), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - FileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - FilePath = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - FileType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - FileSize = table.Column(type: "bigint", nullable: false), - AccessLevel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Status = table.Column(type: "integer", nullable: false), - UserId = table.Column(type: "bigint", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Documents", x => x.Id); - table.ForeignKey( - name: "FK_Documents_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "ChatMessages", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ChatHistoryId = table.Column(type: "bigint", nullable: false), - ChatHistoryId1 = table.Column(type: "bigint", nullable: false), - Content = table.Column(type: "text", nullable: false), - IsUserMessage = table.Column(type: "boolean", nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ChatMessages", x => x.Id); - table.ForeignKey( - name: "FK_ChatMessages_ChatHistories_ChatHistoryId", - column: x => x.ChatHistoryId, - principalTable: "ChatHistories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ChatMessages_ChatHistories_ChatHistoryId1", - column: x => x.ChatHistoryId1, - principalTable: "ChatHistories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "DocumentChunks", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - DocumentId = table.Column(type: "bigint", nullable: false), - Content = table.Column(type: "text", nullable: false), - Position = table.Column(type: "integer", nullable: false), - TokenCount = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DocumentChunks", x => x.Id); - table.ForeignKey( - name: "FK_DocumentChunks_Documents_DocumentId", - column: x => x.DocumentId, - principalTable: "Documents", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MessageReferences", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ChatMessageId = table.Column(type: "bigint", nullable: false), - ChatMessageId1 = table.Column(type: "bigint", nullable: false), - DocumentChunkId = table.Column(type: "bigint", nullable: false), - DocumentChunkId1 = table.Column(type: "bigint", nullable: false), - RelevanceScore = table.Column(type: "double precision", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MessageReferences", x => x.Id); - table.ForeignKey( - name: "FK_MessageReferences_ChatMessages_ChatMessageId", - column: x => x.ChatMessageId, - principalTable: "ChatMessages", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MessageReferences_ChatMessages_ChatMessageId1", - column: x => x.ChatMessageId1, - principalTable: "ChatMessages", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MessageReferences_DocumentChunks_DocumentChunkId", - column: x => x.DocumentChunkId, - principalTable: "DocumentChunks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_MessageReferences_DocumentChunks_DocumentChunkId1", - column: x => x.DocumentChunkId1, - principalTable: "DocumentChunks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "VectorStores", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - DocumentChunkId = table.Column(type: "bigint", nullable: false), - Vector = table.Column(type: "bytea", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VectorStores", x => x.Id); - table.ForeignKey( - name: "FK_VectorStores_DocumentChunks_DocumentChunkId", - column: x => x.DocumentChunkId, - principalTable: "DocumentChunks", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ChatHistories_SessionId", - table: "ChatHistories", - column: "SessionId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ChatHistories_UserId", - table: "ChatHistories", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_ChatHistories_UserId1", - table: "ChatHistories", - column: "UserId1"); - - migrationBuilder.CreateIndex( - name: "IX_ChatMessages_ChatHistoryId", - table: "ChatMessages", - column: "ChatHistoryId"); - - migrationBuilder.CreateIndex( - name: "IX_ChatMessages_ChatHistoryId1", - table: "ChatMessages", - column: "ChatHistoryId1"); - - migrationBuilder.CreateIndex( - name: "IX_ChatMessages_Timestamp", - table: "ChatMessages", - column: "Timestamp"); - - migrationBuilder.CreateIndex( - name: "IX_DocumentChunks_DocumentId", - table: "DocumentChunks", - column: "DocumentId"); - - migrationBuilder.CreateIndex( - name: "IX_Documents_AccessLevel", - table: "Documents", - column: "AccessLevel"); - - migrationBuilder.CreateIndex( - name: "IX_Documents_Status", - table: "Documents", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_Documents_UserId", - table: "Documents", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_Documents_VectorizationStatus", - table: "Documents", - column: "VectorizationStatus"); - - migrationBuilder.CreateIndex( - name: "IX_MessageReferences_ChatMessageId_DocumentChunkId", - table: "MessageReferences", - columns: new[] { "ChatMessageId", "DocumentChunkId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_MessageReferences_ChatMessageId1", - table: "MessageReferences", - column: "ChatMessageId1"); - - migrationBuilder.CreateIndex( - name: "IX_MessageReferences_DocumentChunkId", - table: "MessageReferences", - column: "DocumentChunkId"); - - migrationBuilder.CreateIndex( - name: "IX_MessageReferences_DocumentChunkId1", - table: "MessageReferences", - column: "DocumentChunkId1"); - - migrationBuilder.CreateIndex( - name: "IX_Users_Email", - table: "Users", - column: "Email", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - table: "Users", - column: "Username", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_VectorStores_DocumentChunkId", - table: "VectorStores", - column: "DocumentChunkId", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "MessageReferences"); - - migrationBuilder.DropTable( - name: "VectorStores"); - - migrationBuilder.DropTable( - name: "ChatMessages"); - - migrationBuilder.DropTable( - name: "DocumentChunks"); - - migrationBuilder.DropTable( - name: "ChatHistories"); - - migrationBuilder.DropTable( - name: "Documents"); - - migrationBuilder.DropTable( - name: "Users"); - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs deleted file mode 100644 index 96e4876..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.Designer.cs +++ /dev/null @@ -1,445 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250806075252_UpdateUserTableStringTypes")] - partial class UpdateUserTableStringTypes - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("FullName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs b/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs deleted file mode 100644 index 1104e38..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250806075252_UpdateUserTableStringTypes.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UpdateUserTableStringTypes : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Username", - table: "Users", - type: "text", - maxLength: 50, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(50)", - oldMaxLength: 50); - - migrationBuilder.AlterColumn( - name: "PasswordHash", - table: "Users", - type: "text", - maxLength: 255, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(255)", - oldMaxLength: 255); - - migrationBuilder.AlterColumn( - name: "FullName", - table: "Users", - type: "text", - maxLength: 100, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(100)", - oldMaxLength: 100); - - migrationBuilder.AlterColumn( - name: "Email", - table: "Users", - type: "text", - maxLength: 100, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(100)", - oldMaxLength: 100); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Username", - table: "Users", - type: "character varying(50)", - maxLength: 50, - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldMaxLength: 50); - - migrationBuilder.AlterColumn( - name: "PasswordHash", - table: "Users", - type: "character varying(255)", - maxLength: 255, - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldMaxLength: 255); - - migrationBuilder.AlterColumn( - name: "FullName", - table: "Users", - type: "character varying(100)", - maxLength: 100, - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldMaxLength: 100); - - migrationBuilder.AlterColumn( - name: "Email", - table: "Users", - type: "character varying(100)", - maxLength: 100, - nullable: false, - oldClrType: typeof(string), - oldType: "text", - oldMaxLength: 100); - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs deleted file mode 100644 index 4b858c2..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.Designer.cs +++ /dev/null @@ -1,525 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250806091942_AddVectorizationJobEntity")] - partial class AddVectorizationJobEntity - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("FullName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("PasswordHash") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs b/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs deleted file mode 100644 index 0358633..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250806091942_AddVectorizationJobEntity.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class AddVectorizationJobEntity : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "VectorizationJobs", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - DocumentIds = table.Column>(type: "bigint[]", nullable: false), - Parameters_ModelName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Parameters_ChunkSize = table.Column(type: "integer", nullable: false), - Parameters_ChunkOverlap = table.Column(type: "integer", nullable: false), - Parameters_Separator = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), - Status = table.Column(type: "integer", nullable: false), - ProcessedCount = table.Column(type: "integer", nullable: false), - TotalCount = table.Column(type: "integer", nullable: false), - ErrorMessage = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VectorizationJobs", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_VectorizationJobs_CreatedAt", - table: "VectorizationJobs", - column: "CreatedAt"); - - migrationBuilder.CreateIndex( - name: "IX_VectorizationJobs_Status", - table: "VectorizationJobs", - column: "Status"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "VectorizationJobs"); - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs deleted file mode 100644 index d342e5e..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.Designer.cs +++ /dev/null @@ -1,531 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250807001255_auth2")] - partial class auth2 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs b/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs deleted file mode 100644 index a4fce14..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250807001255_auth2.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class auth2 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "FullName", - table: "Users"); - - migrationBuilder.DropColumn( - name: "IsActive", - table: "Users"); - - migrationBuilder.RenameColumn( - name: "Username", - table: "Users", - newName: "UserName"); - - migrationBuilder.RenameColumn( - name: "PasswordHash", - table: "Users", - newName: "Password"); - - migrationBuilder.RenameIndex( - name: "IX_Users_Username", - table: "Users", - newName: "IX_Users_UserName"); - - migrationBuilder.AddColumn( - name: "Avatar", - table: "Users", - type: "text", - nullable: true); - - migrationBuilder.AddColumn( - name: "Nickname", - table: "Users", - type: "text", - maxLength: 100, - nullable: true); - - migrationBuilder.AddColumn( - name: "Salt", - table: "Users", - type: "text", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "Telephone", - table: "Users", - type: "text", - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Avatar", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Nickname", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Salt", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Telephone", - table: "Users"); - - migrationBuilder.RenameColumn( - name: "UserName", - table: "Users", - newName: "Username"); - - migrationBuilder.RenameColumn( - name: "Password", - table: "Users", - newName: "PasswordHash"); - - migrationBuilder.RenameIndex( - name: "IX_Users_UserName", - table: "Users", - newName: "IX_Users_Username"); - - migrationBuilder.AddColumn( - name: "FullName", - table: "Users", - type: "text", - maxLength: 100, - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "IsActive", - table: "Users", - type: "boolean", - nullable: false, - defaultValue: false); - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs deleted file mode 100644 index 950f9d6..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.Designer.cs +++ /dev/null @@ -1,531 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811022154_UserSay")] - partial class UserSay - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs b/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs deleted file mode 100644 index 3b08974..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811022154_UserSay.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UserSay : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs deleted file mode 100644 index 56a0795..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.Designer.cs +++ /dev/null @@ -1,531 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811064737_UserSay1")] - partial class UserSay1 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs b/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs deleted file mode 100644 index dafd086..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811064737_UserSay1.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UserSay1 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs deleted file mode 100644 index 8c145f5..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.Designer.cs +++ /dev/null @@ -1,531 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811070501_UserSay2")] - partial class UserSay2 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs b/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs deleted file mode 100644 index d02a92c..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811070501_UserSay2.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UserSay2 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs deleted file mode 100644 index 40e3423..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.Designer.cs +++ /dev/null @@ -1,567 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811083039_UserSay3")] - partial class UserSay3 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UersayVector") - .IsRequired() - .HasColumnType("vector(1024)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Usersay") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Usersay") - .IsUnique(); - - b.ToTable("UserSays"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs b/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs deleted file mode 100644 index 69995b3..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811083039_UserSay3.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UserSay3 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "UserSays", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Usersay = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - UersayVector = table.Column(type: "vector(1024)", nullable: false), - Message = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserSays", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_UserSays_Usersay", - table: "UserSays", - column: "Usersay", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "UserSays"); - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs deleted file mode 100644 index 1ad34bb..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.Designer.cs +++ /dev/null @@ -1,568 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811083522_UserSay5")] - partial class UserSay5 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UersayVector") - .IsRequired() - .HasColumnType("vector(1024)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Usersay") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Usersay") - .IsUnique(); - - b.ToTable("UserSays"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs b/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs deleted file mode 100644 index 6caa032..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811083522_UserSay5.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UserSay5 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs deleted file mode 100644 index b11618d..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.Designer.cs +++ /dev/null @@ -1,568 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using RAG.Infrastructure.Data; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811092752_UserSay6")] - partial class UserSay6 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("SessionId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("UserId1") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("SessionId") - .IsUnique(); - - b.HasIndex("UserId"); - - b.HasIndex("UserId1"); - - b.ToTable("ChatHistories"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatHistoryId") - .HasColumnType("bigint"); - - b.Property("ChatHistoryId1") - .HasColumnType("bigint"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsUserMessage") - .HasColumnType("boolean"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatHistoryId"); - - b.HasIndex("ChatHistoryId1"); - - b.HasIndex("Timestamp"); - - b.ToTable("ChatMessages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessLevel") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("FilePath") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("FileType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastVectorizedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("TotalChunks") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.Property("VectorizationErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VectorizationStatus") - .HasColumnType("integer"); - - b.Property("VectorizedChunks") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("AccessLevel"); - - b.HasIndex("Status"); - - b.HasIndex("UserId"); - - b.HasIndex("VectorizationStatus"); - - b.ToTable("Documents"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentId") - .HasColumnType("bigint"); - - b.Property("Position") - .HasColumnType("integer"); - - b.Property("TokenCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("DocumentId"); - - b.ToTable("DocumentChunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChatMessageId") - .HasColumnType("bigint"); - - b.Property("ChatMessageId1") - .HasColumnType("bigint"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("DocumentChunkId1") - .HasColumnType("bigint"); - - b.Property("RelevanceScore") - .HasColumnType("double precision"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ChatMessageId1"); - - b.HasIndex("DocumentChunkId"); - - b.HasIndex("DocumentChunkId1"); - - b.HasIndex("ChatMessageId", "DocumentChunkId") - .IsUnique(); - - b.ToTable("MessageReferences"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Avatar") - .HasColumnType("text"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Nickname") - .HasMaxLength(100) - .HasColumnType("text"); - - b.Property("Password") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("text"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("text"); - - b.Property("Telephone") - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Message") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("UersayVector") - .IsRequired() - .HasColumnType("vector(1024)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Usersay") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("Usersay") - .IsUnique(); - - b.ToTable("UserSays"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property>("DocumentIds") - .IsRequired() - .HasColumnType("bigint[]"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("ProcessedCount") - .HasColumnType("integer"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TotalCount") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAt"); - - b.HasIndex("Status"); - - b.ToTable("VectorizationJobs"); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DocumentChunkId") - .HasColumnType("bigint"); - - b.Property("Vector") - .IsRequired() - .HasColumnType("bytea"); - - b.HasKey("Id"); - - b.HasIndex("DocumentChunkId") - .IsUnique(); - - b.ToTable("VectorStores"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("ChatHistories") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.HasOne("RAG.Domain.Entities.ChatHistory", null) - .WithMany("Messages") - .HasForeignKey("ChatHistoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") - .WithMany() - .HasForeignKey("ChatHistoryId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatHistory"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.HasOne("RAG.Domain.Entities.User", null) - .WithMany("Documents") - .HasForeignKey("UserId"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => - { - b.HasOne("RAG.Domain.Entities.Document", null) - .WithMany("Chunks") - .HasForeignKey("DocumentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => - { - b.HasOne("RAG.Domain.Entities.ChatMessage", null) - .WithMany("References") - .HasForeignKey("ChatMessageId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") - .WithMany() - .HasForeignKey("ChatMessageId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") - .WithMany() - .HasForeignKey("DocumentChunkId1") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("ChatMessage"); - - b.Navigation("DocumentChunk"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => - { - b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => - { - b1.Property("VectorizationJobId") - .HasColumnType("bigint"); - - b1.Property("ChunkOverlap") - .HasColumnType("integer"); - - b1.Property("ChunkSize") - .HasColumnType("integer"); - - b1.Property("ModelName") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b1.Property("Separator") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b1.HasKey("VectorizationJobId"); - - b1.ToTable("VectorizationJobs"); - - b1.WithOwner() - .HasForeignKey("VectorizationJobId"); - }); - - b.Navigation("Parameters") - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => - { - b.HasOne("RAG.Domain.Entities.DocumentChunk", null) - .WithMany() - .HasForeignKey("DocumentChunkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => - { - b.Navigation("Messages"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => - { - b.Navigation("References"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.Document", b => - { - b.Navigation("Chunks"); - }); - - modelBuilder.Entity("RAG.Domain.Entities.User", b => - { - b.Navigation("ChatHistories"); - - b.Navigation("Documents"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs b/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs deleted file mode 100644 index 1afefcc..0000000 --- a/backend/RAG.Infrastructure/Migrations/20250811092752_UserSay6.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace RAG.Infrastructure.Migrations -{ - /// - public partial class UserSay6 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs similarity index 98% rename from backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs rename to backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs index 7abf8d7..d08e24b 100644 --- a/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.Designer.cs +++ b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs @@ -13,8 +13,8 @@ using RAG.Infrastructure.Data; namespace RAG.Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20250811083337_UserSay4")] - partial class UserSay4 + [Migration("20250813075416_RemoveUsersayUniqueConstraintV2")] + partial class RemoveUsersayUniqueConstraintV2 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -121,6 +121,10 @@ namespace RAG.Infrastructure.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + b.Property("FileName") .IsRequired() .HasMaxLength(255) @@ -343,9 +347,6 @@ namespace RAG.Infrastructure.Migrations b.HasKey("Id"); - b.HasIndex("Usersay") - .IsUnique(); - b.ToTable("UserSays"); }); diff --git a/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.cs similarity index 38% rename from backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs rename to backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.cs index c89fda2..8d33e68 100644 --- a/backend/RAG.Infrastructure/Migrations/20250811083337_UserSay4.cs +++ b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.cs @@ -1,24 +1,31 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace RAG.Infrastructure.Migrations { /// - public partial class UserSay4 : Migration + public partial class RemoveUsersayUniqueConstraintV2 : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterDatabase() - .Annotation("Npgsql:PostgresExtension:vector", ",,"); + // 删除Usersay字段的唯一索引 + migrationBuilder.DropIndex( + name: "IX_UserSays_Usersay", + table: "UserSays"); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterDatabase() - .OldAnnotation("Npgsql:PostgresExtension:vector", ",,"); + // 恢复Usersay字段的唯一索引 + migrationBuilder.CreateIndex( + name: "IX_UserSays_Usersay", + table: "UserSays", + column: "Usersay", + unique: true); } } } diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 85e74e7..ba7d85d 100644 --- a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -118,6 +118,10 @@ namespace RAG.Infrastructure.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + b.Property("FileName") .IsRequired() .HasMaxLength(255) @@ -340,9 +344,6 @@ namespace RAG.Infrastructure.Migrations b.HasKey("Id"); - b.HasIndex("Usersay") - .IsUnique(); - b.ToTable("UserSays"); }); diff --git a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj index 9b8050f..ee16d56 100644 --- a/backend/RAG.Infrastructure/RAG.Infrastructure.csproj +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -6,6 +6,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs index add19ca..21a483c 100644 --- a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs +++ b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs @@ -22,7 +22,7 @@ namespace RAG.Infrastructure.Repositories //新增用户输入文本,转化为向量 await _dbSet.AddAsync(userSay); - + await _context.SaveChangesAsync(); } -- Gitee From 47748899bf08df5b80e8d4d92b4ab4cdee9ae058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Wed, 13 Aug 2025 17:41:55 +0800 Subject: [PATCH 28/31] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=8D=95=E4=B8=AA?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=85=A8=E9=83=A8=E7=94=A8=E6=88=B7=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E4=BF=A1=E6=81=AF=EF=BC=8C=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=80=BC=E6=9C=AA=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/Controllers/AuthController.cs | 2 +- .../RAG.Api/Controllers/UserSayController.cs | 25 +- backend/RAG.Api/RAG.Api.http | 15 +- backend/RAG.Application/DTOs/UserDto.cs | 2 +- backend/RAG.Application/DTOs/UserSayDto.cs | 3 + .../Services/UserSayService.cs | 88 ++- backend/RAG.Domain/Entities/UserSay.cs | 5 +- .../Repositories/IUserSayRepository.cs | 2 +- .../20250813085130_UserSay20.Designer.cs | 572 ++++++++++++++++++ .../Migrations/20250813085130_UserSay20.cs | 29 + .../20250813090826_UserSay21.Designer.cs | 572 ++++++++++++++++++ .../Migrations/20250813090826_UserSay21.cs | 22 + .../ApplicationDbContextModelSnapshot.cs | 3 + .../Repositories/UserSayREpository.cs | 13 +- 14 files changed, 1316 insertions(+), 37 deletions(-) create mode 100644 backend/RAG.Application/DTOs/UserSayDto.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.cs diff --git a/backend/RAG.Api/Controllers/AuthController.cs b/backend/RAG.Api/Controllers/AuthController.cs index d79f6ba..544ee42 100644 --- a/backend/RAG.Api/Controllers/AuthController.cs +++ b/backend/RAG.Api/Controllers/AuthController.cs @@ -1,6 +1,6 @@ -using Admin.application.Dtos; using Microsoft.AspNetCore.Mvc; +using RAG.application.Dtos; using RAG.Application.Services; namespace RAG.Api.Controllers; diff --git a/backend/RAG.Api/Controllers/UserSayController.cs b/backend/RAG.Api/Controllers/UserSayController.cs index a32760e..5899fa5 100644 --- a/backend/RAG.Api/Controllers/UserSayController.cs +++ b/backend/RAG.Api/Controllers/UserSayController.cs @@ -27,7 +27,7 @@ namespace RAG.Api.Controllers { - await _userSay.AddUserAsync(req.Usersay, req.Message); + await _userSay.AddUserAsync(req.Usersay, req.Message, req.Vid); } @@ -45,17 +45,36 @@ namespace RAG.Api.Controllers } - return Ok(new { msg = "数据添加成功!" }); + return Ok(new { msg = "数据添加成功!", data = req.Usersay }); } + [HttpGet] + public async Task GetAll() + { + var res = await _userSay.SelAllUserAsync(); + return Ok(new { res}); + } + [HttpGet("{vid}")] + public async Task SelId(int vid) + { + var res = await _userSay.GetByIdAsync(vid); + return Ok(new { res }); + } } - public class UsersayRequest + + + + public class UsersayRequest { public required string Usersay { get; set; } public required string Message { get; set; } + public required int Vid { get; set; } + } + + } \ No newline at end of file diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 222918d..37a0b7c 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -227,9 +227,20 @@ POST {{url}}/api/UserSay/Add Content-Type: application/json { - "Usersay":"你37", - "Message":"11111" + "Usersay":"你37111", + "Message":"11111", + "Vid":"11" } +###查询所有用户的输入 +GET {{url}}/api/UserSay +Accept: application/json + + + +###查询一个用户的输入情况 +GET {{url}}/api/UserSay/11 +Accept: application/json + diff --git a/backend/RAG.Application/DTOs/UserDto.cs b/backend/RAG.Application/DTOs/UserDto.cs index 7b8e98f..8de4d6e 100644 --- a/backend/RAG.Application/DTOs/UserDto.cs +++ b/backend/RAG.Application/DTOs/UserDto.cs @@ -1,3 +1,3 @@ -namespace Admin.application.Dtos; +namespace RAG.application.Dtos; public record UserDto(long Id,string UserName, string Email, string Telephone); \ No newline at end of file diff --git a/backend/RAG.Application/DTOs/UserSayDto.cs b/backend/RAG.Application/DTOs/UserSayDto.cs new file mode 100644 index 0000000..36c5683 --- /dev/null +++ b/backend/RAG.Application/DTOs/UserSayDto.cs @@ -0,0 +1,3 @@ +namespace RAG.application.Dtos; + +public record UserSayDto(string UserSay, string Message, int vid); \ No newline at end of file diff --git a/backend/RAG.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs index 73701fb..fe2c62c 100644 --- a/backend/RAG.Application/Services/UserSayService.cs +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -1,6 +1,8 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using RaG.Application.Common; +using RAG.application.Dtos; using RAG.Domain.Entities; using RAG.Domain.Repositories; @@ -15,19 +17,19 @@ public class UserSayService _userSay = userSay; } - public async Task AddUserAsync(string Usersay,string Message) + public async Task AddUserAsync(string Usersay, string Message, int vid) { - - using var client = new HttpClient(); + + using var client = new HttpClient(); const string apiKey = "sk-ddad3c79867b422c99fe0ed3430ee32c"; // 替换为你的API Key const string apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; //构建请求体 - var requestBody = new + var requestBody = new { model = "text-embedding-v4", - input =Usersay, + input = Usersay, dimensions = 1024, // 注意这里是整数类型,不是字符串 encoding_format = "float" }; @@ -36,8 +38,8 @@ public class UserSayService // 序列化请求体 var jsonContent = JsonSerializer.Serialize(requestBody); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", apiKey); // 发送POST请求 var response = await client.PostAsync(apiUrl, content); @@ -50,12 +52,12 @@ public class UserSayService Console.WriteLine(responseJson); // 假设jsonResponse是API返回的JSON字符串 -try -{ - // 反序列化JSON - var response1 = JsonSerializer.Deserialize(responseJson); + try + { + // 反序列化JSON + var response1 = JsonSerializer.Deserialize(responseJson); - // 检查数据是否存在 + // 检查数据是否存在 if (response1?.Data?.Length > 0) { // 获取第一个embedding向量 @@ -66,10 +68,10 @@ try for (int i = 0; i < 5; i++) { Console.WriteLine(embeddingVector[i]); - - } - await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector)); + + } + await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector, vid)); // 完整向量可以用于后续处理 // 例如: 存储到数据库或进行向量计算 @@ -78,15 +80,15 @@ try { Console.WriteLine("响应中没有包含embedding数据"); } -} -catch (JsonException ex) -{ - Console.WriteLine($"JSON解析错误: {ex.Message}"); -} - - - - + } + catch (JsonException ex) + { + Console.WriteLine($"JSON解析错误: {ex.Message}"); + } + + + + } else { @@ -101,8 +103,44 @@ catch (JsonException ex) } //新增用户输入文本,转化为向量 } - + + + //查询用户输入文本 + public async Task SelAllUserAsync() + { + + var res = await _userSay.SelAllAsync(); + if (res == null) + { + return null; + } + return ApiResult.Success(res); + } + + public async Task GetByIdAsync(int vid) + + { + + var res = await _userSay.GetSayUserAsync(vid); + if (res == null) + { + return ApiResult.Fail(0); + } + foreach (var u in res) + { + System.Console.WriteLine(u.Usersay); + + } + + return ApiResult.Success(new UserSayDto("", "",11 )); + } + + + } + + + diff --git a/backend/RAG.Domain/Entities/UserSay.cs b/backend/RAG.Domain/Entities/UserSay.cs index 018d80f..b4ab661 100644 --- a/backend/RAG.Domain/Entities/UserSay.cs +++ b/backend/RAG.Domain/Entities/UserSay.cs @@ -13,12 +13,15 @@ namespace RAG.Domain.Entities //用户发送消息的向量化结果 public float[] UersayVector { get; private set; } public string Message { get; private set; } + public int Vid { get; private set; } - public UserSay(string usersay, string message, float[] uersayVector) + public UserSay(string usersay, string message, float[] uersayVector, int vid) { this.Usersay = usersay; this.Message = message; this.UersayVector = uersayVector; + Vid = vid; + } diff --git a/backend/RAG.Domain/Repositories/IUserSayRepository.cs b/backend/RAG.Domain/Repositories/IUserSayRepository.cs index b7684fb..03a72fe 100644 --- a/backend/RAG.Domain/Repositories/IUserSayRepository.cs +++ b/backend/RAG.Domain/Repositories/IUserSayRepository.cs @@ -9,7 +9,7 @@ namespace RAG.Domain.Repositories //获取用户id - Task GetSayUserAsync(long id); + Task> GetSayUserAsync(int Vid); //添加文本数据,将数据向量化 Task AddUserSayAsync(UserSay userSay); //删除数据 diff --git a/backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.Designer.cs new file mode 100644 index 0000000..de0cf59 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.Designer.cs @@ -0,0 +1,572 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250813085130_UserSay20")] + partial class UserSay20 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Vid") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.cs b/backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.cs new file mode 100644 index 0000000..7f705e2 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250813085130_UserSay20.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay20 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Vid", + table: "UserSays", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Vid", + table: "UserSays"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.Designer.cs new file mode 100644 index 0000000..89eaaf2 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.Designer.cs @@ -0,0 +1,572 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250813090826_UserSay21")] + partial class UserSay21 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Vid") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.cs b/backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.cs new file mode 100644 index 0000000..8fbef7c --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250813090826_UserSay21.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay21 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index ba7d85d..3d10652 100644 --- a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -342,6 +342,9 @@ namespace RAG.Infrastructure.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("Vid") + .HasColumnType("integer"); + b.HasKey("Id"); b.ToTable("UserSays"); diff --git a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs index 21a483c..3fe6e28 100644 --- a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs +++ b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs @@ -41,12 +41,19 @@ namespace RAG.Infrastructure.Repositories _dbSet.Remove(user); // 同步操作,无需 await await _context.SaveChangesAsync(); // 异步保存更改 } - - public async Task GetSayUserAsync(long id) + + public async Task> GetSayUserAsync(int vid) { //获取单个用户输入文本,转化为向量 - return await _dbSet.FindAsync(id); + var res = await _context.UserSays + .Where(u => u.Vid == vid ) + .ToListAsync(); + await _context.SaveChangesAsync(); + return res; + + + } -- Gitee From 0ce48f160fd161d7658480ea5adfbeeff2e475ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Thu, 14 Aug 2025 08:46:05 +0800 Subject: [PATCH 29/31] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=9F=A5=E8=AF=A2=E5=8D=95=E4=B8=AA=E4=B8=8E?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=80=BC=E6=9C=AA=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/RAG.Api/RAG.Api.http | 3 +-- backend/RAG.Application/Services/UserSayService.cs | 8 ++------ backend/RAG.Domain/Repositories/IUserSayRepository.cs | 2 +- .../RAG.Infrastructure/Repositories/UserSayREpository.cs | 8 +++----- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 37a0b7c..58dbedb 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -241,6 +241,5 @@ Accept: application/json ###查询一个用户的输入情况 -GET {{url}}/api/UserSay/11 -Accept: application/json +GET {{url}}/api/UserSay/1 diff --git a/backend/RAG.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs index fe2c62c..1fe68bc 100644 --- a/backend/RAG.Application/Services/UserSayService.cs +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -126,13 +126,9 @@ public class UserSayService { return ApiResult.Fail(0); } - foreach (var u in res) - { - System.Console.WriteLine(u.Usersay); - - } + - return ApiResult.Success(new UserSayDto("", "",11 )); + return ApiResult.Success(res,"成功"); } diff --git a/backend/RAG.Domain/Repositories/IUserSayRepository.cs b/backend/RAG.Domain/Repositories/IUserSayRepository.cs index 03a72fe..a44d7b2 100644 --- a/backend/RAG.Domain/Repositories/IUserSayRepository.cs +++ b/backend/RAG.Domain/Repositories/IUserSayRepository.cs @@ -9,7 +9,7 @@ namespace RAG.Domain.Repositories //获取用户id - Task> GetSayUserAsync(int Vid); + Task> GetSayUserAsync(int vid); //添加文本数据,将数据向量化 Task AddUserSayAsync(UserSay userSay); //删除数据 diff --git a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs index 3fe6e28..3b94402 100644 --- a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs +++ b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs @@ -49,12 +49,10 @@ namespace RAG.Infrastructure.Repositories var res = await _context.UserSays .Where(u => u.Vid == vid ) .ToListAsync(); - await _context.SaveChangesAsync(); - return res; - - - +UserSay[] array = res.ToArray(); + return array; + } public async Task> SelAllAsync() -- Gitee From a3260c6a0ffcb9303acef52945cb44a6b2ad251b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Thu, 14 Aug 2025 11:04:08 +0800 Subject: [PATCH 30/31] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=B0=86=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=97=AE=E9=A2=98=E8=BF=9B=E8=A1=8C=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E9=97=AE=E9=A2=98=E7=AD=94=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/UserSayController.cs | 6 +- backend/RAG.Api/RAG.Api.http | 7 +- .../Services/KnowledgeBaseService.cs | 3 +- .../Services/UserSayService.cs | 195 ++++-- .../Services/VectorizationService.cs | 5 + backend/RAG.Domain/Entities/UserSay.cs | 12 +- .../Data/ApplicationDbContext.cs | 5 +- .../20250814023800_UserSay22.Designer.cs | 571 +++++++++++++++++ .../Migrations/20250814023800_UserSay22.cs | 36 ++ .../20250814024859_UserSay23.Designer.cs | 575 ++++++++++++++++++ .../Migrations/20250814024859_UserSay23.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 7 +- 12 files changed, 1383 insertions(+), 68 deletions(-) create mode 100644 backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.cs diff --git a/backend/RAG.Api/Controllers/UserSayController.cs b/backend/RAG.Api/Controllers/UserSayController.cs index 5899fa5..15dd21e 100644 --- a/backend/RAG.Api/Controllers/UserSayController.cs +++ b/backend/RAG.Api/Controllers/UserSayController.cs @@ -27,7 +27,7 @@ namespace RAG.Api.Controllers { - await _userSay.AddUserAsync(req.Usersay, req.Message, req.Vid); + await _userSay.AddUserAsync(req.Usersay, req.Message, req.Vid,req.Content); } @@ -45,7 +45,7 @@ namespace RAG.Api.Controllers } - return Ok(new { msg = "数据添加成功!", data = req.Usersay }); + return Ok(new { msg = "数据添加成功!", data = req }); } @@ -73,6 +73,8 @@ namespace RAG.Api.Controllers public required string Usersay { get; set; } public required string Message { get; set; } public required int Vid { get; set; } + public required string Content { get; set; } + } diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index 58dbedb..e133b00 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -227,9 +227,10 @@ POST {{url}}/api/UserSay/Add Content-Type: application/json { - "Usersay":"你37111", - "Message":"11111", - "Vid":"11" + "Usersay":"我是戴", + "Message":"", + "Vid":"11", + "content":"我是中国人" } diff --git a/backend/RAG.Application/Services/KnowledgeBaseService.cs b/backend/RAG.Application/Services/KnowledgeBaseService.cs index 4483549..bf82892 100644 --- a/backend/RAG.Application/Services/KnowledgeBaseService.cs +++ b/backend/RAG.Application/Services/KnowledgeBaseService.cs @@ -80,8 +80,9 @@ namespace RAG.Application.Services // 创建知识库实体 var knowledgeBase = KnowledgeBase.Create(createDto.Name, createDto.Description); - + // 保存到数据库 + System.Console.WriteLine(1111111); await _knowledgeBaseRepository.AddAsync(knowledgeBase); await _unitOfWork.SaveChangesAsync(); diff --git a/backend/RAG.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs index 1fe68bc..414ad65 100644 --- a/backend/RAG.Application/Services/UserSayService.cs +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -2,110 +2,161 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; using RaG.Application.Common; -using RAG.application.Dtos; using RAG.Domain.Entities; using RAG.Domain.Repositories; + namespace RAG.Application.Services; public class UserSayService { + + private static readonly HttpClient httpClient = new HttpClient(); + private readonly IUserSayRepository _userSay; public UserSayService(IUserSayRepository userSay) { _userSay = userSay; } - public async Task AddUserAsync(string Usersay, string Message, int vid) + public async Task AddUserAsync(string Usersay, string Message, int vid,string Content) { + - using var client = new HttpClient(); const string apiKey = "sk-ddad3c79867b422c99fe0ed3430ee32c"; // 替换为你的API Key const string apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; + const string url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; + System.Console.WriteLine(vid); +System.Console.WriteLine(Usersay); - //构建请求体 - var requestBody = new - { - model = "text-embedding-v4", - input = Usersay, - dimensions = 1024, // 注意这里是整数类型,不是字符串 - encoding_format = "float" - }; +// 定义请求模型 +var request = new +{ + model = "qwen-plus", + messages = new[] + { + new { role = "user", content = Usersay } + } +}; + +// 序列化为 JSON +string jsonContent1 = JsonSerializer.Serialize(request, new JsonSerializerOptions +{ + WriteIndented = true // 可选:美化输出 +}); + + string result = await SendPostRequestAsync(url, jsonContent1, apiKey); + System.Console.WriteLine(result); try { - // 序列化请求体 - var jsonContent = JsonSerializer.Serialize(requestBody); - var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", apiKey); + using JsonDocument doc = JsonDocument.Parse(result); + var content1 = doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString(); + Content = content1; - // 发送POST请求 - var response = await client.PostAsync(apiUrl, content); + System.Console.WriteLine(Content); - // 处理响应 - if (response.IsSuccessStatusCode) + + System.Console.WriteLine(content1); + //构建请求体 + var requestBody = new + { + model = "text-embedding-v4", + input = Usersay, + dimensions = 1024, // 注意这里是整数类型,不是字符串 + encoding_format = "float" + }; + try { - var responseJson = await response.Content.ReadAsStringAsync(); - Console.WriteLine("请求成功!响应结果:"); - Console.WriteLine(responseJson); - // 假设jsonResponse是API返回的JSON字符串 + // 序列化请求体 + var jsonContent = JsonSerializer.Serialize(requestBody); + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", apiKey); + + // 发送POST请求 + var response = await httpClient.PostAsync(apiUrl, content); - try + // 处理响应 + if (response.IsSuccessStatusCode) { - // 反序列化JSON - var response1 = JsonSerializer.Deserialize(responseJson); + var responseJson = await response.Content.ReadAsStringAsync(); + Console.WriteLine("请求成功!响应结果:"); + Console.WriteLine(responseJson); + // 假设jsonResponse是API返回的JSON字符串 - // 检查数据是否存在 - if (response1?.Data?.Length > 0) + try { - // 获取第一个embedding向量 - float[] embeddingVector = response1.Data[0].Embedding; + // 反序列化JSON + var response1 = JsonSerializer.Deserialize(responseJson); - Console.WriteLine($"向量维度: {embeddingVector.Length}"); - Console.WriteLine("前5个维度值:"); - for (int i = 0; i < 5; i++) + // 检查数据是否存在 + if (response1?.Data?.Length > 0) { - Console.WriteLine(embeddingVector[i]); + // 获取第一个embedding向量 + float[] embeddingVector = response1.Data[0].Embedding; + Console.WriteLine($"向量维度: {embeddingVector.Length}"); + Console.WriteLine("前5个维度值:"); + for (int i = 0; i < 5; i++) + { + Console.WriteLine(embeddingVector[i]); - } - await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector, vid)); - // 完整向量可以用于后续处理 - // 例如: 存储到数据库或进行向量计算 + } + + await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector, vid, Content)); + + + // 完整向量可以用于后续处理 + // 例如: 存储到数据库或进行向量计算 + } + else + { + Console.WriteLine("响应中没有包含embedding数据"); + } } - else + catch (JsonException ex) { - Console.WriteLine("响应中没有包含embedding数据"); + Console.WriteLine($"JSON解析错误: {ex.Message}"); } - } - catch (JsonException ex) - { - Console.WriteLine($"JSON解析错误: {ex.Message}"); - } + } + else + { + Console.WriteLine($"请求失败,状态码:{(int)response.StatusCode} ({response.ReasonPhrase})"); + var errorDetails = await response.Content.ReadAsStringAsync(); + Console.WriteLine("错误详情:" + errorDetails); + } } - else + catch (Exception ex) { - Console.WriteLine($"请求失败,状态码:{(int)response.StatusCode} ({response.ReasonPhrase})"); - var errorDetails = await response.Content.ReadAsStringAsync(); - Console.WriteLine("错误详情:" + errorDetails); + Console.WriteLine($"发生异常:{ex.Message}"); } } catch (Exception ex) { - Console.WriteLine($"发生异常:{ex.Message}"); + System.Console.WriteLine(ex); } - //新增用户输入文本,转化为向量 } + //新增用户输入文本,转化为向量 + + - //查询用户输入文本 + + + + + //查询用户输入文本 public async Task SelAllUserAsync() { @@ -126,14 +177,44 @@ public class UserSayService { return ApiResult.Fail(0); } - - - return ApiResult.Success(res,"成功"); + + + return ApiResult.Success(res, "成功"); + + + + } + + private static async Task SendPostRequestAsync(string url, string jsonContent, string apiKey) + { + using (var content = new StringContent(jsonContent, Encoding.UTF8, "application/json")) + { + // 设置请求头 + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // 发送请求并获取响应 + HttpResponseMessage response = await httpClient.PostAsync(url, content); + + // 处理响应 + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsStringAsync(); + + } + else + { + return $"请求失败: {response.StatusCode}"; + } } + } + +} + - } + diff --git a/backend/RAG.Application/Services/VectorizationService.cs b/backend/RAG.Application/Services/VectorizationService.cs index 989db80..4a3f454 100644 --- a/backend/RAG.Application/Services/VectorizationService.cs +++ b/backend/RAG.Application/Services/VectorizationService.cs @@ -34,6 +34,8 @@ namespace RAG.Application.Services // 获取文档 var document = await _documentRepository.GetByIdAsync((int)documentId); + System.Console.WriteLine("获取文档"); + System.Console.WriteLine(document); if (document == null) { _logger.LogError("Document with id {DocumentId} not found", documentId); @@ -55,6 +57,9 @@ namespace RAG.Application.Services content = await reader.ReadToEndAsync(); } + System.Console.WriteLine("读取文档内容"); + System.Console.WriteLine(content); + // 文本分块 var chunks = SplitTextIntoChunks( content, diff --git a/backend/RAG.Domain/Entities/UserSay.cs b/backend/RAG.Domain/Entities/UserSay.cs index b4ab661..eb73019 100644 --- a/backend/RAG.Domain/Entities/UserSay.cs +++ b/backend/RAG.Domain/Entities/UserSay.cs @@ -14,14 +14,24 @@ namespace RAG.Domain.Entities public float[] UersayVector { get; private set; } public string Message { get; private set; } public int Vid { get; private set; } + public string Content { get; private set; } - public UserSay(string usersay, string message, float[] uersayVector, int vid) + + public UserSay(string usersay, string message, float[] uersayVector, int vid, string content) { this.Usersay = usersay; this.Message = message; this.UersayVector = uersayVector; Vid = vid; + Content = content; + + + + + //this.Content = content; + //this. = title; + } diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs index 172be1c..8b9e757 100644 --- a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -69,8 +69,9 @@ namespace RAG.Infrastructure.Data .HasMaxLength(50); entity.Property(u => u.Message) - .IsRequired() - .HasMaxLength(100); + .HasColumnType("text"); + entity.Property(u => u.Content) + .HasColumnType("text"); entity.Property(x => x.UersayVector) .HasColumnType("vector(1024)") diff --git a/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.Designer.cs new file mode 100644 index 0000000..95e6b00 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.Designer.cs @@ -0,0 +1,571 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250814023800_UserSay22")] + partial class UserSay22 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Vid") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.cs b/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.cs new file mode 100644 index 0000000..3e14d34 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay22 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Message", + table: "UserSays", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(100)", + oldMaxLength: 100); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Message", + table: "UserSays", + type: "character varying(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.Designer.cs new file mode 100644 index 0000000..4debc1d --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.Designer.cs @@ -0,0 +1,575 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250814024859_UserSay23")] + partial class UserSay23 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Vid") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.cs b/backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.cs new file mode 100644 index 0000000..92fdb85 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814024859_UserSay23.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay23 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Content", + table: "UserSays", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Content", + table: "UserSays"); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 3d10652..411ea65 100644 --- a/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -322,13 +322,16 @@ namespace RAG.Infrastructure.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); b.Property("Message") .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); + .HasColumnType("text"); b.Property("UersayVector") .IsRequired() -- Gitee From 36909eb5c3c341e6c21d8995e67d84e6cf1a2d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E9=A1=BA?= <3024235926@qq.com> Date: Thu, 14 Aug 2025 13:24:53 +0800 Subject: [PATCH 31/31] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RAG.Api/Controllers/UserSayController.cs | 15 +- backend/RAG.Api/RAG.Api.http | 6 +- .../Services/UserSayService.cs | 8 +- backend/RAG.Domain/Entities/UserSay.cs | 4 +- .../Repositories/IUserSayRepository.cs | 2 +- .../20250814050935_UserSay25.Designer.cs | 572 +++++++++++++++++ .../Migrations/20250814050935_UserSay25.cs | 29 + .../20250814051042_UserSay26.Designer.cs | 572 +++++++++++++++++ .../Migrations/20250814051042_UserSay26.cs | 22 + .../20250814051544_UserSay27.Designer.cs | 575 ++++++++++++++++++ .../Migrations/20250814051544_UserSay27.cs | 29 + .../Repositories/UserSayREpository.cs | 10 +- 12 files changed, 1820 insertions(+), 24 deletions(-) create mode 100644 backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.Designer.cs create mode 100644 backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.cs diff --git a/backend/RAG.Api/Controllers/UserSayController.cs b/backend/RAG.Api/Controllers/UserSayController.cs index 15dd21e..47daa1b 100644 --- a/backend/RAG.Api/Controllers/UserSayController.cs +++ b/backend/RAG.Api/Controllers/UserSayController.cs @@ -27,7 +27,10 @@ namespace RAG.Api.Controllers { - await _userSay.AddUserAsync(req.Usersay, req.Message, req.Vid,req.Content); + await _userSay.AddUserAsync(req.Usersay, req.Message, req.id, req.Content); + + var res = await _userSay.GetByIdAsync(req.id); + return Ok(new { res }); } @@ -45,7 +48,7 @@ namespace RAG.Api.Controllers } - return Ok(new { msg = "数据添加成功!", data = req }); + } @@ -56,10 +59,10 @@ namespace RAG.Api.Controllers return Ok(new { res}); } - [HttpGet("{vid}")] - public async Task SelId(int vid) + [HttpGet("{id}")] + public async Task SelId(long id) { - var res = await _userSay.GetByIdAsync(vid); + var res = await _userSay.GetByIdAsync(id); return Ok(new { res }); } @@ -72,7 +75,7 @@ namespace RAG.Api.Controllers public required string Usersay { get; set; } public required string Message { get; set; } - public required int Vid { get; set; } + public required long id { get; set; } public required string Content { get; set; } diff --git a/backend/RAG.Api/RAG.Api.http b/backend/RAG.Api/RAG.Api.http index e133b00..8919f48 100644 --- a/backend/RAG.Api/RAG.Api.http +++ b/backend/RAG.Api/RAG.Api.http @@ -227,9 +227,9 @@ POST {{url}}/api/UserSay/Add Content-Type: application/json { - "Usersay":"我是戴", + "Usersay":"我是戴戴", "Message":"", - "Vid":"11", + "id":"76", "content":"我是中国人" @@ -242,5 +242,5 @@ Accept: application/json ###查询一个用户的输入情况 -GET {{url}}/api/UserSay/1 +GET {{url}}/api/UserSay/74 diff --git a/backend/RAG.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs index 414ad65..32aa234 100644 --- a/backend/RAG.Application/Services/UserSayService.cs +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -20,7 +20,7 @@ public class UserSayService _userSay = userSay; } - public async Task AddUserAsync(string Usersay, string Message, int vid,string Content) + public async Task AddUserAsync(string Usersay, string Message, long id,string Content) { @@ -28,7 +28,7 @@ public class UserSayService const string apiKey = "sk-ddad3c79867b422c99fe0ed3430ee32c"; // 替换为你的API Key const string apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings"; const string url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; - System.Console.WriteLine(vid); + System.Console.WriteLine(id); System.Console.WriteLine(Usersay); // 定义请求模型 @@ -110,7 +110,7 @@ string jsonContent1 = JsonSerializer.Serialize(request, new JsonSerializerOption } - await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector, vid, Content)); + await _userSay.AddUserSayAsync(new UserSay(Usersay, Message, embeddingVector, Content)); // 完整向量可以用于后续处理 @@ -168,7 +168,7 @@ string jsonContent1 = JsonSerializer.Serialize(request, new JsonSerializerOption return ApiResult.Success(res); } - public async Task GetByIdAsync(int vid) + public async Task GetByIdAsync(long vid) { diff --git a/backend/RAG.Domain/Entities/UserSay.cs b/backend/RAG.Domain/Entities/UserSay.cs index eb73019..35d0bce 100644 --- a/backend/RAG.Domain/Entities/UserSay.cs +++ b/backend/RAG.Domain/Entities/UserSay.cs @@ -13,16 +13,14 @@ namespace RAG.Domain.Entities //用户发送消息的向量化结果 public float[] UersayVector { get; private set; } public string Message { get; private set; } - public int Vid { get; private set; } public string Content { get; private set; } - public UserSay(string usersay, string message, float[] uersayVector, int vid, string content) + public UserSay(string usersay, string message, float[] uersayVector, string content) { this.Usersay = usersay; this.Message = message; this.UersayVector = uersayVector; - Vid = vid; Content = content; diff --git a/backend/RAG.Domain/Repositories/IUserSayRepository.cs b/backend/RAG.Domain/Repositories/IUserSayRepository.cs index a44d7b2..95b8fae 100644 --- a/backend/RAG.Domain/Repositories/IUserSayRepository.cs +++ b/backend/RAG.Domain/Repositories/IUserSayRepository.cs @@ -9,7 +9,7 @@ namespace RAG.Domain.Repositories //获取用户id - Task> GetSayUserAsync(int vid); + Task> GetSayUserAsync(long id); //添加文本数据,将数据向量化 Task AddUserSayAsync(UserSay userSay); //删除数据 diff --git a/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.Designer.cs new file mode 100644 index 0000000..79e4913 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.Designer.cs @@ -0,0 +1,572 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250814050935_UserSay25")] + partial class UserSay25 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.cs b/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.cs new file mode 100644 index 0000000..23a7cbe --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay25 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Vid", + table: "UserSays"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Vid", + table: "UserSays", + type: "integer", + nullable: false, + defaultValue: 0); + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.Designer.cs new file mode 100644 index 0000000..ea09524 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.Designer.cs @@ -0,0 +1,572 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250814051042_UserSay26")] + partial class UserSay26 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.cs b/backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.cs new file mode 100644 index 0000000..b3b366b --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814051042_UserSay26.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay26 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.Designer.cs new file mode 100644 index 0000000..80252cc --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.Designer.cs @@ -0,0 +1,575 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RAG.Infrastructure.Data; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250814051544_UserSay27")] + partial class UserSay27 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("UserId1") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SessionId") + .IsUnique(); + + b.HasIndex("UserId"); + + b.HasIndex("UserId1"); + + b.ToTable("ChatHistories"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatHistoryId") + .HasColumnType("bigint"); + + b.Property("ChatHistoryId1") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUserMessage") + .HasColumnType("boolean"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatHistoryId"); + + b.HasIndex("ChatHistoryId1"); + + b.HasIndex("Timestamp"); + + b.ToTable("ChatMessages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessLevel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileContent") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastVectorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalChunks") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("VectorizationErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VectorizationStatus") + .HasColumnType("integer"); + + b.Property("VectorizedChunks") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessLevel"); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("VectorizationStatus"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentId") + .HasColumnType("bigint"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TokenCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.ToTable("DocumentChunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatMessageId") + .HasColumnType("bigint"); + + b.Property("ChatMessageId1") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("DocumentChunkId1") + .HasColumnType("bigint"); + + b.Property("RelevanceScore") + .HasColumnType("double precision"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChatMessageId1"); + + b.HasIndex("DocumentChunkId"); + + b.HasIndex("DocumentChunkId1"); + + b.HasIndex("ChatMessageId", "DocumentChunkId") + .IsUnique(); + + b.ToTable("MessageReferences"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Nickname") + .HasMaxLength(100) + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("text"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Telephone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.UserSay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("UersayVector") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usersay") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Vid") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UserSays"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property>("DocumentIds") + .IsRequired() + .HasColumnType("bigint[]"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProcessedCount") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TotalCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("Status"); + + b.ToTable("VectorizationJobs"); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DocumentChunkId") + .HasColumnType("bigint"); + + b.Property("Vector") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("DocumentChunkId") + .IsUnique(); + + b.ToTable("VectorStores"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("ChatHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.HasOne("RAG.Domain.Entities.ChatHistory", null) + .WithMany("Messages") + .HasForeignKey("ChatHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatHistory", "ChatHistory") + .WithMany() + .HasForeignKey("ChatHistoryId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatHistory"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.HasOne("RAG.Domain.Entities.User", null) + .WithMany("Documents") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.DocumentChunk", b => + { + b.HasOne("RAG.Domain.Entities.Document", null) + .WithMany("Chunks") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.MessageReference", b => + { + b.HasOne("RAG.Domain.Entities.ChatMessage", null) + .WithMany("References") + .HasForeignKey("ChatMessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.ChatMessage", "ChatMessage") + .WithMany() + .HasForeignKey("ChatMessageId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RAG.Domain.Entities.DocumentChunk", "DocumentChunk") + .WithMany() + .HasForeignKey("DocumentChunkId1") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatMessage"); + + b.Navigation("DocumentChunk"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.VectorizationJob", b => + { + b.OwnsOne("RAG.Domain.Entities.VectorizationParams", "Parameters", b1 => + { + b1.Property("VectorizationJobId") + .HasColumnType("bigint"); + + b1.Property("ChunkOverlap") + .HasColumnType("integer"); + + b1.Property("ChunkSize") + .HasColumnType("integer"); + + b1.Property("ModelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b1.Property("Separator") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b1.HasKey("VectorizationJobId"); + + b1.ToTable("VectorizationJobs"); + + b1.WithOwner() + .HasForeignKey("VectorizationJobId"); + }); + + b.Navigation("Parameters") + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Infrastructure.Data.VectorStore", b => + { + b.HasOne("RAG.Domain.Entities.DocumentChunk", null) + .WithMany() + .HasForeignKey("DocumentChunkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatHistory", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.ChatMessage", b => + { + b.Navigation("References"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.Document", b => + { + b.Navigation("Chunks"); + }); + + modelBuilder.Entity("RAG.Domain.Entities.User", b => + { + b.Navigation("ChatHistories"); + + b.Navigation("Documents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.cs b/backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.cs new file mode 100644 index 0000000..4b2cd2a --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250814051544_UserSay27.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class UserSay27 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Vid", + table: "UserSays", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Vid", + table: "UserSays"); + } + } +} diff --git a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs index 3b94402..f953685 100644 --- a/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs +++ b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs @@ -43,15 +43,11 @@ namespace RAG.Infrastructure.Repositories } - public async Task> GetSayUserAsync(int vid) + public async Task> GetSayUserAsync(long id) { //获取单个用户输入文本,转化为向量 - var res = await _context.UserSays - .Where(u => u.Vid == vid ) - .ToListAsync(); - -UserSay[] array = res.ToArray(); - return array; + return await _dbSet.Where(c => c.Id == id).ToListAsync(); + } -- Gitee