From 51a5fc398c00dd73bb6124fc97d083414363bbd3 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 5 Oct 2025 17:23:34 +0500 Subject: [PATCH] final markdown --- .../parser/controller/ReportController.java | 101 ++++++++---------- ...ля генерации отчётов в формате Markdown.md | 85 +++++++++++++++ 2 files changed, 129 insertions(+), 57 deletions(-) create mode 100644 Техническое Задание на доработку AI-агента для генерации отчётов в формате Markdown.md diff --git a/src/main/java/kz/konturai/parser/controller/ReportController.java b/src/main/java/kz/konturai/parser/controller/ReportController.java index a00c39f..8421cfa 100644 --- a/src/main/java/kz/konturai/parser/controller/ReportController.java +++ b/src/main/java/kz/konturai/parser/controller/ReportController.java @@ -10,7 +10,6 @@ import kz.konturai.parser.model.ReportHistory; import kz.konturai.parser.repository.ReportHistoryRepository; import kz.konturai.parser.service.ReportGenerationService; import kz.konturai.parser.service.DeepResearchService; -import kz.konturai.parser.service.ResearchPdfService; import kz.konturai.parser.service.ReportSynthesisService; import kz.konturai.parser.service.MinIOService; import org.springframework.data.domain.Page; @@ -38,20 +37,17 @@ public class ReportController { private final ReportHistoryRepository reportHistoryRepository; private final MinIOService minIOService; private final DeepResearchService deepResearchService; - private final ResearchPdfService researchPdfService; private final ReportSynthesisService reportSynthesisService; public ReportController(ReportGenerationService reportGenerationService, ReportHistoryRepository reportHistoryRepository, MinIOService minIOService, DeepResearchService deepResearchService, - ResearchPdfService researchPdfService, ReportSynthesisService reportSynthesisService) { this.reportGenerationService = reportGenerationService; this.reportHistoryRepository = reportHistoryRepository; this.minIOService = minIOService; this.deepResearchService = deepResearchService; - this.researchPdfService = researchPdfService; this.reportSynthesisService = reportSynthesisService; } @@ -79,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()) { @@ -94,61 +90,52 @@ public class ReportController { request.getBreadth(), request.getReportType()); - // Генерация уникального ID задачи - String taskId = UUID.randomUUID().toString(); - - // Предполагаемое время завершения - LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8); - - // Запускаем асинхронный процесс: deep-research -> synthesis via Ollama -> PDF - // -> сохранение - deepResearchService.generateReportAsync(deepRequest) + // Синхронно выполняем: deep-research -> synthesis via Ollama -> Markdown + DeepResearchResponse researchResponse = deepResearchService.generateReportAsync(deepRequest) .publishOn(Schedulers.boundedElastic()) - .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]; - byte[] pdfBytes = researchPdfService.generatePdfReport(request.getQuery(), - synthesizedMarkdown, researchResponse.getVisitedUrls()); - String filename = "research_report_" + System.currentTimeMillis() + ".pdf"; - saveResearchReportToHistory(request, filename, pdfBytes, 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()); - }); + .block(); + if (researchResponse == null || researchResponse.getLearnings() == null + || researchResponse.getLearnings().isEmpty()) { + return ResponseEntity.internalServerError().build(); + } - ReportGenerationResponse response = new ReportGenerationResponse( - taskId, - "Исследование запущено в фоновом режиме. Ожидаемое время завершения: " - + estimatedCompletion - .format(java.time.format.DateTimeFormatter.ofPattern( - "HH:mm")), - estimatedCompletion, - "PROCESSING"); + String synthesizedMarkdown = reportSynthesisService + .synthesizeReport(request.getQuery(), researchResponse.getLearnings(), + request.getLang()) + .publishOn(Schedulers.boundedElastic()) + .block(); + if (synthesizedMarkdown == null) { + return ResponseEntity.internalServerError().build(); + } - return ResponseEntity.ok(ApiResponse.success("Исследовательский отчёт поставлен в очередь", response)); + 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); + } + + private String buildMarkdownWithSources(String synthesizedMarkdown, java.util.List visitedUrls) { + StringBuilder sb = new StringBuilder(); + sb.append(synthesizedMarkdown == null ? "" : synthesizedMarkdown.trim()); + sb.append("\n\n---\n\n"); + sb.append("## Источники\n\n"); + if (visitedUrls == null || visitedUrls.isEmpty()) { + sb.append("(источники не предоставлены)\n"); + return sb.toString(); + } + for (int i = 0; i < visitedUrls.size(); i++) { + String url = visitedUrls.get(i); + if (url == null || url.trim().isEmpty()) { + continue; + } + sb.append((i + 1)).append(". ").append(url.trim()).append("\n"); + } + return sb.toString(); } private void saveResearchReportToHistory(ResearchReportRequest request, String filename, diff --git a/Техническое Задание на доработку AI-агента для генерации отчётов в формате Markdown.md b/Техническое Задание на доработку AI-агента для генерации отчётов в формате Markdown.md new file mode 100644 index 0000000..06f1d4d --- /dev/null +++ b/Техническое Задание на доработку AI-агента для генерации отчётов в формате Markdown.md @@ -0,0 +1,85 @@ +### **Техническое Задание на доработку AI-агента для генерации отчётов в формате Markdown (.md)** + +**Проект:** Модификация существующего бэкенда для генерации отчётов. + +#### **1. Общее описание** + +Целью данной доработки является замена модуля генерации PDF-файлов на более простой и быстрый модуль, который будет формировать и отдавать пользователю итоговый отчёт в текстовом формате **Markdown (`.md`)**. + +Бэкенд по-прежнему будет выполнять многоступенчатый процесс исследования (вызов `deep-research` API, затем синтез отчёта через `Ollama`), но финальным результатом будет текстовый `.md` файл, а не PDF. + +#### **2. Требования к реализации** + +**2.1. Модификация эндпоинта `/api/parser/report`** + +- **Метод:** `POST` + +- **Тело запроса (`Request Body`):** Остаётся без изменений (принимает `query`, `lang`, `depth` и т.д.). + +- **Успешный ответ (`Success Response`):** **(Ключевое изменение)** + + - **Код:** `200 OK` + - **Заголовки:** + - `Content-Type: text/markdown; charset=UTF-8` (Важно для корректного отображения кириллицы). + - `Content-Disposition: attachment; filename="research_report.md"` (Этот заголовок заставит браузер скачать файл, а не просто показать его). + - **Тело ответа:** Готовый текст отчёта в формате Markdown. + +- **Ответы с ошибками (`Error Responses`):** Остаются без изменений (`400`, `500` и т.д.). + +**2.2. Изменение логики работы агента** + +Процесс будет выглядеть так: + +1. **Приём и валидация запроса:** Логика остаётся без изменений. +2. **Вызов `deep-research` API:** Логика остаётся без изменений. Бэкенд получает "сырые" `learnings` и `visitedUrls`. +3. **Вызов Ollama для синтеза:** Логика остаётся без изменений. Бэкенд получает от Ollama финальный, красиво структурированный отчёт в виде строки Markdown. +4. **ОТМЕНА:** Шаг генерации PDF полностью удаляется. Сервис `ResearchPdfService` и зависимость от библиотеки iText больше не нужны. +5. **НОВЫЙ ШАГ: Формирование итогового `.md` файла:** + - Бэкенд берёт строку Markdown, полученную от Ollama. + - К этой строке в конец добавляется раздел "Источники". Бэкенд должен программно сгенерировать этот раздел, добавив заголовок `## Источники` и пронумерованный список URL-адресов из `visitedUrls`. +6. **Отправка `.md` файла клиенту:** Бэкенд отправляет итоговую строку (отчёт + источники) как тело HTTP-ответа с заголовками, указанными в п. 2.1. + +**2.3. Формат итогового Markdown-файла** + +Итоговая строка, которая будет отправлена клиенту, должна иметь следующую структуру: + +```markdown +# {Заголовок, сгенерированный Ollama, или просто ваш query} + +## Введение + +... текст от Ollama ... + +## Основная часть + +... текст от Ollama ... + +### Подраздел + +... текст от Ollama ... + +## Заключение + +... текст от Ollama ... + +--- + +## Источники + +1. https://... +2. https://... +3. ... +``` + +#### **3. Технологический стек** + +- **Бэкенд:** Java/Spring Boot (без изменений). +- **HTTP-клиент:** `WebClient` (без изменений) + +#### **4. Нефункциональные требования** + +- **Асинхронность:** Рекомендация по асинхронной обработке запроса (через `jobId` и отдельный эндпоинт для статуса) остаётся актуальной, так как сам процесс исследования по-прежнему занимает много времени. + +#### **5. Ожидаемый результат** + +При отправке `POST` запроса на эндпоинт `/api/parser/report` браузер пользователя должен инициировать скачивание файла `research_report.md`. Этот файл должен корректно открываться в любом текстовом редакторе, поддерживающем Markdown, и содержать полный, структурированный отчёт на русском языке вместе со списком источников.