final markdown

This commit is contained in:
root
2025-10-05 17:23:34 +05:00
parent e5b292fea4
commit 51a5fc398c
2 changed files with 129 additions and 57 deletions
@@ -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<ApiResponse<ReportGenerationResponse>> generateResearchReport(
public ResponseEntity<byte[]> 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<String> 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,
@@ -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, и содержать полный, структурированный отчёт на русском языке вместе со списком источников.