diff --git a/src/main/java/kz/konturai/parser/controller/ReportController.java b/src/main/java/kz/konturai/parser/controller/ReportController.java index 36a3d8b..640d41f 100644 --- a/src/main/java/kz/konturai/parser/controller/ReportController.java +++ b/src/main/java/kz/konturai/parser/controller/ReportController.java @@ -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); diff --git a/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java b/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java index 127b088..b871986 100644 --- a/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java +++ b/src/main/java/kz/konturai/parser/dto/DeepResearchResponse.java @@ -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 learnings = new ArrayList<>(); // Инициализация, чтобы избежать NullPointerException + @JsonProperty("visitedUrls") private List visitedUrls; @@ -23,20 +27,14 @@ public class DeepResearchResponse { public DeepResearchResponse() { } - public String getReport() { - return report; + // Геттеры и сеттеры для существующих полей... + + public List 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 learnings) { // <-- ДОБАВИТЬ СЕТТЕР + this.learnings = learnings; } public List 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(); } -} +} \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/DeepResearchService.java b/src/main/java/kz/konturai/parser/service/DeepResearchService.java index f8cda47..ddf2013 100644 --- a/src/main/java/kz/konturai/parser/service/DeepResearchService.java +++ b/src/main/java/kz/konturai/parser/service/DeepResearchService.java @@ -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()); diff --git a/src/main/java/kz/konturai/parser/service/ResearchPdfService.java b/src/main/java/kz/konturai/parser/service/ResearchPdfService.java index 5f09098..cf662d6 100644 --- a/src/main/java/kz/konturai/parser/service/ResearchPdfService.java +++ b/src/main/java/kz/konturai/parser/service/ResearchPdfService.java @@ -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 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; - } -} +} \ No newline at end of file