This commit is contained in:
root
2025-10-05 15:14:41 +05:00
parent 2ef8f0dc3a
commit 2d918f7d02
4 changed files with 127 additions and 152 deletions
@@ -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;
}
}
}