diff --git a/backend/RAG.Api/Controllers/AuthController.cs b/backend/RAG.Api/Controllers/AuthController.cs new file mode 100644 index 0000000000000000000000000000000000000000..544ee429710c96842c7ea50038302ff6f7ef8b12 --- /dev/null +++ b/backend/RAG.Api/Controllers/AuthController.cs @@ -0,0 +1,118 @@ + +using Microsoft.AspNetCore.Mvc; +using RAG.application.Dtos; +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/Controllers/ChatController.cs b/backend/RAG.Api/Controllers/ChatController.cs new file mode 100644 index 0000000000000000000000000000000000000000..6314df2fcd8291b3abcc780c7957c530525f88c4 --- /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 required string Message { get; set; } + public int? TopK { get; set; } + public required string SessionId { get; set; } + } + + public class ChatResponse + { + public required string Query { get; set; } + public required string Response { get; set; } + public required List References { get; set; } + } + + public class Reference + { + public required 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 0000000000000000000000000000000000000000..eb58ddc633f624718528fed221fabbedf2248415 --- /dev/null +++ b/backend/RAG.Api/Controllers/DocumentController.cs @@ -0,0 +1,233 @@ +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 + { + // 正确引用 IDocumentService 所在的命名空间 +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 + { + // 获取文件流、文件名、文件类型和文件大小 + 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) + { + 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 cancellationToken = HttpContext.RequestAborted; + var results = await _documentService.UploadDocumentsAsync(uploadDtos, cancellationToken); + return Ok(new { documents = 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(); + } + + /// + /// 批量更新文档权限 + /// + [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/Controllers/DocumentsController.cs b/backend/RAG.Api/Controllers/DocumentsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..97eddfc7f1c06370f59e53f1d62db59a267ecb20 --- /dev/null +++ b/backend/RAG.Api/Controllers/DocumentsController.cs @@ -0,0 +1,90 @@ +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 + }; + + // 获取文件流、文件名、文件类型和文件大小 + 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); + } + } + + /// + /// 获取文档进度 + /// + [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/KnowledgeBaseController.cs b/backend/RAG.Api/Controllers/KnowledgeBaseController.cs new file mode 100644 index 0000000000000000000000000000000000000000..d8a3c86f4e83c2cb92501645e9db94259a592414 --- /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/TestExceptionController.cs b/backend/RAG.Api/Controllers/TestExceptionController.cs new file mode 100644 index 0000000000000000000000000000000000000000..91c4afe60fbaee8b5a26911394314232c01d1a96 --- /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/Controllers/UserSayController.cs b/backend/RAG.Api/Controllers/UserSayController.cs new file mode 100644 index 0000000000000000000000000000000000000000..47daa1b45c18fb5bada5205fd7777cabdd4ddefc --- /dev/null +++ b/backend/RAG.Api/Controllers/UserSayController.cs @@ -0,0 +1,85 @@ +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, req.id, req.Content); + + var res = await _userSay.GetByIdAsync(req.id); + return Ok(new { res }); + + + } + + + catch (Exception ex) + + + { + + + return BadRequest(new { message = "服务器错误!" + ex.Message }); + + + } + + + + + } + + [HttpGet] + public async Task GetAll() + { + var res = await _userSay.SelAllUserAsync(); + return Ok(new { res}); + } + + [HttpGet("{id}")] + public async Task SelId(long id) + { + var res = await _userSay.GetByIdAsync(id); + return Ok(new { res }); + } + + } + + + + public class UsersayRequest + { + + public required string Usersay { get; set; } + public required string Message { get; set; } + public required long id { get; set; } + public required string Content { get; set; } + + + } + + +} \ 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 0000000000000000000000000000000000000000..467f52b01afd34dca09d7720c2c4ab043b98bf0a --- /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/Middleware/ExceptionMiddleware.cs b/backend/RAG.Api/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..08687da8bf0f27c1426a2546ade9c2b35f3dd40e --- /dev/null +++ b/backend/RAG.Api/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,101 @@ +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 env = context.RequestServices.GetRequiredService(); + + var response = exception switch + { + ValidationException ex => new ErrorResponse + { + Code = "VALIDATION_ERROR", + Message = "输入验证失败", + Details = ex.Errors, + StackTrace = env.IsDevelopment() ? exception.StackTrace : null + }, + NotFoundException ex => new ErrorResponse + { + Code = "NOT_FOUND", + Message = ex.Message, + StackTrace = env.IsDevelopment() ? exception.StackTrace : null + }, + _ => new ErrorResponse + { + Code = "INTERNAL_ERROR", + Message = env.IsDevelopment() ? exception.Message : "服务器内部错误", + Details = env.IsDevelopment() ? exception.InnerException?.Message : null, + StackTrace = env.IsDevelopment() ? exception.StackTrace : null + } + }; + + 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 string StackTrace { 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 new file mode 100644 index 0000000000000000000000000000000000000000..bf6ce3266e977013b9353742f0ea8fa5ea9ec103 --- /dev/null +++ b/backend/RAG.Api/Program.cs @@ -0,0 +1,114 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.EntityFrameworkCore; +using MediatR; +using RAG.Infrastructure.Data; +using RAG.Api.Middleware; +using System.Reflection; +using RAG.Infrastructure; +using RAG.Application; + + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +// 配置数据库上下文 +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// 配置MediatR +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(RAG.Application.Class1).Assembly)); + +// 配置AutoMapper +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.AddScoped(); + +// 注册知识库服务 +builder.Services.AddScoped(); + +// 注册向量存储服务 +builder.Services.AddScoped(); + +// 注册进度服务 +builder.Services.AddScoped(); + +// 注册文档处理器 +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>(); + +// 注册知识库仓储 +builder.Services.AddScoped(); + +// 注册文档仓储 +builder.Services.AddScoped(); + +// 注册文档分块仓储 +builder.Services.AddScoped(); + +// 注册向量化服务 +builder.Services.AddScoped(); + +// 注册工作单元 +builder.Services.AddScoped(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", + builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .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()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +// 注册异常处理中间件 +app.UseMiddleware(); + +app.UseHttpsRedirection(); + +app.UseCors("AllowAll"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + +public partial class Program { } \ No newline at end of file diff --git a/backend/RAG.Api/Properties/launchSettings.json b/backend/RAG.Api/Properties/launchSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..8749fc5330d8920679267f75d51e1b772dd0145f --- /dev/null +++ b/backend/RAG.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5097", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "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 new file mode 100644 index 0000000000000000000000000000000000000000..2a17132ea9ff6955f5328ee62865e31d411af7f0 --- /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 0000000000000000000000000000000000000000..8919f48794f082ec999e8e49fbb601d08e5a8d8d --- /dev/null +++ b/backend/RAG.Api/RAG.Api.http @@ -0,0 +1,246 @@ +@url = http://localhost:5097 + +### 文档管理 API 测试 + +#### 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-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/8375959069814838810 +Accept: application/json + +### + +#### 7. 预览文档 +GET {{url}}/api/document/8375959069814838810/preview +Accept: application/json + +### + +#### 8. 更新文档(包含权限) +PUT {{url}}/api/document/8375959069814838810 +Content-Type: application/json + +{ + "Title": "更新后的文档标题", + "AccessLevel": "public" +} + +### + +#### 9. 删除单个文档 +DELETE {{url}}/api/document/4631834103230408532 + +### + +#### 10. 批量更新文档权限 +PUT {{url}}/api/document/update-access-level +Content-Type: application/json + +{ + "Ids": [4631834103230408532, 5761862722737776785], + "AccessLevel": "public" +} + +### + +#### 11. 批量删除文档 +DELETE {{url}}/api/document/batch +Content-Type: application/json + +[ +3040509232552355159, 1876963397423496466 +] + +### + +#### 知识库管理 API 测试 + +#### 1. 获取知识库统计信息 +GET {{url}}/api/knowledgebase/stats +Accept: application/json + +### + +#### 2. 获取文档状态 +GET {{url}}/api/knowledgebase/documents/701403795935186172/vectorization-status +Accept: application/json + +### + +#### 3. 向量化单个文档 +POST {{url}}/api/knowledgebase/documents/701403795935186172/vectorize +Content-Type: application/json + +{ + "ModelName": "default", + "ChunkSize": 500, + "ChunkOverlap": 50, + "Separator": "\n\n" +} + +### + +#### 4. 批量向量化文档 +POST {{url}}/api/knowledgebase/documents/batch-vectorize +Content-Type: application/json + +{ + "DocumentIds": [701403795935186172, 172579765607330619, 7483280351647518150], + "Parameters": { + "ModelName": "default", + "ChunkSize": 500, + "ChunkOverlap": 50, + "Separator": "\n\n" + } +} + +### + +#### 5. 获取向量化任务列表 +GET {{url}}/api/knowledgebase/vectorization-jobs?page=1&pageSize=10&status=Processing +Accept: application/json + +### + +#### 6. 获取向量化任务详情 +GET {{url}}/api/knowledgebase/vectorization-jobs/3196309357888426600 +Accept: application/json +### + +#### 原始天气 API 测试 (保留) +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": "" +}; + + +###多模态向量化 +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":"", + "id":"76", + "content":"我是中国人" + + +} + +###查询所有用户的输入 +GET {{url}}/api/UserSay +Accept: application/json + + + +###查询一个用户的输入情况 +GET {{url}}/api/UserSay/74 + diff --git a/backend/RAG.Api/appsettings.json b/backend/RAG.Api/appsettings.json new file mode 100644 index 0000000000000000000000000000000000000000..069a50c5661e0b1ebedd6ce17ae63ff37bf61d15 --- /dev/null +++ b/backend/RAG.Api/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection":"Host=182.92.86.71;Port=5432;Database=postgres;Username=schoolproject01;Password=YourSecurePassword123" + }, + "VectorStore": { + "Type": "Postgres", +"DefaultConnection": "Host=182.92.86.71;Port=5432;Database=postgres;Username=schoolproject01;Password=YourSecurePassword123" + }, + "jwt":{ + "Secret":"YourSuperSecretKeyForAdminToken2025!", + "ExpireMintes":120 + } +} diff --git a/backend/RAG.Application/Class1.cs b/backend/RAG.Application/Class1.cs new file mode 100644 index 0000000000000000000000000000000000000000..973f93a0d9d730ccce0d14eda2d072906938888f --- /dev/null +++ b/backend/RAG.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace RAG.Application; + +public class Class1 +{ + +} diff --git a/frontend/txt.txt b/backend/RAG.Application/Commands/UploadDocumentCommand.cs similarity index 100% rename from frontend/txt.txt rename to backend/RAG.Application/Commands/UploadDocumentCommand.cs diff --git a/backend/RAG.Application/Common/ApiResult.cs b/backend/RAG.Application/Common/ApiResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..0af6ec3a57465684631fc9656da685dcea1ca488 --- /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/BatchUpdateDocumentsAccessLevelDto.cs b/backend/RAG.Application/DTOs/BatchUpdateDocumentsAccessLevelDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..5e225985732f45ce35f2d40c5cb223773e040069 --- /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/DTOs/BatchVectorizationRequestDto.cs b/backend/RAG.Application/DTOs/BatchVectorizationRequestDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..8eaf388bee944f357499939984a939b074c4a382 --- /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/DocumentContentDto.cs b/backend/RAG.Application/DTOs/DocumentContentDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..a3ef1d813d93b9f6ad763c6331808f4641c55d5b --- /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/DTOs/DocumentDto.cs b/backend/RAG.Application/DTOs/DocumentDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..03d2d97ed316c5cb3fafe1721e4bcaf360e01fa2 --- /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 0000000000000000000000000000000000000000..225f3e3a9a4d5ba7816c0c90b3a3e04d9ae5f59c --- /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 0000000000000000000000000000000000000000..6f9a2568b7343ad03a5824b971592c491e6f53c0 --- /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 0000000000000000000000000000000000000000..09daf79438e958d4c425e3bdcce7481a5a427176 --- /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/DTOs/DocumentVectorizationStatusDto.cs b/backend/RAG.Application/DTOs/DocumentVectorizationStatusDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..c7eabcdee3e1a967dd872e4ddfca55de3402c9e3 --- /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 0000000000000000000000000000000000000000..9fdb6305920fcb387ad2ee1e2292692d4b071fc4 --- /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 0000000000000000000000000000000000000000..66ad8aa6b3cf86c3fa46bb0d6c12cfd7718bb83f --- /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 0000000000000000000000000000000000000000..474a9842f8180f1245b5a62cd3782b3895f6ad6b --- /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 0000000000000000000000000000000000000000..cbce8e57765116ed92711e901ece640f0141b5b5 --- /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 0000000000000000000000000000000000000000..0e671e0c83dac1d662201f5ee4c7ddc1d54e00a6 --- /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/UserDto.cs b/backend/RAG.Application/DTOs/UserDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..8de4d6e8edbceead2c6df99d814ae7170b0d7d7d --- /dev/null +++ b/backend/RAG.Application/DTOs/UserDto.cs @@ -0,0 +1,3 @@ +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 0000000000000000000000000000000000000000..36c5683dede93bb16863471affed8abb017e7374 --- /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/DTOs/VectorizationJobDto.cs b/backend/RAG.Application/DTOs/VectorizationJobDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..a656c9aa7e06dcac18c2101fc0a35b9b0e46a2aa --- /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 0000000000000000000000000000000000000000..8e00e7475f949ec308d485cd182d519daff760ef --- /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/Handlers/UploadDocumentHandler.cs b/backend/RAG.Application/Handlers/UploadDocumentHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/RAG.Application/Interfaces/EmbeddingResponse.cs b/backend/RAG.Application/Interfaces/EmbeddingResponse.cs new file mode 100644 index 0000000000000000000000000000000000000000..70a272fc537669c5a4ae626cc24e4ec4c01fa766 --- /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/Interfaces/IDocumentProcessor.cs b/backend/RAG.Application/Interfaces/IDocumentProcessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad4d7ca7e729a3191c65456401cc8ee5aaaa128f --- /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/Interfaces/IDocumentService.cs b/backend/RAG.Application/Interfaces/IDocumentService.cs new file mode 100644 index 0000000000000000000000000000000000000000..ef0c9118fe748e1fda14871e8bb43f8256022852 --- /dev/null +++ b/backend/RAG.Application/Interfaces/IDocumentService.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using RAG.Application.DTOs; + +namespace RAG.Application.Interfaces +{ + public interface IDocumentService + { + // 上传文档 + 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); + + // 获取单个文档 + Task GetDocumentByIdAsync(long id); + + // 预览文档 + Task PreviewDocumentAsync(long id); + + // 编辑文档 + Task UpdateDocumentAsync(long id, DocumentUpdateDto updateDto); + + // 删除文档 + Task DeleteDocumentAsync(long id); + Task DeleteDocumentsAsync(IEnumerable ids); + + // 批量更新文档权限 + 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 + { + 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; } = Array.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/Interfaces/IKnowledgeBaseService.cs b/backend/RAG.Application/Interfaces/IKnowledgeBaseService.cs new file mode 100644 index 0000000000000000000000000000000000000000..b2adc50a66f15142e67a68c684288c62c588f159 --- /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 0000000000000000000000000000000000000000..467372a67a7afad73d57b9bbdea247e0637ee7ef --- /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/RAG.Application.csproj b/backend/RAG.Application/RAG.Application.csproj new file mode 100644 index 0000000000000000000000000000000000000000..780f723c8714fe289976509b13b8d90f2c11a9ad --- /dev/null +++ b/backend/RAG.Application/RAG.Application.csproj @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/backend/RAG.Application/ServiceCollectionExtensions.cs b/backend/RAG.Application/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..701e72394ae0e3f34687b9692c037d513764bc72 --- /dev/null +++ b/backend/RAG.Application/ServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ + + +using Microsoft.Extensions.DependencyInjection; +using RAG.Application.Services; + +namespace RAG.Application; + + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAppAddication(this IServiceCollection services) + { + + services.AddScoped(); + 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 0000000000000000000000000000000000000000..0cb24318935815eaf40f649600f1bd32c30044e6 --- /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.HashPassword(password, user.Salt) == user.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.Application/Services/DocumentProcessors/BaseDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/BaseDocumentProcessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..f8895be6c0da08ae7376a9d874e2dbc553638b78 --- /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 0000000000000000000000000000000000000000..da1989dee6e6b0d36cce91952eba8a32f308b961 --- /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 0000000000000000000000000000000000000000..e74ded3202f2b0d7995b414209681cdeee7366fb --- /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/MarkdownDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/MarkdownDocumentProcessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..d87496fbe85fff883f086559bb716ba6f45134bf --- /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/OfficeDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/OfficeDocumentProcessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..48bbb1eb6c2cf253aea37ac2c01cffae04177b73 --- /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 0000000000000000000000000000000000000000..763246f837316a2c78b1125bec7066253577c316 --- /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/DocumentProcessors/TextDocumentProcessor.cs b/backend/RAG.Application/Services/DocumentProcessors/TextDocumentProcessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..7ea7645b1956ced353b3d1fe2d419bcb0129868b --- /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 diff --git a/backend/RAG.Application/Services/DocumentService.cs b/backend/RAG.Application/Services/DocumentService.cs new file mode 100644 index 0000000000000000000000000000000000000000..c5b8ab49a6dac0e9c7ba4a16f9d09366ff45420f --- /dev/null +++ b/backend/RAG.Application/Services/DocumentService.cs @@ -0,0 +1,840 @@ +using System; +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; +using RAG.Application.Services.DocumentProcessors; +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 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( + 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; + } + + public async Task> UploadDocumentsAsync(IEnumerable uploadDtos, CancellationToken cancellationToken = default) + { + var results = new List(); + + foreach (var uploadDto in uploadDtos) + { + using (var fileStream = uploadDto.File.OpenReadStream()) + { + var fileName = uploadDto.File.FileName; + // 获取文件扩展名作为文件类型 + var fileType = Path.GetExtension(fileName).ToLower(); + var fileSize = uploadDto.File.Length; + + var result = await UploadDocumentAsync(uploadDto, fileStream, fileName, fileType, fileSize, cancellationToken); + 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(new object[] { 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(new object[] { id }); + + if (document == null) + { + throw new KeyNotFoundException($"Document with id {id} not found"); + } + + // 从数据库读取文件内容 + string content = string.Empty; + byte[]? fileData = document.FileContent; + + if (document.FileType.ToLower() == "txt" || document.FileType.ToLower() == "md") + { + using (var stream = new MemoryStream(document.FileContent)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + } + + 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(new object[] { id }); + + if (document == null) + { + throw new KeyNotFoundException($"Document with id {id} not found"); + } + + // 更新文档属性 + // 由于 Document.Title 的 set 访问器不可访问,推测应该调用文档实体的更新方法 + // 使用实体的业务方法更新标题 + document.UpdateTitle(updateDto.Title); + + // 更新访问权限(如果提供) + if (!string.IsNullOrEmpty(updateDto.AccessLevel)) + { + document.UpdateAccessLevel(updateDto.AccessLevel); + } + else + { + // 如果未提供权限,更新修改时间 + 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(new object[] { id }); + + if (document == null) + { + return false; + } + + // 删除数据库记录 + _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; + } + + // 删除数据库记录 + _context.Documents.RemoveRange(documents); + await _context.SaveChangesAsync(); + + 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; + } + 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(new object[] { 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 = new MemoryStream(document.FileContent)) + 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 + { + // 获取文档(避免闭包问题) + 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 = new MemoryStream(doc.FileContent)) + using (var reader = new StreamReader(stream)) + { + content = await reader.ReadToEndAsync(); + } + + // 文本分块 + var chunks = SplitTextIntoChunks( + content, + parameters.ChunkSize, + parameters.ChunkOverlap, + parameters.Separator ?? string.Empty); + + doc.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 = doc.Id, + chunkId = i + 1, + position = i + 1 + }); + + // 创建文档块 + var chunk = DocumentChunk.Create( + doc.Id, + chunkContent, + i + 1, + chunkContent.Length); + + _context.DocumentChunks.Add(chunk); + doc.AddChunk(chunk); + + // 更新进度 + doc.UpdateVectorizationProgress(i + 1); + await _unitOfWork.SaveChangesAsync(); + } + + // 完成文档向量化 + doc.CompleteVectorization(); + job.Complete(); + await _unitOfWork.SaveChangesAsync(); + } + catch (Exception ex) + { + // 记录文档向量化失败 + var doc = await _context.Documents.FindAsync(new object[] { documentId }); + if (doc != null) + { + doc.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)}"); + } + + // 修改:允许所有状态的文档进行向量化 + // 仅记录非Completed状态的文档但不阻止向量化 + var nonCompletedDocuments = documents + .Where(d => d.Status != DocumentStatus.Completed) + .ToList(); + + if (nonCompletedDocuments.Any()) + { + // 记录警告但不抛出异常 + Console.WriteLine($"Warning: Documents not in Completed status but will be vectorized: {string.Join(", ", nonCompletedDocuments.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 + { + // 获取文档列表(避免闭包问题) + var docs = await _context.Documents + .Where(d => request.DocumentIds.Contains(d.Id)) + .ToListAsync(); + + foreach (var doc in docs) + { + try + { + // 读取文档内容 + string content; + // 使用数据库中的文件内容,而不是从文件系统读取 + using (var memoryStream = new MemoryStream(doc.FileContent)) + using (var reader = new StreamReader(memoryStream)) + { + content = await reader.ReadToEndAsync(); + } + // 文本分块 + var chunks = SplitTextIntoChunks( + content, + request.Parameters.ChunkSize, + request.Parameters.ChunkOverlap, + request.Parameters.Separator ?? string.Empty); + // 开始文档向量化 + doc.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 = doc.Id, + chunkId = i + 1, + position = i + 1 + }); + + // 创建文档块 + var chunk = DocumentChunk.Create( + doc.Id, + chunkContent, + i + 1, + chunkContent.Length); + + _context.DocumentChunks.Add(chunk); + doc.AddChunk(chunk); + + // 更新进度 + doc.UpdateVectorizationProgress(i + 1); + await _unitOfWork.SaveChangesAsync(); + } + + // 完成文档向量化 + doc.CompleteVectorization(); + await _unitOfWork.SaveChangesAsync(); + } + catch (Exception ex) + { + // 记录文档向量化失败 + var docToUpdate = await _context.Documents.FindAsync(new object[] { doc.Id }); + if (docToUpdate != null) + { + docToUpdate.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(new object[] { 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("Page size must be greater than 0"); + + var totalCount = await _vectorizationJobRepository.CountAsync(); + var jobs = await _vectorizationJobRepository.GetAllAsync(); + var items = jobs + .OrderByDescending(job => job.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(job => new VectorizationJobDto + { + 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(); + + return new PagedResult + { + Items = items, + TotalCount = totalCount, + Page = page, + PageSize = pageSize + }; + } + + public async Task GetVectorizationJobAsync(long jobId) + { + var job = await _vectorizationJobRepository.GetByIdAsync(jobId); + if (job == null) + throw new KeyNotFoundException("Vectorization job 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, + ErrorMessage = job.ErrorMessage, + DocumentIds = job.DocumentIds + }; + } + + // 文本分块辅助方法 + private static List SplitTextIntoChunks(string text, int chunkSize, int chunkOverlap, string separator) + { + var chunks = new List(); + if (string.IsNullOrEmpty(text)) + return chunks; + + // 使用分隔符分割文本 + var splitOptions = string.IsNullOrEmpty(separator) ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries; + var paragraphs = text.Split(new[] { separator }, splitOptions); + + 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; + } + // 支持的文件类型列表 + private readonly List _supportedFileTypes = new List { ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".txt", ".md" }; + // 验证文件类型是否有效 + private bool IsValidFileType(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + return false; + 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(fileName)) + { + throw new InvalidOperationException($"Unsupported file type: {Path.GetExtension(fileName)}"); + } + // 2. 查找合适的文档处理器 + var processor = _documentProcessorFactory.GetProcessor(fileType); + if (processor == null) + { + throw new InvalidOperationException($"No processor found for file type: {fileType}"); + } + + // 3. 读取文件内容到byte[] + byte[] fileContent; + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream, cancellationToken); + fileContent = memoryStream.ToArray(); + } + + // 4. 提取文档内容 + var progressKey = Guid.NewGuid().ToString(); + using (var memoryStream = new MemoryStream(fileContent)) + { + var documentContent = await processor.ExtractContentAsync(memoryStream, fileName, progressKey); + } + + // 5. 创建文档实体 + var document = Document.Create( + title: request.Title, + fileName: fileName, + filePath: string.Empty, + fileType: fileType, + fileSize: fileSize, + accessLevel: request.AccessLevel, + fileContent: fileContent + ); + + // 6. 保存文档到数据库 + _context.Documents.Add(document); + await _unitOfWork.SaveChangesAsync(); + + // 7. 返回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 + }; + } + } +} diff --git a/backend/RAG.Application/Services/KnowledgeBaseService.cs b/backend/RAG.Application/Services/KnowledgeBaseService.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf82892b18f2d31efc13f87ccf46c7ef913ac0ee --- /dev/null +++ b/backend/RAG.Application/Services/KnowledgeBaseService.cs @@ -0,0 +1,210 @@ +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); + + // 保存到数据库 + System.Console.WriteLine(1111111); + 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/ProgressService.cs b/backend/RAG.Application/Services/ProgressService.cs new file mode 100644 index 0000000000000000000000000000000000000000..85b9c5e21ce1644f6c1b04da191b670ac292ce11 --- /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.Application/Services/UserSayService.cs b/backend/RAG.Application/Services/UserSayService.cs new file mode 100644 index 0000000000000000000000000000000000000000..32aa2345a3d4a9c6bafe1ed930069258571f417a --- /dev/null +++ b/backend/RAG.Application/Services/UserSayService.cs @@ -0,0 +1,224 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using RaG.Application.Common; +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, long id,string Content) + { + + + + 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(id); +System.Console.WriteLine(Usersay); + +// 定义请求模型 +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 + { + using JsonDocument doc = JsonDocument.Parse(result); + var content1 = doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString(); + Content = content1; + + System.Console.WriteLine(Content); + + + System.Console.WriteLine(content1); + //构建请求体 + 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"); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", apiKey); + + // 发送POST请求 + var response = await httpClient.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, Content)); + + + // 完整向量可以用于后续处理 + // 例如: 存储到数据库或进行向量计算 + } + 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}"); + } + } + catch (Exception ex) + { + System.Console.WriteLine(ex); + } + } + //新增用户输入文本,转化为向量 + + + + + + + + + //查询用户输入文本 + public async Task SelAllUserAsync() + { + + var res = await _userSay.SelAllAsync(); + if (res == null) + { + return null; + } + return ApiResult.Success(res); + } + + public async Task GetByIdAsync(long vid) + + { + + var res = await _userSay.GetSayUserAsync(vid); + if (res == null) + { + return ApiResult.Fail(0); + } + + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..4a3f454bcd5d1711805000460857d84f51c2163a --- /dev/null +++ b/backend/RAG.Application/Services/VectorizationService.cs @@ -0,0 +1,284 @@ +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); + System.Console.WriteLine("获取文档"); + System.Console.WriteLine(document); + 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(); + } + + System.Console.WriteLine("读取文档内容"); + System.Console.WriteLine(content); + + // 文本分块 + 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/Class1.cs b/backend/RAG.Domain/Class1.cs new file mode 100644 index 0000000000000000000000000000000000000000..43b53cf6fcd894db873a562625d338d75574d28f --- /dev/null +++ b/backend/RAG.Domain/Class1.cs @@ -0,0 +1,6 @@ +namespace RAG.Domain; + +public class Class1 +{ + +} diff --git a/backend/RAG.Domain/Entities/ChatHistory.cs b/backend/RAG.Domain/Entities/ChatHistory.cs new file mode 100644 index 0000000000000000000000000000000000000000..e29119982f4dc4962ba40b9451a5b556e5cd24bb --- /dev/null +++ b/backend/RAG.Domain/Entities/ChatHistory.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System; + +namespace RAG.Domain.Entities +{ + public class ChatHistory : EntityBase + { + private ChatHistory() + { + SessionId = string.Empty; // 初始化 SessionId 以避免 null 值 + } + + 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 : EntityBase + { + 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 : EntityBase + { + 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 new file mode 100644 index 0000000000000000000000000000000000000000..427d4186731e36dec306ff6e2bcf4eb857039ce6 --- /dev/null +++ b/backend/RAG.Domain/Entities/Document.cs @@ -0,0 +1,182 @@ +// RAG.Domain/Entities/Document.cs +using System; +using RAG.Domain.Events; + +namespace RAG.Domain.Entities +{ + public class Document : EntityBase + { + private readonly List _chunks = new(); + + // 私有构造函数,确保通过工厂方法创建 + 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, + string fileName, + string filePath, + string fileType, + long fileSize, + 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"); + // 允许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) + 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, + FileContent = fileContent ?? Array.Empty(), // 设置文件内容 + AccessLevel = accessLevel.ToLower(), + Status = DocumentStatus.Pending + }; + + // 发布领域事件 + document.AddDomainEvent(new DocumentCreatedEvent(document.Id)); + + return document; + } + + 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 byte[] FileContent { get; private set; } // 新增属性用于存储文件内容 + public string AccessLevel { get; private set; } + public DocumentStatus Status { get; private set; } + + public IReadOnlyList Chunks => _chunks.AsReadOnly(); + + // 业务方法 + public void UpdateTitle(string newTitle) + { + if (string.IsNullOrWhiteSpace(newTitle)) + throw new ArgumentException("Title cannot be empty"); + + Title = newTitle; + UpdatedAt = DateTime.UtcNow; + } + + // 更新修改时间 + public void UpdateUpdatedAt(DateTime updatedTime) + { + 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) + throw new InvalidOperationException("Document must be in pending status"); + + Status = DocumentStatus.Processing; + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new DocumentProcessingStartedEvent(Id)); + } + + public void CompleteProcessing() + { + if (Status != DocumentStatus.Processing) + throw new InvalidOperationException("Document must be in processing status"); + + Status = DocumentStatus.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 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/DocumentChunk.cs b/backend/RAG.Domain/Entities/DocumentChunk.cs new file mode 100644 index 0000000000000000000000000000000000000000..c69c10e1b58da75fa2a683c332c4c4b53d2ceae8 --- /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 0000000000000000000000000000000000000000..5aec26f523f2703a1571768e0c962b72cf089ec5 --- /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/EntityBase.cs b/backend/RAG.Domain/Entities/EntityBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a2ab07c0593fd626fa62f66ad3658d900aa0ed8 --- /dev/null +++ b/backend/RAG.Domain/Entities/EntityBase.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace RAG.Domain.Entities +{ + 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() + { + _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/KnowledgeBase.cs b/backend/RAG.Domain/Entities/KnowledgeBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..f30bca24f14f21c7b08d7e029784f369cd4bff0f --- /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/User.cs b/backend/RAG.Domain/Entities/User.cs new file mode 100644 index 0000000000000000000000000000000000000000..8c5baa5e92cc12d9407370c1860d9021d9d6612b --- /dev/null +++ b/backend/RAG.Domain/Entities/User.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System; + +namespace RAG.Domain.Entities +{ + public class User : EntityBase + { + 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(); + } +} \ No newline at end of file diff --git a/backend/RAG.Domain/Entities/UserSay.cs b/backend/RAG.Domain/Entities/UserSay.cs new file mode 100644 index 0000000000000000000000000000000000000000..35d0bce412f33b9afa2eb41d57e6d9a4a00ba97f --- /dev/null +++ b/backend/RAG.Domain/Entities/UserSay.cs @@ -0,0 +1,39 @@ +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 string Content { get; private set; } + + + public UserSay(string usersay, string message, float[] uersayVector, string content) + { + this.Usersay = usersay; + this.Message = message; + this.UersayVector = uersayVector; + Content = content; + + + + + //this.Content = content; + //this. = title; + + + } + + + + + } +} \ 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 0000000000000000000000000000000000000000..c0dc1f27d851e8f9fdf78e6acd0fab58ce6a78b2 --- /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 0000000000000000000000000000000000000000..51adafe6e0073a3098dda69f941f4fd690ffb6fe --- /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 0000000000000000000000000000000000000000..6867cef7d4b53c4b13f3f63c9fabf4a820d335fe --- /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 0000000000000000000000000000000000000000..bd876862a5db0379d7f63db92814141e761c1f1f --- /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/Events/DocumentCreatedEvent.cs b/backend/RAG.Domain/Events/DocumentCreatedEvent.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ef12d0bf57606e7cbe769b96475171504342d89 --- /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 0000000000000000000000000000000000000000..cd7258a91fabe4e75d9764e462acd77f283df180 --- /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 0000000000000000000000000000000000000000..8fa972c3255b4bba9a812bd20448b764519e1cd3 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..30402ac0e7ae3a405870959f590998658261852c --- /dev/null +++ b/backend/RAG.Domain/RAG.Domain.csproj @@ -0,0 +1,9 @@ + + + + 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 0000000000000000000000000000000000000000..ef230eb6b3eb508edc6354acc15d224262e2d7ef --- /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 0000000000000000000000000000000000000000..d74f7a7b214b985bfa6a13974b6cc07ff42da077 --- /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 0000000000000000000000000000000000000000..86f8ebaec3b045d538bc9e952551ed4ffe36641a --- /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/IERepository.cs b/backend/RAG.Domain/Repositories/IERepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..ccc3e05c0c5080971861a37b6ed31505393aa534 --- /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.Domain/Repositories/IKnowledgeBaseRepository.cs b/backend/RAG.Domain/Repositories/IKnowledgeBaseRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..a0a645d6f75f973b5b5934adb4890ab4be70ddcf --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..58d4acf9c040374cbaa45a04dc38cf72fcdc0eb7 --- /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(long 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 0000000000000000000000000000000000000000..a5e77395b553a69312edea9d2a724e9947ef7146 --- /dev/null +++ b/backend/RAG.Domain/Repositories/IUnitOfWork.cs @@ -0,0 +1,18 @@ +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; } + IKnowledgeBaseRepository KnowledgeBases { 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 0000000000000000000000000000000000000000..add4a5246a929e7f23a64698bdbe2ef8f814903c --- /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/Repositories/IUserSayRepository.cs b/backend/RAG.Domain/Repositories/IUserSayRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..95b8faec1a51a7a6d158bac508d5f176819aaa42 --- /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.Domain/Repositories/IVectorStore.cs b/backend/RAG.Domain/Repositories/IVectorStore.cs new file mode 100644 index 0000000000000000000000000000000000000000..031c686de1fa423085c4c201b26617732c617414 --- /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, double threshold = 0.7); + } + + public class VectorResult + { + public int Id { 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.Domain/Services/IDocumentProcessingService.cs b/backend/RAG.Domain/Services/IDocumentProcessingService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5ffcc6d7ec2cda7f0b9f9dfddcb98da869510dce --- /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/Class1.cs b/backend/RAG.Infrastructure/Class1.cs new file mode 100644 index 0000000000000000000000000000000000000000..cfa7d06618759119ee2b9abb79453f2c03248a42 --- /dev/null +++ b/backend/RAG.Infrastructure/Class1.cs @@ -0,0 +1,6 @@ +namespace RAG.Infrastructure; + +public class Class1 +{ + +} diff --git a/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..8b9e757ee05219769ce9f5a3ef5bb7962652ba26 --- /dev/null +++ b/backend/RAG.Infrastructure/Data/ApplicationDbContext.cs @@ -0,0 +1,258 @@ + + +using System.Text.Json; +using Microsoft.EntityFrameworkCore; + +using RAG.Domain.Entities; + +namespace RAG.Infrastructure.Data +{ + public class ApplicationDbContext : DbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) + {} + + // 核心数据表 + 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; } + public DbSet ChatMessages { get; set; } + public DbSet MessageReferences { get; set; } + + // 向量存储表 + public DbSet VectorStores { get; set; } + // 向量化任务表 + public DbSet VectorizationJobs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // 忽略所有实体的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); + 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) + .HasColumnType("text"); + entity.Property(u => u.Content) + .HasColumnType("text"); + + entity.Property(x => x.UersayVector) + .HasColumnType("vector(1024)") + .HasConversion( + 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(); + }); + + + + + // 配置User实体 + modelBuilder.Entity(entity => + { + entity.HasKey(u => u.Id); + entity.Property(u => u.UserName).IsRequired().HasMaxLength(50).HasColumnType("text"); + entity.Property(u => u.Email).IsRequired().HasMaxLength(100).HasColumnType("text"); + 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.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.FileContent).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实体 + 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); + + // 索引 + + // 配置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(); + }); + + // 配置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/FileStorage/DatabaseFileStorageService.cs b/backend/RAG.Infrastructure/FileStorage/DatabaseFileStorageService.cs new file mode 100644 index 0000000000000000000000000000000000000000..45ddaf0a5d4f26adfc04aa18d878cbe1c22d8356 --- /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/FileStorage/IFileStorageService.cs b/backend/RAG.Infrastructure/FileStorage/IFileStorageService.cs new file mode 100644 index 0000000000000000000000000000000000000000..0f81ed454d223a199a2774a14ff1920b3ad9ac1f --- /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 0000000000000000000000000000000000000000..c945f5f4ebe79421ad6890d6a3bc14b9eae1bb35 --- /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 Task FileExistsAsync(string filePath) + { + return Task.FromResult(File.Exists(filePath)); + } + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..d08e24b25998d660d143c84c7d65df893f6fff17 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.Designer.cs @@ -0,0 +1,569 @@ +// +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("20250813075416_RemoveUsersayUniqueConstraintV2")] + partial class RemoveUsersayUniqueConstraintV2 + { + /// + 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.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/20250813075416_RemoveUsersayUniqueConstraintV2.cs b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.cs new file mode 100644 index 0000000000000000000000000000000000000000..8d33e685d34c64d4c8a8112962cacccecb73d8a5 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/20250813075416_RemoveUsersayUniqueConstraintV2.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RAG.Infrastructure.Migrations +{ + /// + public partial class RemoveUsersayUniqueConstraintV2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // 删除Usersay字段的唯一索引 + migrationBuilder.DropIndex( + name: "IX_UserSays_Usersay", + table: "UserSays"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // 恢复Usersay字段的唯一索引 + migrationBuilder.CreateIndex( + name: "IX_UserSays_Usersay", + table: "UserSays", + column: "Usersay", + unique: true); + } + } +} 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 0000000000000000000000000000000000000000..de0cf597f22032cd8d28e40fccf6f681feb86f53 --- /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 0000000000000000000000000000000000000000..7f705e208c5b4cef756d04261f6b2cdb5104d7f0 --- /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 0000000000000000000000000000000000000000..89eaaf28be31713f3e942ac03ec158e3cdcbd7df --- /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 0000000000000000000000000000000000000000..8fbef7c06d1425b26544308132f0af5c1a0404a1 --- /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/20250814023800_UserSay22.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814023800_UserSay22.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..95e6b00c591ad9171c0ae04ae4ddd9dce6b34eca --- /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 0000000000000000000000000000000000000000..3e14d34b1ae7287f5dbe3e6ce5a030b051fee974 --- /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 0000000000000000000000000000000000000000..4debc1d9551b24ac0c3302a1d5721ba27402c21c --- /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 0000000000000000000000000000000000000000..92fdb85f12420a805abfd9cbd7661debb4152059 --- /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/20250814050935_UserSay25.Designer.cs b/backend/RAG.Infrastructure/Migrations/20250814050935_UserSay25.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..79e49132e49788317dd7fc3dd7903532ccd3856b --- /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 0000000000000000000000000000000000000000..23a7cbe51fcc5e053607fbbd04bd90ca9b09d3b6 --- /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 0000000000000000000000000000000000000000..ea09524aeb2e12df78367e4bfbf8a88671eb2707 --- /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 0000000000000000000000000000000000000000..b3b366b6d20aefe44b3a032c7b3daab9bd4b6c6a --- /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 0000000000000000000000000000000000000000..80252ccff0d0f4c4657154156daa579328db6826 --- /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 0000000000000000000000000000000000000000..4b2cd2ae07de488dae9b2879e9ad997c0a7ee226 --- /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/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000000000000000000000000000000000000..411ea6575b75a7ed0010bf89bbbd3e0062d97cf0 --- /dev/null +++ b/backend/RAG.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,572 @@ +// +using System; +using System.Collections.Generic; +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.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/RAG.Infrastructure.csproj b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj new file mode 100644 index 0000000000000000000000000000000000000000..ee16d56e6f5a7b04c5336acffc72908a42f9d3a7 --- /dev/null +++ b/backend/RAG.Infrastructure/RAG.Infrastructure.csproj @@ -0,0 +1,35 @@ + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + net8.0 + enable + enable + + + + + + + diff --git a/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs b/backend/RAG.Infrastructure/Repositories/ApplicationUnitOfWork.cs new file mode 100644 index 0000000000000000000000000000000000000000..5249030cad239710d88fe8ddd8bf953113405565 --- /dev/null +++ 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 0000000000000000000000000000000000000000..a5dde37e46e2b9b6d3624c5168145d5678c9cb0c --- /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 0000000000000000000000000000000000000000..1f719f3d0daaa95ae17c818fdda682e74b397497 --- /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/EReposiotory.cs b/backend/RAG.Infrastructure/Repositories/EReposiotory.cs new file mode 100644 index 0000000000000000000000000000000000000000..4f7acd5fa3faefc9bbd807b9f3e9be1d2ad26bcc --- /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/Repositories/KnowledgeBaseRepository.cs b/backend/RAG.Infrastructure/Repositories/KnowledgeBaseRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..7f392cfc3f62be24d6a1eeacce4814bb0ba427ae --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..e96dede1ca50525ba8824d7e2aa036573b6004a4 --- /dev/null +++ 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(long 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/Repositories/UserSayREpository.cs b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs new file mode 100644 index 0000000000000000000000000000000000000000..f95368526c7c888c32234fb6d6e6dc1cdd5d1723 --- /dev/null +++ b/backend/RAG.Infrastructure/Repositories/UserSayREpository.cs @@ -0,0 +1,68 @@ +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); + await _context.SaveChangesAsync(); + + } + + 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.Where(c => c.Id == id).ToListAsync(); + + + } + + 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/RAG.Infrastructure/Security/PasswordHasher.cs b/backend/RAG.Infrastructure/Security/PasswordHasher.cs new file mode 100644 index 0000000000000000000000000000000000000000..db0cfb104308b9d82db0b4a9aa2e919a7cfc7149 --- /dev/null +++ b/backend/RAG.Infrastructure/Security/PasswordHasher.cs @@ -0,0 +1,28 @@ +using BCrypt.Net; + + +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 VerifyHashedPassword(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 VerifyHashedPassword(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 0000000000000000000000000000000000000000..147a842b6eb92bbdd2d6e01b330340871f8d2be7 --- /dev/null +++ b/backend/RAG.Infrastructure/ServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ + + + +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(IUserSayRepository<>), typeof(UserSayRepository<>)); + + services.AddScoped(typeof(IPasswordHasher), typeof(PasswordHasher)); + services.AddSingleton(); + + + + + // services.AddScoped (); + + return services; + } +} \ No newline at end of file diff --git a/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs b/backend/RAG.Infrastructure/Services/DocumentProcessingService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/RAG.Infrastructure/Token/JwtTokenGererator.cs b/backend/RAG.Infrastructure/Token/JwtTokenGererator.cs new file mode 100644 index 0000000000000000000000000000000000000000..d61810afa1a01e231101421a69bba078ab92b66a --- /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 diff --git a/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs new file mode 100644 index 0000000000000000000000000000000000000000..47a7afb973f313a98c5f7e23d737310154053c61 --- /dev/null +++ b/backend/RAG.Infrastructure/VectorStore/PgVectorStore.cs @@ -0,0 +1,164 @@ +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, double threshold = 0.7) + { + if (!_initialized) + { + throw new InvalidOperationException("Vector store not initialized"); + } + + // 模拟向量搜索 + // 实际应用中,这里应该使用PostgreSQL的向量扩展进行相似度搜索 + return await Task.Run(() => + { + // 初始搜索,应用阈值 + 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 + { + 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 with threshold {threshold}"); + 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/backend/RAG.Tests/DomainTests/DocumentTests.cs b/backend/RAG.Tests/DomainTests/DocumentTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/RAG.Tests/RAG.Tests.csproj b/backend/RAG.Tests/RAG.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..1d8592fe6c547f2d87dffe1399d811cd3f4d7675 --- /dev/null +++ b/backend/RAG.Tests/RAG.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/RAG.Tests/UnitTest1.cs b/backend/RAG.Tests/UnitTest1.cs new file mode 100644 index 0000000000000000000000000000000000000000..3326ee98b27e2d675adcb3e521b20005801d619b --- /dev/null +++ b/backend/RAG.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace RAG.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/backend/RAG.sln b/backend/RAG.sln new file mode 100644 index 0000000000000000000000000000000000000000..70304a8c56b514bb456c89d6dbba72587dd1c5f7 --- /dev/null +++ b/backend/RAG.sln @@ -0,0 +1,90 @@ + +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", "{7AA1BD7D-89C9-4DD3-902D-5036A29DD33D}" +EndProject +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", "{09BBBB92-0E93-4195-8830-ED2546A17771}" +EndProject +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", "{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 +EndGlobal diff --git a/query b/query new file mode 100644 index 0000000000000000000000000000000000000000..a02a1cc751d91766a989d3858a43be6530ee7410 --- /dev/null +++ b/query @@ -0,0 +1 @@ +postgresql diff --git a/test-api.html b/test-api.html new file mode 100644 index 0000000000000000000000000000000000000000..73a36cd116d3609a1a40f96cd34e4cd97c8bf934 --- /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 0000000000000000000000000000000000000000..9ef0e2b8cf2d851f1395ab1112998f96c6ba9c97 --- /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 0000000000000000000000000000000000000000..5c95ebe186c07660d809d5d202e66fc6d240ea7d --- /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 diff --git a/test-upload-fix.ps1 b/test-upload-fix.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..85606858069cbc757dc6e44f2c4917f2f1094470 --- /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