This commit is contained in:
root
2025-10-06 19:22:37 +05:00
parent c656aa90d2
commit e6e4f6b809
4 changed files with 230 additions and 27 deletions
@@ -0,0 +1,90 @@
package kz.konturai.parser.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class OpenAiChartService {
private final WebClient webClient;
@Value("${openai.model.name:gpt-4o-mini}")
private String modelName;
@Value("${openai.timeoutMs:90000}")
private long timeoutMs;
public OpenAiChartService(
@Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String apiUrl,
@Value("${openai.api.key:}") String apiKey) {
HttpClient httpClient = HttpClient.create();
this.webClient = WebClient.builder()
.baseUrl(apiUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
public Mono<String> getChartDataJson(String aggregatedLearnings) {
String prompt = buildChartDataPrompt(aggregatedLearnings);
return callChatCompletions(prompt);
}
public Mono<String> getChartSvg(String jsonData) {
String prompt = buildSvgPrompt(jsonData);
return callChatCompletions(prompt);
}
private Mono<String> callChatCompletions(String prompt) {
Map<String, Object> body = new HashMap<>();
body.put("model", modelName);
body.put("temperature", 0);
body.put("messages", List.of(
Map.of("role", "user", "content", prompt)));
return this.webClient.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
})
.timeout(Duration.ofMillis(timeoutMs))
.map(resp -> {
try {
List<Map<String, Object>> choices = (List<Map<String, Object>>) resp.get("choices");
if (choices == null || choices.isEmpty())
return null;
Map<String, Object> choice0 = choices.get(0);
Map<String, Object> message = (Map<String, Object>) choice0.get("message");
if (message == null)
return null;
Object content = message.get("content");
return content == null ? null : String.valueOf(content);
} catch (Exception e) {
return null;
}
});
}
private String buildChartDataPrompt(String learningsAsString) {
return "Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Найди в нём все наборы числовых данных, которые можно представить в виде простых диаграмм (столбчатых или круговых). Для КАЖДОГО найденного набора данных создай JSON-объект. Верни ТОЛЬКО JSON-массив этих объектов. Структура каждого объекта: {\\\"chartType\\\": \\\"bar\\\" или \\\"pie\\\", \\\"title\\\": \\\"Название диаграммы\\\", \\\"labels\\\": [\\\"Метка 1\\\"], \\\"data\\\": [число1]}. Если подходящих данных нет, верни пустой массив []. Текст для анализа:\n---\n"
+ learningsAsString + "\n---";
}
private String buildSvgPrompt(String json) {
return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. SVG должен быть стильным и читаемым, с подписями на русском языке. Не добавляй никаких комментариев, верни ТОЛЬКО SVG-код. JSON с данными:\n---\n"
+ json + "\n---";
}
}
@@ -19,10 +19,13 @@ import java.util.stream.Collectors;
public class ReportSynthesisService {
private final OllamaAnalyticsService ollamaAnalyticsService;
private final OpenAiChartService openAiChartService;
private final ObjectMapper objectMapper = new ObjectMapper();
public ReportSynthesisService(OllamaAnalyticsService ollamaAnalyticsService) {
public ReportSynthesisService(OllamaAnalyticsService ollamaAnalyticsService,
OpenAiChartService openAiChartService) {
this.ollamaAnalyticsService = ollamaAnalyticsService;
this.openAiChartService = openAiChartService;
}
public Mono<String> synthesizeReport(String originalQuery, List<String> learnings, String lang) {
@@ -40,33 +43,34 @@ public class ReportSynthesisService {
buildTextPrompt(originalQuery, aggregatedLearnings, lang)))
.subscribeOn(Schedulers.boundedElastic());
Mono<List<byte[]>> chartsMono = Mono.fromCallable(() -> {
String jsonArrayString = ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings,
buildChartDataPrompt(aggregatedLearnings));
String cleanJsonArray = extractJsonArray(jsonArrayString);
if (cleanJsonArray == null || cleanJsonArray.isEmpty() || cleanJsonArray.equals("[]")) {
return new ArrayList<byte[]>();
}
List<ChartData> chartDataList = objectMapper.readValue(cleanJsonArray,
new TypeReference<List<ChartData>>() {
});
List<byte[]> 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);
Mono<List<byte[]>> chartsMono = openAiChartService.getChartDataJson(aggregatedLearnings)
.flatMap(jsonArrayString -> {
String cleanJsonArray = extractJsonArray(jsonArrayString);
if (cleanJsonArray == null || cleanJsonArray.isEmpty() || cleanJsonArray.equals("[]")) {
return reactor.core.publisher.Mono.just(new ArrayList<byte[]>());
}
} catch (Exception e) {
System.err.println("Failed to generate a chart: " + e.getMessage());
}
}
return chartImages;
}).subscribeOn(Schedulers.boundedElastic());
try {
List<ChartData> chartDataList = objectMapper.readValue(cleanJsonArray,
new TypeReference<List<ChartData>>() {
});
return reactor.core.publisher.Flux.fromIterable(chartDataList)
.concatMap(chartData -> {
try {
String chartDataJson = objectMapper.writeValueAsString(chartData);
return openAiChartService.getChartSvg(chartDataJson)
.map(this::extractSvgCode)
.map(SvgToPngConverter::convert)
.filter(Objects::nonNull);
} catch (Exception e) {
return reactor.core.publisher.Mono.empty();
}
})
.collectList();
} catch (Exception e) {
return reactor.core.publisher.Mono.just(new ArrayList<byte[]>());
}
})
.subscribeOn(Schedulers.boundedElastic());
return Mono.zip(textMono, chartsMono)
.map(tuple -> new FinalReportPayload(tuple.getT1(), tuple.getT2()));
@@ -53,6 +53,12 @@ logging.level.com.mongodb=WARN
logging.level.kz.konturai.parser.service.OllamaAnalyticsService=INFO
# OpenAI Configuration
openai.api.key=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
openai.api.url=https://api.openai.com/v1/chat/completions
openai.model.name=gpt-4o-mini
openai.timeoutMs=90000
# Email Configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587