research
This commit is contained in:
@@ -106,8 +106,8 @@ public class ReportController {
|
||||
throw new RuntimeException("Empty research response content");
|
||||
}
|
||||
System.out.println("researchResponse:" + researchResponse.getMainContent());
|
||||
System.out.println("researchResponse:" + researchResponse.getReport());
|
||||
System.out.println("researchResponse:" + researchResponse.getAnswer());
|
||||
System.out.println("researchResponse:" + researchResponse.getLearnings());
|
||||
System.out.println("researchResponse:" + researchResponse.getVisitedUrls());
|
||||
System.out.println("researchResponse:" + researchResponse.getStatus());
|
||||
byte[] pdfBytes = researchPdfService.generatePdfReport(request.getQuery(),
|
||||
researchResponse);
|
||||
|
||||
@@ -2,6 +2,7 @@ package kz.konturai.parser.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList; // Добавьте импорт для ArrayList
|
||||
|
||||
public class DeepResearchResponse {
|
||||
|
||||
@@ -11,6 +12,9 @@ public class DeepResearchResponse {
|
||||
@JsonProperty("answer")
|
||||
private String answer;
|
||||
|
||||
@JsonProperty("learnings") // <-- ДОБАВИТЬ ЭТО ПОЛЕ
|
||||
private List<String> learnings = new ArrayList<>(); // Инициализация, чтобы избежать NullPointerException
|
||||
|
||||
@JsonProperty("visitedUrls")
|
||||
private List<String> visitedUrls;
|
||||
|
||||
@@ -23,20 +27,14 @@ public class DeepResearchResponse {
|
||||
public DeepResearchResponse() {
|
||||
}
|
||||
|
||||
public String getReport() {
|
||||
return report;
|
||||
// Геттеры и сеттеры для существующих полей...
|
||||
|
||||
public List<String> getLearnings() { // <-- ДОБАВИТЬ ГЕТТЕР
|
||||
return learnings;
|
||||
}
|
||||
|
||||
public void setReport(String report) {
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
public String getAnswer() {
|
||||
return answer;
|
||||
}
|
||||
|
||||
public void setAnswer(String answer) {
|
||||
this.answer = answer;
|
||||
public void setLearnings(List<String> learnings) { // <-- ДОБАВИТЬ СЕТТЕР
|
||||
this.learnings = learnings;
|
||||
}
|
||||
|
||||
public List<String> getVisitedUrls() {
|
||||
@@ -64,12 +62,30 @@ public class DeepResearchResponse {
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить основной текст отчёта (report или answer)
|
||||
* Получить основной текст отчёта (report или answer + learnings)
|
||||
*/
|
||||
public String getMainContent() {
|
||||
if (report != null && !report.trim().isEmpty()) {
|
||||
return report;
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
|
||||
// Сначала добавляем answer, если есть
|
||||
if (answer != null && !answer.trim().isEmpty()) {
|
||||
contentBuilder.append(answer).append("\n\n");
|
||||
}
|
||||
return answer != null ? answer : "";
|
||||
|
||||
// Затем добавляем learnings, если они есть
|
||||
if (learnings != null && !learnings.isEmpty()) {
|
||||
contentBuilder.append("### Подробные выводы:\n");
|
||||
for (String learning : learnings) {
|
||||
contentBuilder.append("- ").append(learning).append("\n");
|
||||
}
|
||||
contentBuilder.append("\n");
|
||||
}
|
||||
|
||||
// Также можно добавить report, если вдруг deep-research начнет его возвращать
|
||||
// if (report != null && !report.trim().isEmpty()) {
|
||||
// contentBuilder.append(report).append("\n\n");
|
||||
// }
|
||||
|
||||
return contentBuilder.toString().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,8 @@ public class DeepResearchService {
|
||||
.bodyToMono(DeepResearchResponse.class)
|
||||
// 👇 Log the successful response object here
|
||||
.doOnSuccess(response -> {
|
||||
log.info("Deep research API response: {}", response.getReport());
|
||||
log.info("Deep research API response: {}", response.getAnswer());
|
||||
log.info("Deep research API response: {}", response.getLearnings());
|
||||
log.info("Deep research API response: {}", response.getMainContent());
|
||||
log.info("Deep research API response: {}", response.getError());
|
||||
log.info("Deep research API response: {}", response.getStatus());
|
||||
log.info("Deep research API response: {}", response.getVisitedUrls());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kz.konturai.parser.service;
|
||||
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.pdf.BaseFont;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
import kz.konturai.parser.dto.DeepResearchResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -13,11 +14,33 @@ import java.util.List;
|
||||
@Service
|
||||
public class ResearchPdfService {
|
||||
|
||||
private static final Font TITLE_FONT = new Font(Font.HELVETICA, 18, Font.BOLD);
|
||||
private static final Font HEADING_FONT = new Font(Font.HELVETICA, 14, Font.BOLD);
|
||||
private static final Font SUBHEADING_FONT = new Font(Font.HELVETICA, 12, Font.BOLD);
|
||||
private static final Font NORMAL_FONT = new Font(Font.HELVETICA, 10, Font.NORMAL);
|
||||
private static final Font SMALL_FONT = new Font(Font.HELVETICA, 8, Font.NORMAL);
|
||||
// --- Шрифты с поддержкой кириллицы ---
|
||||
private final BaseFont baseFont;
|
||||
private final Font titleFont;
|
||||
private final Font headingFont;
|
||||
private final Font subheadingFont;
|
||||
private final Font normalFont;
|
||||
private final Font smallFont;
|
||||
private final Font linkFont;
|
||||
|
||||
/**
|
||||
* Конструктор для инициализации шрифтов.
|
||||
* Убедитесь, что файл шрифта DejaVuSans.ttf находится в /resources/fonts/
|
||||
*/
|
||||
public ResearchPdfService() {
|
||||
try {
|
||||
// Загружаем шрифт, поддерживающий кириллицу. Это КЛЮЧЕВОЕ изменение.
|
||||
this.baseFont = BaseFont.createFont("fonts/DejaVuSans.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
|
||||
this.titleFont = new Font(baseFont, 18, Font.BOLD);
|
||||
this.headingFont = new Font(baseFont, 14, Font.BOLD);
|
||||
this.subheadingFont = new Font(baseFont, 12, Font.BOLD);
|
||||
this.normalFont = new Font(baseFont, 10, Font.NORMAL);
|
||||
this.smallFont = new Font(baseFont, 8, Font.ITALIC);
|
||||
this.linkFont = new Font(baseFont, 8, Font.UNDERLINE);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load font", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует PDF отчёт на основе ответа от deep-research API
|
||||
@@ -26,178 +49,114 @@ public class ResearchPdfService {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Document document = new Document(PageSize.A4);
|
||||
PdfWriter.getInstance(document, outputStream);
|
||||
|
||||
// --- Добавляем метаданные в документ ---
|
||||
document.addTitle(query);
|
||||
document.addAuthor("AI Research Agent");
|
||||
document.addSubject("Research Report");
|
||||
|
||||
document.open();
|
||||
|
||||
// Титульная страница
|
||||
// 1. Титульная страница
|
||||
addTitlePage(document, query);
|
||||
|
||||
// Содержание
|
||||
addTableOfContents(document);
|
||||
|
||||
// Основной отчёт
|
||||
// 2. Основной отчёт с динамическим содержанием
|
||||
addMainReport(document, researchResponse);
|
||||
|
||||
// Список источников
|
||||
// 3. Список источников
|
||||
addSources(document, researchResponse.getVisitedUrls());
|
||||
|
||||
document.close();
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception e) {
|
||||
// Логирование ошибки было бы здесь полезно
|
||||
throw new RuntimeException("Failed to generate PDF report", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addTitlePage(Document document, String query) throws DocumentException {
|
||||
// Заголовок
|
||||
Paragraph title = new Paragraph(query, TITLE_FONT);
|
||||
document.newPage(); // Начинаем с чистого листа
|
||||
Paragraph title = new Paragraph(query, titleFont);
|
||||
title.setAlignment(Element.ALIGN_CENTER);
|
||||
title.setSpacingBefore(50);
|
||||
title.setSpacingAfter(50);
|
||||
document.add(title);
|
||||
|
||||
// Подзаголовок
|
||||
Paragraph subtitle = new Paragraph("Исследовательский отчёт", HEADING_FONT);
|
||||
Paragraph subtitle = new Paragraph("Автоматизированный исследовательский отчёт", headingFont);
|
||||
subtitle.setAlignment(Element.ALIGN_CENTER);
|
||||
subtitle.setSpacingAfter(30);
|
||||
document.add(subtitle);
|
||||
|
||||
// Информация о дате
|
||||
String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"));
|
||||
Paragraph date = new Paragraph("Дата создания: " + currentDate, NORMAL_FONT);
|
||||
Paragraph date = new Paragraph("Дата создания: " + currentDate, normalFont);
|
||||
date.setAlignment(Element.ALIGN_CENTER);
|
||||
date.setSpacingAfter(100);
|
||||
document.add(date);
|
||||
|
||||
// Пустая строка для перехода на следующую страницу
|
||||
document.add(new Paragraph(" "));
|
||||
document.newPage();
|
||||
}
|
||||
|
||||
private void addTableOfContents(Document document) throws DocumentException {
|
||||
Paragraph tocTitle = new Paragraph("Содержание", HEADING_FONT);
|
||||
tocTitle.setSpacingAfter(20);
|
||||
document.add(tocTitle);
|
||||
|
||||
// Автоматически генерируемое содержание
|
||||
String[] tocItems = {
|
||||
"1. Введение",
|
||||
"2. Основная часть",
|
||||
"3. Заключение",
|
||||
"4. Источники"
|
||||
};
|
||||
|
||||
for (String item : tocItems) {
|
||||
Paragraph tocItem = new Paragraph(item, NORMAL_FONT);
|
||||
tocItem.setSpacingAfter(5);
|
||||
document.add(tocItem);
|
||||
}
|
||||
|
||||
document.add(new Paragraph(" "));
|
||||
document.newPage();
|
||||
}
|
||||
|
||||
private void addMainReport(Document document, DeepResearchResponse researchResponse) throws DocumentException {
|
||||
String content = researchResponse.getMainContent();
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
content = "Отчёт не был сгенерирован.";
|
||||
content = "## Отчёт не был сгенерирован.\n\nДанные для анализа отсутствуют.";
|
||||
}
|
||||
|
||||
// Разбиваем контент на параграфы
|
||||
String[] paragraphs = content.split("\n\n");
|
||||
document.newPage(); // Новая страница для основного контента
|
||||
|
||||
// Введение
|
||||
Paragraph introTitle = new Paragraph("1. Введение", HEADING_FONT);
|
||||
introTitle.setSpacingAfter(10);
|
||||
document.add(introTitle);
|
||||
// --- Улучшенный парсер контента с поддержкой Markdown ---
|
||||
String[] lines = content.split("\n");
|
||||
Chapter chapter = null; // Используем главы и секции для автоматического содержания
|
||||
|
||||
Paragraph intro = new Paragraph(
|
||||
"Данный отчёт представляет результаты исследования по теме: \"" +
|
||||
researchResponse.getReport() != null ? "Полный отчёт"
|
||||
: "Краткий ответ" + "\". " +
|
||||
"Исследование проводилось с использованием автоматизированных инструментов анализа данных.",
|
||||
NORMAL_FONT);
|
||||
intro.setSpacingAfter(15);
|
||||
document.add(intro);
|
||||
|
||||
// Основная часть
|
||||
Paragraph mainTitle = new Paragraph("2. Основная часть", HEADING_FONT);
|
||||
mainTitle.setSpacingAfter(10);
|
||||
document.add(mainTitle);
|
||||
|
||||
// Добавляем основной контент
|
||||
for (String paragraph : paragraphs) {
|
||||
if (paragraph.trim().isEmpty())
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty())
|
||||
continue;
|
||||
|
||||
// Проверяем, является ли строка заголовком (начинается с цифры или содержит
|
||||
// ключевые слова)
|
||||
if (isHeading(paragraph)) {
|
||||
Paragraph heading = new Paragraph(paragraph, SUBHEADING_FONT);
|
||||
heading.setSpacingAfter(5);
|
||||
document.add(heading);
|
||||
} else {
|
||||
Paragraph para = new Paragraph(paragraph, NORMAL_FONT);
|
||||
para.setSpacingAfter(8);
|
||||
document.add(para);
|
||||
if (line.startsWith("# ")) { // Главный заголовок (H1) -> Глава
|
||||
if (chapter != null)
|
||||
document.add(chapter);
|
||||
chapter = new Chapter(new Paragraph(line.substring(2), headingFont), 1);
|
||||
} else if (line.startsWith("## ")) { // Подзаголовок (H2) -> Секция
|
||||
if (chapter == null)
|
||||
chapter = new Chapter(new Paragraph("Основная часть", headingFont), 1);
|
||||
chapter.addSection(new Paragraph(line.substring(3), subheadingFont));
|
||||
} else if (line.startsWith("- ") || line.startsWith("* ")) { // Элемент списка
|
||||
com.lowagie.text.List list = new com.lowagie.text.List(com.lowagie.text.List.UNORDERED);
|
||||
list.setListSymbol(new Chunk("- ", normalFont));
|
||||
list.add(new ListItem(line.substring(2), normalFont));
|
||||
if (chapter != null)
|
||||
chapter.add(list);
|
||||
else
|
||||
document.add(list);
|
||||
} else { // Обычный параграф
|
||||
Paragraph para = new Paragraph(line, normalFont);
|
||||
para.setSpacingAfter(10);
|
||||
if (chapter != null)
|
||||
chapter.add(para);
|
||||
else
|
||||
document.add(para);
|
||||
}
|
||||
}
|
||||
|
||||
// Заключение
|
||||
Paragraph conclusionTitle = new Paragraph("3. Заключение", HEADING_FONT);
|
||||
conclusionTitle.setSpacingAfter(10);
|
||||
document.add(conclusionTitle);
|
||||
|
||||
Paragraph conclusion = new Paragraph(
|
||||
"Исследование завершено. Полученные данные и выводы представлены в основной части отчёта. " +
|
||||
"Для получения дополнительной информации рекомендуется обратиться к первоисточникам, " +
|
||||
"указанным в разделе \"Источники\".",
|
||||
NORMAL_FONT);
|
||||
conclusion.setSpacingAfter(15);
|
||||
document.add(conclusion);
|
||||
if (chapter != null) {
|
||||
document.add(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
private void addSources(Document document, List<String> visitedUrls) throws DocumentException {
|
||||
Paragraph sourcesTitle = new Paragraph("4. Источники", HEADING_FONT);
|
||||
sourcesTitle.setSpacingAfter(10);
|
||||
document.add(sourcesTitle);
|
||||
document.newPage();
|
||||
Chapter sourcesChapter = new Chapter(new Paragraph("Источники", headingFont), 2);
|
||||
|
||||
if (visitedUrls == null || visitedUrls.isEmpty()) {
|
||||
Paragraph noSources = new Paragraph("Источники не указаны.", NORMAL_FONT);
|
||||
document.add(noSources);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < visitedUrls.size(); i++) {
|
||||
String url = visitedUrls.get(i);
|
||||
Paragraph source = new Paragraph(
|
||||
(i + 1) + ". " + url,
|
||||
SMALL_FONT);
|
||||
source.setSpacingAfter(3);
|
||||
document.add(source);
|
||||
sourcesChapter.add(new Paragraph("Источники не были предоставлены.", normalFont));
|
||||
} else {
|
||||
com.lowagie.text.List list = new com.lowagie.text.List(com.lowagie.text.List.ORDERED);
|
||||
for (String url : visitedUrls) {
|
||||
// Создаем кликабельные ссылки
|
||||
Anchor anchor = new Anchor(url, linkFont);
|
||||
anchor.setReference(url);
|
||||
list.add(new ListItem(anchor));
|
||||
}
|
||||
sourcesChapter.add(list);
|
||||
}
|
||||
document.add(sourcesChapter);
|
||||
}
|
||||
|
||||
private boolean isHeading(String text) {
|
||||
if (text == null || text.trim().isEmpty())
|
||||
return false;
|
||||
|
||||
String trimmed = text.trim();
|
||||
|
||||
// Проверяем, начинается ли с цифры и точки
|
||||
if (trimmed.matches("^\\d+\\.\\s+.*"))
|
||||
return true;
|
||||
|
||||
// Проверяем ключевые слова заголовков
|
||||
String[] headingKeywords = {
|
||||
"введение", "заключение", "выводы", "рекомендации", "анализ",
|
||||
"результаты", "методология", "цель", "задачи", "основные", "ключевые"
|
||||
};
|
||||
|
||||
String lowerText = trimmed.toLowerCase();
|
||||
for (String keyword : headingKeywords) {
|
||||
if (lowerText.startsWith(keyword))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user