diff --git a/.DS_Store b/.DS_Store index 547a843..dad7ad8 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java b/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java index 9a6782a..a4519fd 100644 --- a/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java +++ b/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java @@ -50,19 +50,28 @@ public class OllamaAnalyticsService { } public MarketItem.Analytics analyzeText(String text) { + return analyzeText(text, "ru"); + } + + public MarketItem.Analytics analyzeText(String text, String language) { MarketItem.Analytics analytics = new MarketItem.Analytics(); + boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language); try { - String summary = generate(text, - "Напиши краткое саммари следующей новостной статьи на русском языке. Ответ должен содержать только саммари из 3-4 предложений, без лишних вступлений. Статья: "); + String summaryPrompt = isRussian + ? "Напиши краткое саммари следующей новостной статьи на русском языке. Ответ должен содержать только саммари из 3-4 предложений, без лишних вступлений. Статья: " + : "Write a brief summary of the following news article in English. The response should contain only a summary of 3-4 sentences, without any unnecessary introductions. Article: "; + String summary = generate(text, summaryPrompt); analytics.setSummary(summary); } catch (Exception e) { logger.warn("Failed to get summary from Ollama: {}", e.getMessage()); } try { - String tagsCsv = generate(text, - "Извлеки 5-7 ключевых слов или тегов из текста новостной статьи. В ответе дай только список тегов через запятую, без нумерации и заголовков. Статья: "); + String tagsPrompt = isRussian + ? "Извлеки 5-7 ключевых слов или тегов из текста новостной статьи. В ответе дай только список тегов через запятую, без нумерации и заголовков. Статья: " + : "Extract 5-7 key words or tags from the news article text. In your response, provide only a list of tags separated by commas, without numbering or headers. Article: "; + String tagsCsv = generate(text, tagsPrompt); String[] tags = tagsCsv == null ? new String[] {} : tagsCsv.replace("\n", " ").split("\\s*,\\s*"); analytics.setTags(tags); } catch (Exception e) { @@ -70,8 +79,10 @@ public class OllamaAnalyticsService { } try { - String sentiment = generate(text, - "Определи тональность текста новостной статьи. В ответе дай только одно слово латиницей: positive, negative или neutral. Статья: "); + String sentimentPrompt = isRussian + ? "Определи тональность текста новостной статьи. В ответе дай только одно слово латиницей: positive, negative или neutral. Статья: " + : "Determine the sentiment of the news article text. In your response, provide only one word in English: positive, negative, or neutral. Article: "; + String sentiment = generate(text, sentimentPrompt); if (sentiment != null) { sentiment = sentiment.trim().toLowerCase(); } @@ -81,8 +92,10 @@ public class OllamaAnalyticsService { } try { - String entitiesJson = generate(text, - "Извлеки из текста имена людей, названия компаний и географические локации. В ответе дай только JSON объект следующей структуры: {\\\"persons\\\": [], \\\"companies\\\": [], \\\"locations\\\": []}. Статья: "); + String entitiesPrompt = isRussian + ? "Извлеки из текста имена людей, названия компаний и географические локации. В ответе дай только JSON объект следующей структуры: {\\\"persons\\\": [], \\\"companies\\\": [], \\\"locations\\\": []}. Статья: " + : "Extract from the text names of people, company names, and geographic locations. In your response, provide only a JSON object with the following structure: {\\\"persons\\\": [], \\\"companies\\\": [], \\\"locations\\\": []}. Article: "; + String entitiesJson = generate(text, entitiesPrompt); Map entities = new HashMap<>(); if (entitiesJson != null && entitiesJson.trim().startsWith("{")) { try { @@ -136,6 +149,10 @@ public class OllamaAnalyticsService { } public String generateWithInstruction(String text, String instruction) { + return generateWithInstruction(text, instruction, "ru"); + } + + public String generateWithInstruction(String text, String instruction, String language) { String prompt = instruction + "\n\n" + text; Map requestBody = new HashMap<>(); requestBody.put("model", modelName); diff --git a/src/main/java/kz/konturai/parser/service/OpenAiChartService.java b/src/main/java/kz/konturai/parser/service/OpenAiChartService.java index e67ed2a..4f3cb8c 100644 --- a/src/main/java/kz/konturai/parser/service/OpenAiChartService.java +++ b/src/main/java/kz/konturai/parser/service/OpenAiChartService.java @@ -48,17 +48,25 @@ public class OpenAiChartService { } public Mono getChartDataJson(String aggregatedLearnings) { + return getChartDataJson(aggregatedLearnings, "ru"); + } + + public Mono getChartDataJson(String aggregatedLearnings, String language) { // 2. Логируем начало важной операции logger.info("Requesting JSON data for charts from OpenAI..."); - String prompt = buildChartDataPrompt(aggregatedLearnings); + String prompt = buildChartDataPrompt(aggregatedLearnings, language); return callChatCompletions("Chart Data Extraction", prompt); } public Mono getChartSvg(String jsonData) { + return getChartSvg(jsonData, "ru"); + } + + public Mono getChartSvg(String jsonData, String language) { // 3. Логируем параметры, с которыми вызывается метод logger.info("Requesting SVG code for chart from OpenAI..."); logger.info("SVG generation request based on JSON: {}", jsonData); // DEBUG, чтобы не засорять логи - String prompt = buildSvgPrompt(jsonData); + String prompt = buildSvgPrompt(jsonData, language); return callChatCompletions("SVG Generation", prompt); } @@ -127,86 +135,78 @@ public class OpenAiChartService { // прерывать цепочку вызовов } - private String buildChartDataPromptRussian(String learningsAsString) { - return "Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Найди в нём все наборы числовых данных, которые можно представить в виде простых диаграмм (столбчатых или круговых). Для КАЖДОГО найденного набора данных создай JSON-объект. Верни ТОЛЬКО JSON-массив этих объектов. Структура каждого объекта: {\\\"chartType\\\": \\\"bar\\\" или \\\"pie\\\", \\\"title\\\": \\\"Название диаграммы\\\", \\\"labels\\\": [\\\"Метка 1\\\"], \\\"data\\\": [число1]}. Если подходящих данных нет, верни пустой массив []. Текст для анализа:\n---\n" - + learningsAsString + "\n---"; + private String buildChartDataPrompt(String learningsAsString, String language) { + boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language); + + if (isRussian) { + return "Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Найди в нём все наборы числовых данных, которые можно представить в виде простых диаграмм (столбчатых или круговых). Для КАЖДОГО найденного набора данных создай JSON-объект. Верни ТОЛЬКО JSON-массив этих объектов. Структура каждого объекта: {\\\"chartType\\\": \\\"bar\\\" или \\\"pie\\\", \\\"title\\\": \\\"Название диаграммы\\\", \\\"labels\\\": [\\\"Метка 1\\\"], \\\"data\\\": [число1]}. Если подходящих данных нет, верни пустой массив []. Текст для анализа:\n---\n" + + learningsAsString + "\n---"; + } else { + return "You are an AI data analyst. Carefully analyze the following text. Find all sets of numerical data suitable for simple bar or pie charts. For EACH data set you find, create a JSON object." + + + " Return ONLY a JSON array of these objects." + + + " The structure of each object MUST STRICTLY be: " + + "{\"chartType\": \"...\", \"title\": \"Chart Title\", \"labels\": [\"Label 1\"], \"data\": [number1]}." + + + + "\n\nCRITICAL RULES:\n" + + "1. For 'chartType', use 'pie' if the data represents parts of a whole (like percentages or market shares). In all other cases, use 'bar'.\n" + + + "2. The 'data' array MUST contain ONLY NUMBERS. Do not include text, units, or placeholders. Convert all values to simple numbers.\n" + + + "3. If a numerical value is not available for a label, you MUST use the number 0 as a placeholder.\n" + + + "4. DO NOT omit any keys.\n\n" + + + "Example of a correct response:\n" + + "[{\"chartType\":\"pie\",\"title\":\"Market Share\",\"labels\":[\"Company A\",\"Company B\"],\"data\":[60,40]}]" + + + "\n\n" + + "If no suitable data is found, return an empty array [].\n\n" + + "Text to analyze:\n---\n" + + learningsAsString + "\n---"; + } } - // private String buildChartDataPrompt(String learningsAsString) { - // return "You are an AI data analyst. Carefully analyze the following text. - // Find all sets of numerical data suitable for simple bar or pie charts. For - // EACH data set you find, create a JSON object." - // + - // " Return ONLY a JSON array of these objects. The structure of each object - // MUST STRICTLY be: " + - // "{\"chartType\": \"bar\" or \"pie\", \"title\": \"Chart Title\", \"labels\": - // [\"Label 1\"], \"data\": [number1]}. " - // + - // "DO NOT omit any keys." + - // "\n\n" + - // "Example of a correct response:\n" + - // "[{\"chartType\":\"bar\",\"title\":\"Growth by - // Year\",\"labels\":[\"2023\",\"2024\"],\"data\":[100,150]}]" - // + - // "\n\n" + - // "If no suitable data is found, return an empty array [].\n\n" + - // "Text to analyze:\n---\n" + - // learningsAsString + "\n---"; - // } - private String buildChartDataPrompt(String learningsAsString) { - return "You are an AI data analyst robot. Carefully analyze the following text. Find all sets of numerical data suitable for simple diagrams. For EACH data set you find, create a JSON object." - + - " Return ONLY a JSON array of these objects." + + private String buildChartDataPromptRussian(String learningsAsString) { + return buildChartDataPrompt(learningsAsString, "ru"); + } - " The structure of each object MUST STRICTLY be: " + - "{\"chartType\": \"...\", \"title\": \"Chart Title\", \"labels\": [\"Label 1\"], \"data\": [number1]}." - + + private String buildSvgPrompt(String json, String language) { + boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language); - "\n\nCRITICAL RULES:\n" + - "1. For 'chartType', use 'pie' if the data represents parts of a whole (like percentages or market shares). In all other cases, use 'bar'.\n" - + - "2. The 'data' array MUST contain ONLY NUMBERS. Do not include text, units, or placeholders. Convert all values to simple numbers.\n" - + - "3. If a numerical value is not available for a label, you MUST use the number 0 as a placeholder.\n" + - "4. DO NOT omit any keys.\n\n" + + if (isRussian) { + return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. " + + + "SVG должен быть стильным и читаемым, с подписями на русском языке. \n" + + "ВАЖНЫЕ ПРАВИЛА:\n" + + "1. Все значения атрибутов width, height, x, y, r должны быть ПОЛОЖИТЕЛЬНЫМИ числами (>= 0).\n" + + "2. Не добавляй никаких комментариев или объяснений.\n" + + "3. Верни ТОЛЬКО SVG-код, начиная с тега и заканчивая .\n\n" + + "JSON с данными:\n---\n" + + json + "\n---"; + } else { + return "You are a data visualization expert that generates clean, valid SVG code.\n" + + "Based on the following JSON, generate a complete and valid SVG code for a bar chart.\n\n" + - "Example of a correct response:\n" + - "[{\"chartType\":\"pie\",\"title\":\"Market Share\",\"labels\":[\"Company A\",\"Company B\"],\"data\":[60,40]}]" - + - "\n\n" + - "If no suitable data is found, return an empty array [].\n\n" + - "Text to analyze:\n---\n" + - learningsAsString + "\n---"; + "IMPORTANT RULES:\n" + + "1. The SVG coordinate system starts with (0,0) in the TOP-LEFT corner. The Y-axis points DOWN.\n" + + "2. Therefore, to draw a bar upwards from the bottom, the 'y' attribute of the must be calculated as (chart_height - bar_height), and the 'height' attribute must ALWAYS be a POSITIVE number.\n" + + + "3. All dimensional attributes (width, height, x, y, r) MUST be POSITIVE numbers (>= 0).\n" + + "4. The chart must be visually appealing, with appropriate padding, colors, and English labels.\n" + + "5. Double-check your final SVG code to ensure no height or width attributes are negative.\n" + + "6. Return ONLY the raw SVG code, starting with and ending with . Do not include any explanations or markdown.\n\n" + + + + "JSON data:\n---\n" + + json + "\n---"; + } } private String buildSvgPromptRussian(String json) { - return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. " - + - "SVG должен быть стильным и читаемым, с подписями на русском языке. \n" + - "ВАЖНЫЕ ПРАВИЛА:\n" + - "1. Все значения атрибутов width, height, x, y, r должны быть ПОЛОЖИТЕЛЬНЫМИ числами (>= 0).\n" + - "2. Не добавляй никаких комментариев или объяснений.\n" + - "3. Верни ТОЛЬКО SVG-код, начиная с тега и заканчивая .\n\n" + - "JSON с данными:\n---\n" + - json + "\n---"; - } - - private String buildSvgPrompt(String json) { - return "You are a data visualization expert that generates clean, valid SVG code.\n" + - "Based on the following JSON, generate a complete and valid SVG code for a bar chart.\n\n" + - - "IMPORTANT RULES:\n" + - "1. The SVG coordinate system starts with (0,0) in the TOP-LEFT corner. The Y-axis points DOWN.\n" + - "2. Therefore, to draw a bar upwards from the bottom, the 'y' attribute of the must be calculated as (chart_height - bar_height), and the 'height' attribute must ALWAYS be a POSITIVE number.\n" - + - "3. All dimensional attributes (width, height, x, y, r) MUST be POSITIVE numbers (>= 0).\n" + - "4. The chart must be visually appealing, with appropriate padding, colors, and Russian labels.\n" + - "5. Double-check your final SVG code to ensure no height or width attributes are negative.\n" + - "6. Return ONLY the raw SVG code, starting with and ending with . Do not include any explanations or markdown.\n\n" - + - - "JSON data:\n---\n" + - json + "\n---"; + return buildSvgPrompt(json, "ru"); } } \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java b/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java index 909a9c0..87fe294 100644 --- a/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java +++ b/src/main/java/kz/konturai/parser/service/ReportSynthesisService.java @@ -31,7 +31,8 @@ public class ReportSynthesisService { public Mono synthesizeReport(String originalQuery, List learnings, String lang) { String aggregatedLearnings = formatLearnings(learnings); String prompt = buildTextPrompt(originalQuery, aggregatedLearnings, lang); - return Mono.fromCallable(() -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, prompt)); + return Mono + .fromCallable(() -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, prompt, lang)); } public Mono synthesizeReportAndCharts(String originalQuery, List learnings, @@ -40,10 +41,10 @@ public class ReportSynthesisService { Mono textMono = Mono.fromCallable( () -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, - buildTextPrompt(originalQuery, aggregatedLearnings, lang))) + buildTextPrompt(originalQuery, aggregatedLearnings, lang), lang)) .subscribeOn(Schedulers.boundedElastic()); - Mono> chartsMono = openAiChartService.getChartDataJson(aggregatedLearnings) + Mono> chartsMono = openAiChartService.getChartDataJson(aggregatedLearnings, lang) .flatMap(jsonArrayString -> { String cleanJsonArray = extractJsonArray(jsonArrayString); if (cleanJsonArray == null || cleanJsonArray.isEmpty() || cleanJsonArray.equals("[]")) { @@ -57,7 +58,7 @@ public class ReportSynthesisService { .concatMap(chartData -> { try { String chartDataJson = objectMapper.writeValueAsString(chartData); - return openAiChartService.getChartSvg(chartDataJson) + return openAiChartService.getChartSvg(chartDataJson, lang) .map(this::extractSvgCode) .map(SvgToPngConverter::convert) .filter(Objects::nonNull); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 03887b2..533264f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -54,7 +54,7 @@ logging.level.com.mongodb=WARN logging.level.kz.konturai.parser.service.OllamaAnalyticsService=INFO # OpenAI Configuration -openai.api.key=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 +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