From ecc6089040f920159143372c0d1064cf32a50b1e Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Dec 2025 19:43:33 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 166 +++++++++++------- 1 file changed, 100 insertions(+), 66 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index f8516df..6aabd03 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -15,7 +15,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -1671,8 +1670,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName, - 2); + // Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini) + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, null, 2); if (!isValidAiResponse(result)) { logger.warn("genSectionMarket: AI returned invalid or refusal response"); @@ -1695,6 +1694,11 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. " + + "Ты обязан найти объект `audienceAnalysis` в переданном тебе тексте. " + + "Обязательно вставь теги [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]]. " + + "ЗАПРЕЩЕНО писать фразу \"К сожалению, данных нет\" (и любые вариации).\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( @@ -1761,8 +1765,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName, - 2); + // Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini) + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, null, 2); if (!isValidAiResponse(result)) { logger.warn("genSectionAudience: AI returned invalid or refusal response"); @@ -1785,6 +1789,11 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. " + + "Ты обязан найти объект `competitorAnalysis` в переданном тебе тексте. " + + "Обязательно вставь теги [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]]. " + + "ЗАПРЕЩЕНО писать фразу \"К сожалению, данных нет\" (и любые вариации).\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( @@ -1849,8 +1858,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName, - 2); + // Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini) + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, null, 2); if (!isValidAiResponse(result)) { logger.warn("genSectionCompetitors: AI returned invalid or refusal response"); @@ -3523,6 +3532,10 @@ public class MarketingAnalysisService { continue; } + if (!chartsData.containsKey(mapping.dataKey)) { + logger.warn("replaceChartPlaceholders: chartsData missing key '{}' for placeholder {}", + mapping.dataKey, placeholderVariant); + } Object chartData = chartsData.get(mapping.dataKey); if (!isEmptyChartData(chartData)) { try { @@ -3575,6 +3588,10 @@ public class MarketingAnalysisService { e.getMessage()); } } + if (mapping != null && !chartsData.containsKey(mapping.dataKey)) { + logger.warn("replaceChartPlaceholders: chartsData missing key '{}' for remaining placeholder {}", + mapping.dataKey, placeholderVariant); + } logger.warn("replaceChartPlaceholders: Removing leftover placeholder: {}", placeholderVariant); result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant); } @@ -3615,6 +3632,10 @@ public class MarketingAnalysisService { // делаем мягче sectionToPlaceholders.put("VI\\.\\s*SWOT[-\\s]?анализ", List.of("[[CHART_SWOT]]")); + // Умный fallback: если модель НЕ ВСТАВИЛА плейсхолдер в текст секции, + // а данные для него есть — добавляем сам плейсхолдер в КОНЕЦ секции. + // Далее replaceChartPlaceholders() (final pass) заменит плейсхолдер на + // ```json: блок. for (Map.Entry> sectionEntry : sectionToPlaceholders.entrySet()) { String sectionPattern = sectionEntry.getKey(); List placeholders = sectionEntry.getValue(); @@ -3622,87 +3643,100 @@ public class MarketingAnalysisService { continue; } - StringBuilder blocksToInsert = new StringBuilder(); - boolean hasAnyBlock = false; + // 1) Найти старт секции по заголовку (с ## или без) + java.util.regex.Pattern sectionStartPattern = java.util.regex.Pattern.compile( + "(?im)^(?:##+\\s*)?" + sectionPattern + ".*$"); + java.util.regex.Matcher sectionStartMatcher = sectionStartPattern.matcher(result); + if (!sectionStartMatcher.find()) { + logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern); + continue; + } + + int sectionStart = sectionStartMatcher.start(); + int scanFrom = sectionStartMatcher.end(); + + // 2) Найти конец секции (следующий заголовок вида IV./V./VI... или Markdown + // heading) + java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern.compile( + "(?im)^(?:##+\\s*)?[IVX]+\\.\\s+.*$"); + java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(result); + int sectionEnd = result.length(); + if (nextSectionMatcher.find(scanFrom)) { + sectionEnd = nextSectionMatcher.start(); + } + + String sectionText = result.substring(sectionStart, sectionEnd); + StringBuilder tagsToAppend = new StringBuilder(); + boolean willAppendAnything = false; for (String placeholder : placeholders) { - String normalizedPlaceholder = normalizePlaceholderToken(placeholder); - if (placeholder == null || placeholder.isBlank() - || processedPlaceholders.contains(normalizedPlaceholder)) { + if (placeholder == null || placeholder.isBlank()) { continue; } ChartMapping mapping = placeholderMap.get(placeholder); if (mapping == null) { continue; } - Object chartData = chartsData.get(mapping.dataKey); - if (isEmptyChartData(chartData)) { + + // Если плейсхолдер уже есть в тексте секции (даже с пробелами/регистром) — не + // добавляем + if (sectionContainsPlaceholder(sectionText, placeholder)) { continue; } - try { - String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); - String jsonBlock = "\n\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; - blocksToInsert.append(jsonBlock); - hasAnyBlock = true; - processedPlaceholders.add(normalizedPlaceholder); - } catch (JsonProcessingException e) { - logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage()); + + // Если данные отсутствуют — не добавляем тег (он будет удален final pass-ом) + if (chartsData == null || !chartsData.containsKey(mapping.dataKey)) { + logger.warn( + "addMissingChartsToSections: chartsData missing key '{}' for placeholder {} in section {}", + mapping.dataKey, placeholder, sectionPattern); + continue; } + + Object chartData = chartsData.get(mapping.dataKey); + if (isEmptyChartData(chartData)) { + logger.warn( + "addMissingChartsToSections: chartData empty for key '{}' (placeholder {}) in section {}", + mapping.dataKey, placeholder, sectionPattern); + continue; + } + + // Добавляем тег в конец секции. Это решает кейс: «модель забыла вставить + // [[TABLE_COMPARISON]]» + tagsToAppend.append("\n\n").append(placeholder).append("\n"); + willAppendAnything = true; + + // Не считаем как processed (пусть final pass заменит его на JSON-блок) + // но помечаем, чтобы ниже не пытаться добавлять повторно в рамках этой итерации + processedPlaceholders.add(normalizePlaceholderToken(placeholder)); } - if (!hasAnyBlock) { + if (!willAppendAnything) { continue; } - java.util.regex.Pattern pattern = java.util.regex.Pattern - .compile("(##+\\s*" + sectionPattern + "[^#]*)", java.util.regex.Pattern.CASE_INSENSITIVE); - java.util.regex.Matcher matcher = pattern.matcher(result); - boolean found = matcher.find(); - - if (!found) { - pattern = java.util.regex.Pattern - .compile("(^|\\n)\\s*" + sectionPattern + "[^\\n]*(\\n|$)", - java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); - matcher = pattern.matcher(result); - found = matcher.find(); - } - - if (!found) { - logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern); - continue; - } - - int insertPos = matcher.end(); - String remainingText = result.substring(insertPos); - - java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern.compile( - "^(##+\\s*[IVX]+\\.|\\s*[IVX]+\\.\\s+[A-ZА-Я])", - java.util.regex.Pattern.MULTILINE); - java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(remainingText); - - String blocks = blocksToInsert.toString(); - - if (nextSectionMatcher.find()) { - int nextSectionPos = insertPos + nextSectionMatcher.start(); - result = result.substring(0, nextSectionPos) + blocks + result.substring(nextSectionPos); - } else { - java.util.regex.Pattern romanNumeralPattern = java.util.regex.Pattern - .compile("\\n\\s*[IVX]+\\.\\s+[A-ZА-Я]", java.util.regex.Pattern.MULTILINE); - java.util.regex.Matcher romanMatcher = romanNumeralPattern.matcher(remainingText); - if (romanMatcher.find()) { - int nextSectionPos = insertPos + romanMatcher.start(); - result = result.substring(0, nextSectionPos) + blocks + result.substring(nextSectionPos); - } else { - result = result + blocks; - } - } - logger.debug("addMissingChartsToSections: Added fallback blocks to section matching pattern: {}", + result = result.substring(0, sectionEnd) + tagsToAppend + result.substring(sectionEnd); + logger.debug("addMissingChartsToSections: Added missing placeholder tags to section pattern: {}", sectionPattern); } return result; } + private boolean sectionContainsPlaceholder(String sectionText, String canonicalPlaceholder) { + if (sectionText == null || sectionText.isEmpty() || canonicalPlaceholder == null + || canonicalPlaceholder.isBlank()) { + return false; + } + // canonicalPlaceholder like [[CHART_MARKET_SHARE]] + String inner = canonicalPlaceholder.replace("[[", "").replace("]]", "").trim(); + if (inner.isEmpty()) { + return false; + } + String regex = "\\[\\[\\s*" + java.util.regex.Pattern.quote(inner) + "\\s*\\]\\]"; + return java.util.regex.Pattern.compile(regex, java.util.regex.Pattern.CASE_INSENSITIVE).matcher(sectionText) + .find(); + } + public MarketingAnalysisResult getAnalysisResult(String analysisId) { Optional optAnalysis = repository.findById(analysisId); if (optAnalysis.isEmpty()) {