final markdown
This commit is contained in:
@@ -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,
|
||||
|
||||
+85
@@ -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, и содержать полный, структурированный отчёт на русском языке вместе со списком источников.
|
||||
Reference in New Issue
Block a user