diff --git a/src/main/java/kz/konturai/parser/controller/ReportController.java b/src/main/java/kz/konturai/parser/controller/ReportController.java index 8421cfa..52c4b44 100644 --- a/src/main/java/kz/konturai/parser/controller/ReportController.java +++ b/src/main/java/kz/konturai/parser/controller/ReportController.java @@ -75,7 +75,7 @@ public class ReportController { } @PostMapping - public ResponseEntity generateResearchReport( + public ResponseEntity> generateResearchReport( @Validated @RequestBody ResearchReportRequest request) { // Валидация входных данных if (request.getQuery() == null || request.getQuery().trim().isEmpty()) { @@ -90,33 +90,63 @@ public class ReportController { request.getBreadth(), request.getReportType()); - // Синхронно выполняем: deep-research -> synthesis via Ollama -> Markdown - DeepResearchResponse researchResponse = deepResearchService.generateReportAsync(deepRequest) + // Генерация уникального ID задачи + String taskId = UUID.randomUUID().toString(); + + // Предполагаемое время завершения + LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8); + + // Асинхронный процесс: deep-research -> synthesis via Ollama -> Markdown -> + // сохранение + deepResearchService.generateReportAsync(deepRequest) .publishOn(Schedulers.boundedElastic()) - .block(); - if (researchResponse == null || researchResponse.getLearnings() == null - || researchResponse.getLearnings().isEmpty()) { - return ResponseEntity.internalServerError().build(); - } + .flatMap(researchResponse -> { + if (researchResponse == null + || researchResponse.getLearnings() == null + || researchResponse.getLearnings().isEmpty()) { + return reactor.core.publisher.Mono.error( + new RuntimeException( + "Empty research response content")); + } + return reportSynthesisService + .synthesizeReport(request.getQuery(), + researchResponse.getLearnings(), + request.getLang()) + .map(synthesizedMarkdown -> new Object[] { researchResponse, + synthesizedMarkdown }); + }) + .map(tuple -> { + DeepResearchResponse researchResponse = (DeepResearchResponse) tuple[0]; + String synthesizedMarkdown = (String) tuple[1]; + String finalMarkdown = buildMarkdownWithSources(synthesizedMarkdown, + researchResponse.getVisitedUrls()); + byte[] mdBytes = finalMarkdown + .getBytes(java.nio.charset.StandardCharsets.UTF_8); + String filename = "research_report_" + System.currentTimeMillis() + ".md"; + saveResearchMarkdownToHistory(request, filename, mdBytes, researchResponse); + return true; + }) + .doOnError(e -> { + System.err.println("Error generating research report async: " + e.getMessage()); + }) + .subscribe( + ok -> { + }, + err -> { + // Avoid onErrorDropped by consuming error here + System.err.println("Async pipeline error: " + err.getMessage()); + }); - String synthesizedMarkdown = reportSynthesisService - .synthesizeReport(request.getQuery(), researchResponse.getLearnings(), - request.getLang()) - .publishOn(Schedulers.boundedElastic()) - .block(); - if (synthesizedMarkdown == null) { - return ResponseEntity.internalServerError().build(); - } + ReportGenerationResponse response = new ReportGenerationResponse( + taskId, + "Исследование запущено в фоновом режиме. Ожидаемое время завершения: " + + estimatedCompletion + .format(java.time.format.DateTimeFormatter.ofPattern( + "HH:mm")), + estimatedCompletion, + "PROCESSING"); - String finalMarkdown = buildMarkdownWithSources(synthesizedMarkdown, researchResponse.getVisitedUrls()); - byte[] bytes = finalMarkdown.getBytes(java.nio.charset.StandardCharsets.UTF_8); - - String filename = "research_report.md"; - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + filename + "\"") - .contentType(MediaType.parseMediaType("text/markdown; charset=UTF-8")) - .body(bytes); + return ResponseEntity.ok(ApiResponse.success("Исследовательский отчёт поставлен в очередь", response)); } private String buildMarkdownWithSources(String synthesizedMarkdown, java.util.List visitedUrls) { @@ -164,6 +194,32 @@ public class ReportController { } } + private void saveResearchMarkdownToHistory(ResearchReportRequest request, String filename, + byte[] mdBytes, DeepResearchResponse researchResponse) { + try { + // Загрузка в MinIO + minIOService.uploadFile(filename, mdBytes, + MediaType.parseMediaType("text/markdown; charset=UTF-8").toString()); + + // Сохранение в базу данных + ReportHistory reportHistory = new ReportHistory(); + reportHistory.setReportTitle(request.getQuery()); + reportHistory.setAuthorName("AI Research Agent"); + reportHistory.setCompanyName("Deep Research Service"); + reportHistory.setStartDate(LocalDateTime.now()); + reportHistory.setEndDate(LocalDateTime.now()); + reportHistory.setFormat("MD"); + reportHistory.setFilename(filename); + reportHistory.setFilePath(filename); + reportHistory.setFileSize(mdBytes.length); + reportHistory.setCreatedAt(LocalDateTime.now()); + + reportHistoryRepository.save(reportHistory); + } catch (Exception e) { + System.err.println("Error saving research markdown to history: " + e.getMessage()); + } + } + @GetMapping("/history") public ResponseEntity> history( @RequestParam(name = "page", defaultValue = "0") int page, @@ -198,10 +254,17 @@ public class ReportController { byte[] bytes = inputStream.readAllBytes(); inputStream.close(); - MediaType mt = rh.getFormat() != null && rh.getFormat().equalsIgnoreCase("DOCX") - ? MediaType - .parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document") - : MediaType.APPLICATION_PDF; + MediaType mt; + if (rh.getFormat() != null && rh.getFormat().equalsIgnoreCase("DOCX")) { + mt = MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + } else if (rh.getFormat() != null && (rh.getFormat().equalsIgnoreCase("MD") + || rh.getFilename() != null + && rh.getFilename().toLowerCase().endsWith(".md"))) { + mt = MediaType.parseMediaType("text/markdown; charset=UTF-8"); + } else { + mt = MediaType.APPLICATION_PDF; + } return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + rh.getFilename() + "\"")