diff --git a/src/main/java/kz/konturai/parser/dto/ChartData.java b/src/main/java/kz/konturai/parser/dto/ChartData.java new file mode 100644 index 0000000..486555f --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/ChartData.java @@ -0,0 +1,43 @@ +package kz.konturai.parser.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChartData { + private String chartType; + private String title; + private java.util.List labels; + private java.util.List data; + + public String getChartType() { + return chartType; + } + + public void setChartType(String chartType) { + this.chartType = chartType; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public java.util.List getLabels() { + return labels; + } + + public void setLabels(java.util.List labels) { + this.labels = labels; + } + + public java.util.List getData() { + return data; + } + + public void setData(java.util.List data) { + this.data = data; + } +} diff --git a/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java b/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java index 63bb498..d0ba2c3 100644 --- a/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java +++ b/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java @@ -1,9 +1,14 @@ package kz.konturai.parser.service; import kz.konturai.parser.dto.FinalReportPayload; +import kz.konturai.parser.dto.ChartData; import kz.konturai.parser.util.SvgToPngConverter; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.List; @@ -14,6 +19,7 @@ import java.util.stream.Collectors; public class ReportSynthesisService { private final OllamaAnalyticsService ollamaAnalyticsService; + private final ObjectMapper objectMapper = new ObjectMapper(); public ReportSynthesisService(OllamaAnalyticsService ollamaAnalyticsService) { this.ollamaAnalyticsService = ollamaAnalyticsService; @@ -31,53 +37,41 @@ public class ReportSynthesisService { Mono textMono = Mono.fromCallable( () -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, - buildTextPrompt(originalQuery, aggregatedLearnings, lang))); + buildTextPrompt(originalQuery, aggregatedLearnings, lang))) + .subscribeOn(Schedulers.boundedElastic()); Mono> chartsMono = Mono.fromCallable(() -> { - String json = extractSvgCode(ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, - buildChartDataPrompt(aggregatedLearnings))); - if (json == null || json.trim().isEmpty() || json.trim().equals("{}")) { + String jsonArrayString = ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, + buildChartDataPrompt(aggregatedLearnings)); + String cleanJsonArray = extractJsonArray(jsonArrayString); + if (cleanJsonArray == null || cleanJsonArray.isEmpty() || cleanJsonArray.equals("[]")) { return new ArrayList(); } - String svg = ollamaAnalyticsService.generateWithInstruction(json, buildSvgPrompt(json)); - byte[] png = SvgToPngConverter.convert(svg); - List list = new ArrayList<>(); - if (png != null && png.length > 0) { - list.add(png); + List chartDataList = objectMapper.readValue(cleanJsonArray, + new TypeReference>() { + }); + List chartImages = new ArrayList<>(); + for (ChartData chartData : chartDataList) { + try { + String chartDataJson = objectMapper.writeValueAsString(chartData); + String rawSvgResponse = ollamaAnalyticsService.generateWithInstruction(chartDataJson, + buildSvgPrompt(chartDataJson)); + String cleanSvg = extractSvgCode(rawSvgResponse); + byte[] png = SvgToPngConverter.convert(cleanSvg); + if (png != null && png.length > 0) { + chartImages.add(png); + } + } catch (Exception e) { + System.err.println("Failed to generate a chart: " + e.getMessage()); + } } - return list; - }); + return chartImages; + }).subscribeOn(Schedulers.boundedElastic()); return Mono.zip(textMono, chartsMono) .map(tuple -> new FinalReportPayload(tuple.getT1(), tuple.getT2())); } - private String extractSvgCode(String ollamaResponse) { - if (ollamaResponse == null || ollamaResponse.isBlank()) { - return null; - } - - // Try to find content within XML markdown block first - int startXml = ollamaResponse.indexOf("```xml"); - if (startXml != -1) { - int startSvg = ollamaResponse.indexOf(""); - if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) { - return ollamaResponse.substring(startSvg, endSvg + 6); - } - } - - // Fallback: find the first and last tags in the whole string - int startSvg = ollamaResponse.indexOf(""); - - if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) { - return ollamaResponse.substring(startSvg, endSvg + 6); - } - - return null; // Return null if no SVG found - } - private String formatLearnings(List learnings) { if (learnings == null || learnings.isEmpty()) { return "- (данные не были предоставлены)"; @@ -110,7 +104,7 @@ public class ReportSynthesisService { } private String buildChartDataPrompt(String learningsAsString) { - return "Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Если найдёшь в нём числовые данные, которые можно представить в виде простой столбчатой или круговой диаграммы (например, рост по годам, доли рынка, сравнение показателей), верни ТОЛЬКО один JSON-объект со структурой: {\\\"chartType\\\": \\\"bar\\\" или \\\"pie\\\", \\\"title\\\": \\\"Название диаграммы\\\", \\\"labels\\\": [\\\"Метка 1\\\", \\\"Метка 2\\\"], \\\"data\\\": [число1, число2]}. Если подходящих данных нет, верни пустой JSON-объект {}. Текст для анализа:\n---\n" + return "Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Найди в нём все наборы числовых данных, которые можно представить в виде простых диаграмм (столбчатых или круговых). Для КАЖДОГО найденного набора данных создай JSON-объект. Верни ТОЛЬКО JSON-массив этих объектов. Структура каждого объекта: {\"chartType\": \"bar\" или \"pie\", \"title\": \"Название диаграммы\", \"labels\": [\"Метка 1\"], \"data\": [число1]}. Если подходящих данных нет, верни пустой массив []. Текст для анализа:\n---\n" + learningsAsString + "\n---"; } @@ -118,4 +112,35 @@ public class ReportSynthesisService { return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. SVG должен быть стильным и читаемым, с подписями на русском языке. Не добавляй никаких комментариев, верни ТОЛЬКО SVG-код. JSON с данными:\n---\n" + json + "\n---"; } + + private String extractJsonArray(String ollamaResponse) { + if (ollamaResponse == null) + return null; + int startIndex = ollamaResponse.indexOf("["); + int endIndex = ollamaResponse.lastIndexOf("]"); + if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) { + return ollamaResponse.substring(startIndex, endIndex + 1); + } + return null; + } + + private String extractSvgCode(String ollamaResponse) { + if (ollamaResponse == null || ollamaResponse.isBlank()) { + return null; + } + int startXml = ollamaResponse.indexOf("```xml"); + if (startXml != -1) { + int startSvg = ollamaResponse.indexOf(""); + if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) { + return ollamaResponse.substring(startSvg, endSvg + 6); + } + } + int startSvg = ollamaResponse.indexOf(""); + if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) { + return ollamaResponse.substring(startSvg, endSvg + 6); + } + return null; + } }