This commit is contained in:
root
2025-10-06 18:59:45 +05:00
parent 2d5765337e
commit c656aa90d2
2 changed files with 106 additions and 38 deletions
@@ -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<String> labels;
private java.util.List<Number> 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<String> getLabels() {
return labels;
}
public void setLabels(java.util.List<String> labels) {
this.labels = labels;
}
public java.util.List<Number> getData() {
return data;
}
public void setData(java.util.List<Number> data) {
this.data = data;
}
}
@@ -1,9 +1,14 @@
package kz.konturai.parser.service; package kz.konturai.parser.service;
import kz.konturai.parser.dto.FinalReportPayload; import kz.konturai.parser.dto.FinalReportPayload;
import kz.konturai.parser.dto.ChartData;
import kz.konturai.parser.util.SvgToPngConverter; import kz.konturai.parser.util.SvgToPngConverter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; 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.ArrayList;
import java.util.List; import java.util.List;
@@ -14,6 +19,7 @@ import java.util.stream.Collectors;
public class ReportSynthesisService { public class ReportSynthesisService {
private final OllamaAnalyticsService ollamaAnalyticsService; private final OllamaAnalyticsService ollamaAnalyticsService;
private final ObjectMapper objectMapper = new ObjectMapper();
public ReportSynthesisService(OllamaAnalyticsService ollamaAnalyticsService) { public ReportSynthesisService(OllamaAnalyticsService ollamaAnalyticsService) {
this.ollamaAnalyticsService = ollamaAnalyticsService; this.ollamaAnalyticsService = ollamaAnalyticsService;
@@ -31,53 +37,41 @@ public class ReportSynthesisService {
Mono<String> textMono = Mono.fromCallable( Mono<String> textMono = Mono.fromCallable(
() -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, () -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings,
buildTextPrompt(originalQuery, aggregatedLearnings, lang))); buildTextPrompt(originalQuery, aggregatedLearnings, lang)))
.subscribeOn(Schedulers.boundedElastic());
Mono<List<byte[]>> chartsMono = Mono.fromCallable(() -> { Mono<List<byte[]>> chartsMono = Mono.fromCallable(() -> {
String json = extractSvgCode(ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, String jsonArrayString = ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings,
buildChartDataPrompt(aggregatedLearnings))); buildChartDataPrompt(aggregatedLearnings));
if (json == null || json.trim().isEmpty() || json.trim().equals("{}")) { String cleanJsonArray = extractJsonArray(jsonArrayString);
if (cleanJsonArray == null || cleanJsonArray.isEmpty() || cleanJsonArray.equals("[]")) {
return new ArrayList<byte[]>(); return new ArrayList<byte[]>();
} }
String svg = ollamaAnalyticsService.generateWithInstruction(json, buildSvgPrompt(json)); List<ChartData> chartDataList = objectMapper.readValue(cleanJsonArray,
byte[] png = SvgToPngConverter.convert(svg); new TypeReference<List<ChartData>>() {
List<byte[]> list = new ArrayList<>(); });
if (png != null && png.length > 0) { List<byte[]> chartImages = new ArrayList<>();
list.add(png); 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) return Mono.zip(textMono, chartsMono)
.map(tuple -> new FinalReportPayload(tuple.getT1(), tuple.getT2())); .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("<svg", startXml);
int endSvg = ollamaResponse.lastIndexOf("</svg>");
if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) {
return ollamaResponse.substring(startSvg, endSvg + 6);
}
}
// Fallback: find the first <svg> and last </svg> tags in the whole string
int startSvg = ollamaResponse.indexOf("<svg");
int endSvg = ollamaResponse.lastIndexOf("</svg>");
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<String> learnings) { private String formatLearnings(List<String> learnings) {
if (learnings == null || learnings.isEmpty()) { if (learnings == null || learnings.isEmpty()) {
return "- (данные не были предоставлены)"; return "- (данные не были предоставлены)";
@@ -110,7 +104,7 @@ public class ReportSynthesisService {
} }
private String buildChartDataPrompt(String learningsAsString) { 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---"; + learningsAsString + "\n---";
} }
@@ -118,4 +112,35 @@ public class ReportSynthesisService {
return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. SVG должен быть стильным и читаемым, с подписями на русском языке. Не добавляй никаких комментариев, верни ТОЛЬКО SVG-код. JSON с данными:\n---\n" return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. SVG должен быть стильным и читаемым, с подписями на русском языке. Не добавляй никаких комментариев, верни ТОЛЬКО SVG-код. JSON с данными:\n---\n"
+ json + "\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("<svg", startXml);
int endSvg = ollamaResponse.lastIndexOf("</svg>");
if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) {
return ollamaResponse.substring(startSvg, endSvg + 6);
}
}
int startSvg = ollamaResponse.indexOf("<svg");
int endSvg = ollamaResponse.lastIndexOf("</svg>");
if (startSvg != -1 && endSvg != -1 && endSvg > startSvg) {
return ollamaResponse.substring(startSvg, endSvg + 6);
}
return null;
}
} }