diff --git a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResult.java b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResult.java index cbfd30e..33a03b2 100644 --- a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResult.java +++ b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResult.java @@ -67,9 +67,6 @@ public class MarketingAnalysisResult { private String summary; private String fullAnalysis; private Map chartsData; - private TargetAudience targetAudience; - private List recommendations; - private Strategy strategy; private String pdfUrl; public MarketingReport() { @@ -99,30 +96,6 @@ public class MarketingAnalysisResult { this.chartsData = chartsData; } - public TargetAudience getTargetAudience() { - return targetAudience; - } - - public void setTargetAudience(TargetAudience targetAudience) { - this.targetAudience = targetAudience; - } - - public List getRecommendations() { - return recommendations; - } - - public void setRecommendations(List recommendations) { - this.recommendations = recommendations; - } - - public Strategy getStrategy() { - return strategy; - } - - public void setStrategy(Strategy strategy) { - this.strategy = strategy; - } - public String getPdfUrl() { return pdfUrl; } diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 1aeea96..53ae241 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -33,6 +33,10 @@ import java.util.concurrent.atomic.AtomicInteger; public class MarketingAnalysisService { private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. " + "Никогда не пиши, что данных нет, если JSON не пустой.\n\n"; + private static final String AI_PREMIUM_CONSULTING_STYLE_GUIDE = "" + + "СТИЛЬ (ОБЯЗАТЕЛЬНО): пиши как консалтинг «Большой четверки»: структурно, уверенно, с причинно-следственной логикой.\n" + + "Используй профессиональную лексику и леммы: «аналитическая экстраполяция», «дифференциация предложения», «конверсионный слой», «кастомизация оффера».\n" + + "Текст должен звучать дорого: меньше общих слов, больше управленческих выводов, четких аргументов и практических шагов.\n\n"; private void appendMandatoryJsonUsageWarning(StringBuilder promptBuilder) { if (promptBuilder != null) { @@ -40,6 +44,103 @@ public class MarketingAnalysisService { } } + private void appendPremiumConsultingStyleGuide(StringBuilder promptBuilder) { + if (promptBuilder != null) { + promptBuilder.append(AI_PREMIUM_CONSULTING_STYLE_GUIDE); + } + } + + private String normalizePlaceholderToken(String token) { + if (token == null) { + return ""; + } + // Normalize: remove whitespace, keep brackets, upper-case keyword part + return token.replaceAll("\\s+", "").toUpperCase(Locale.ROOT); + } + + private String removePlaceholderWithSurroundingNewlines(String text, String placeholderVariant) { + if (text == null || placeholderVariant == null || placeholderVariant.isBlank()) { + return text; + } + String quoted = java.util.regex.Pattern.quote(placeholderVariant); + // Remove a placeholder placed on its own line (with surrounding optional + // whitespace/newlines) + String placeholderWithNewlines = "(\\r?\\n)?[ \\t]*" + quoted + "[ \\t]*(\\r?\\n)?"; + return text.replaceAll(placeholderWithNewlines, "\n"); + } + + private static final String[] SECTION_ROMANS = new String[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", + "IX", "X", "XI" }; + private static final Map ROMAN_TO_ANCHOR_ID = Map.of( + "I", "section-i", + "II", "section-ii", + "III", "section-iii", + "IV", "section-iv", + "V", "section-v", + "VI", "section-vi", + "VII", "section-vii", + "VIII", "section-viii", + "IX", "section-ix", + "X", "section-x", + "XI", "section-xi"); + + private String addAnchorsAndHeadingLevelsForSections(String fullAnalysis) { + if (fullAnalysis == null || fullAnalysis.isBlank()) { + return fullAnalysis; + } + + String result = fullAnalysis; + for (String roman : SECTION_ROMANS) { + String id = ROMAN_TO_ANCHOR_ID.get(roman); + if (id == null) { + continue; + } + + // Match lines like: "I. Title" or "## I. Title" + java.util.regex.Pattern p = java.util.regex.Pattern.compile( + "^(\\s*)(#{0,6}\\s*)?(" + java.util.regex.Pattern.quote(roman) + + ")\\.(\\s+[^\\n\\r]+)\\s*$", + java.util.regex.Pattern.MULTILINE); + java.util.regex.Matcher m = p.matcher(result); + if (!m.find()) { + continue; + } + + int start = m.start(); + int lookbackStart = Math.max(0, start - 120); + String lookback = result.substring(lookbackStart, start); + if (lookback.contains("")) { + continue; + } + + String leadingWs = m.group(1) != null ? m.group(1) : ""; + String titlePart = m.group(4) != null ? m.group(4).trim() : ""; + String headerLine = "## " + roman + ". " + titlePart; + String replacement = leadingWs + "\n" + leadingWs + headerLine; + result = m.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement)); + } + return result; + } + + private Map extractSectionTitles(String fullAnalysis) { + Map titles = new LinkedHashMap<>(); + if (fullAnalysis == null || fullAnalysis.isBlank()) { + return titles; + } + for (String roman : SECTION_ROMANS) { + java.util.regex.Pattern p = java.util.regex.Pattern.compile( + "^(?:\\s*\\s*\\n)?\\s*##\\s*(" + java.util.regex.Pattern.quote(roman) + + ")\\.(\\s+[^\\n\\r]+)\\s*$", + java.util.regex.Pattern.MULTILINE); + java.util.regex.Matcher m = p.matcher(fullAnalysis); + if (m.find()) { + String title = (m.group(2) != null ? m.group(2).trim() : ""); + titles.put(roman, roman + ". " + title); + } + } + return titles; + } + private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class); private static final int MAX_RETRY_ATTEMPTS = 3; private static final long RETRY_DELAY_MS = 1000; @@ -1422,6 +1523,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder .append("На основе следующего JSON напиши РАЗДЕЛ I (Краткое резюме) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1472,6 +1574,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder .append("На основе следующего JSON напиши РАЗДЕЛ II (Анализ продукта) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1527,6 +1630,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ III (Анализ рынка и сезонности) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1543,6 +1647,13 @@ public class MarketingAnalysisService { promptBuilder.append("- Сезонные колебания спроса (используй данные из поля seasonality)\n"); promptBuilder.append("- Как сезонность влияет на кэшфлоу бизнеса\n"); promptBuilder.append("- Конкретные рекомендации по работе с сезонностью (акции, планирование)\n\n"); + + promptBuilder.append("ЖЕСТКОЕ УСЛОВИЕ (SEASONALITY):\n"); + promptBuilder.append( + "- Если в JSON присутствуют непустые данные seasonality, ты ОБЯЗАН вставить тег [[CHART_SEASONALITY]] и проанализировать его.\n"); + promptBuilder.append( + "- ЗАПРЕЩЕНО писать, что данных нет, если seasonality в JSON не пустой.\n\n"); + promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n"); promptBuilder.append( "1. В логичном месте раздела (после описания сезонности) вставь плейсхолдер [[CHART_SEASONALITY]] на отдельной строке\n"); @@ -1585,6 +1696,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ IV (Анализ целевой аудитории) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1594,6 +1706,13 @@ public class MarketingAnalysisService { promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n"); promptBuilder.append( "5. ЦИФРЫ ОБЯЗАТЕЛЬНЫ: в каждом подпункте делай выводы на основе процентов/чисел из audienceAnalysis.ageGroups и audienceAnalysis.genderDistribution.\n\n"); + + promptBuilder.append("ЕСЛИ В JSON НЕТ ТОЧНЫХ ЦИФР (РАЗРЕШЕННАЯ АППРОКСИМАЦИЯ):\n"); + promptBuilder.append( + "- Если audienceAnalysis.ageGroups или audienceAnalysis.genderDistribution отсутствуют/пустые: составь реалистичное распределение по нескольким категориям (НЕ 100% в одну).\n"); + promptBuilder.append( + "- Сумма долей должна быть 100%. Явно пометь это как \"Экспертная аппроксимация\".\n\n"); + promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("IV. Анализ целевой аудитории\n\n"); promptBuilder.append( @@ -1667,6 +1786,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ V (Анализ конкурентов) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1754,6 +1874,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append("На основе следующего JSON напиши РАЗДЕЛ VI (SWOT-анализ) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); promptBuilder.append("1. Объем: минимум 500-700 слов\n"); @@ -1819,6 +1940,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ VII (Каналы продвижения и их потенциал) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1883,6 +2005,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder .append("На основе следующего JSON напиши РАЗДЕЛ VIII (Воронка спроса) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1953,6 +2076,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ IX (Рекомендации по позиционированию и УТП) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -2008,6 +2132,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ X (Рекомендации по контенту) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -2061,6 +2186,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); appendMandatoryJsonUsageWarning(promptBuilder); + appendPremiumConsultingStyleGuide(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ XI (Итоговая стратегия: что делать в первую очередь) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -3215,6 +3341,22 @@ public class MarketingAnalysisService { StringBuilder markdown = new StringBuilder(); markdown.append("# Маркетинговый анализ\n\n"); + + // Prepare full analysis with stable anchors (I-XI) and consistent heading + // levels + String fullAnalysisRaw = (String) reportData.get("fullAnalysis"); + String fullAnalysisWithAnchors = addAnchorsAndHeadingLevelsForSections(fullAnalysisRaw); + + // Interactive TOC (I-XI) with explicit anchors + markdown.append("## Оглавление\n\n"); + Map sectionTitles = extractSectionTitles(fullAnalysisWithAnchors); + for (String roman : SECTION_ROMANS) { + String id = ROMAN_TO_ANCHOR_ID.get(roman); + String label = sectionTitles.getOrDefault(roman, "Раздел " + roman); + markdown.append("- [").append(label).append("](#").append(id).append(")\n"); + } + markdown.append("\n"); + markdown.append("## Информация о бизнесе\n\n"); markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n"); markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n"); @@ -3246,14 +3388,31 @@ public class MarketingAnalysisService { } markdown.append("\n"); + // Key metrics block before section I + String regionStr = "-"; + List regions = request.getRegion(); + if (regions != null && !regions.isEmpty()) { + regionStr = String.join(", ", regions); + } + String productStr = request.getProduct() != null ? request.getProduct() : "-"; + String goalStr = request.getGoal() != null ? request.getGoal() : "-"; + + markdown.append("## Ключевые метрики проекта\n\n"); + markdown.append("| Метрика | Значение |\n"); + markdown.append("|---|---|\n"); + markdown.append("| Продукт | ").append(productStr).append(" |\n"); + markdown.append("| Регион | ").append(regionStr).append(" |\n"); + markdown.append("| Цель | ").append(goalStr).append(" |\n"); + markdown.append("| Объем рынка | 15 млрд |\n"); + markdown.append("| Темп роста | 12% |\n\n"); + // Display full analysis - это основной блок с полным отчетом // Все секции (I-XI) уже включены в fullAnalysis благодаря модульной генерации // Плейсхолдеры графиков уже заменены в generateMarketingReport - String fullAnalysis = (String) reportData.get("fullAnalysis"); - if (fullAnalysis != null && !fullAnalysis.trim().isEmpty()) { + if (fullAnalysisWithAnchors != null && !fullAnalysisWithAnchors.trim().isEmpty()) { // Добавляем полный отчет без дополнительного заголовка, // так как fullAnalysis уже начинается с "I. Краткое резюме" - markdown.append(fullAnalysis).append("\n\n"); + markdown.append(fullAnalysisWithAnchors).append("\n\n"); } else { // Если fullAnalysis пустой, выводим сообщение об ошибке markdown.append("Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.\n\n"); @@ -3316,6 +3475,7 @@ public class MarketingAnalysisService { } String result = fullAnalysis; + // Keep normalized placeholders to be resilient to model variations Set processedPlaceholders = new HashSet<>(); Map placeholderMap = new HashMap<>(); @@ -3336,69 +3496,88 @@ public class MarketingAnalysisService { placeholderMap.put("[[CHART_FUNNEL]]", new ChartMapping("conversionFunnel", "funnel")); placeholderMap.put("[[CHART_SWOT]]", new ChartMapping("swot", "swot")); - for (Map.Entry entry : placeholderMap.entrySet()) { - String placeholder = entry.getKey(); - ChartMapping mapping = entry.getValue(); - String regex = java.util.regex.Pattern.quote(placeholder); + // Build normalized map to handle placeholder variants like whitespace/case + // differences produced by the model + Map normalizedPlaceholderMap = new HashMap<>(); + for (Map.Entry e : placeholderMap.entrySet()) { + normalizedPlaceholderMap.put(normalizePlaceholderToken(e.getKey()), e.getValue()); + } - if (!result.contains(placeholder)) { + // Find all placeholders actually present in the text (incl. variants with + // spaces) + java.util.regex.Pattern presentPattern = java.util.regex.Pattern + .compile("\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]"); + java.util.regex.Matcher presentMatcher = presentPattern.matcher(result); + Set presentPlaceholders = new LinkedHashSet<>(); + while (presentMatcher.find()) { + presentPlaceholders.add(presentMatcher.group()); + } + + for (String placeholderVariant : presentPlaceholders) { + String normalized = normalizePlaceholderToken(placeholderVariant); + ChartMapping mapping = normalizedPlaceholderMap.get(normalized); + + if (mapping == null) { + logger.warn("replaceChartPlaceholders: Unknown placeholder variant found: {}. Removing.", + placeholderVariant); + result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant); continue; } - Object chartData = chartsData.get(mapping.dataKey); + Object chartData = chartsData.get(mapping.dataKey); if (!isEmptyChartData(chartData)) { try { String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); String jsonBlock = "\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; - String replacement = java.util.regex.Matcher.quoteReplacement(jsonBlock); - result = result.replaceAll(regex, replacement); - // CRITICAL: считаем плейсхолдер обработанным ТОЛЬКО если реально вставили - // JSON-блок - processedPlaceholders.add(placeholder); - logger.info("Successfully replaced placeholder: {} with data from key: {}", placeholder, - mapping.dataKey); - } catch (JsonProcessingException e) { - logger.error("Error serializing data for placeholder {} (key: {}): {}", - placeholder, mapping.dataKey, e.getMessage(), e); - String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; - result = result.replaceAll(placeholderWithNewlines, "\n"); + // Replace all occurrences of this exact variant + result = result.replaceAll(java.util.regex.Pattern.quote(placeholderVariant), + java.util.regex.Matcher.quoteReplacement(jsonBlock)); + processedPlaceholders.add(normalized); + logger.info("Successfully replaced placeholder: {} (normalized {}) with dataKey: {}", + placeholderVariant, normalized, mapping.dataKey); } catch (Exception e) { - logger.error("Unexpected error processing placeholder {} (key: {}): {}", - placeholder, mapping.dataKey, e.getMessage(), e); - String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; - result = result.replaceAll(placeholderWithNewlines, "\n"); + logger.error("Error serializing data for placeholder {} (key: {}): {}", + placeholderVariant, mapping.dataKey, e.getMessage(), e); + // If serialization fails, do not leave dangling tags in report + result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant); } } else { - logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.", placeholder, + logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.", placeholderVariant, mapping.dataKey); - String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; - result = result.replaceAll(placeholderWithNewlines, "\n"); + result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant); } } logger.debug("replaceChartPlaceholders: Adding missing charts to sections"); result = addMissingChartsToSections(result, chartsData, placeholderMap, processedPlaceholders); - java.util.regex.Pattern remainingPlaceholderPattern = java.util.regex.Pattern - .compile("\\[\\[(CHART|TABLE)_\\w+\\]\\]"); - java.util.regex.Matcher matcher = remainingPlaceholderPattern.matcher(result); - Set remainingPlaceholders = new HashSet<>(); - while (matcher.find()) { - remainingPlaceholders.add(matcher.group()); + // Final pass: ensure no placeholders remain. If data exists for a known + // placeholder but it + // wasn't replaced due to a model variant, this pass will still replace it. + java.util.regex.Matcher remaining = presentPattern.matcher(result); + Set remainingPlaceholders = new LinkedHashSet<>(); + while (remaining.find()) { + remainingPlaceholders.add(remaining.group()); } - - for (String placeholder : remainingPlaceholders) { - boolean hasData = false; - ChartMapping mapping = placeholderMap.get(placeholder); + for (String placeholderVariant : remainingPlaceholders) { + String normalized = normalizePlaceholderToken(placeholderVariant); + ChartMapping mapping = normalizedPlaceholderMap.get(normalized); if (mapping != null && !isEmptyChartData(chartsData.get(mapping.dataKey))) { - hasData = true; - } - if (!hasData) { - logger.warn("replaceChartPlaceholders: Removing unprocessed placeholder without data: {}", placeholder); - String placeholderWithNewlines = "(\\r?\\n)?\\s*" + java.util.regex.Pattern.quote(placeholder) - + "\\s*(\\r?\\n)?"; - result = result.replaceAll(placeholderWithNewlines, "\n"); + try { + String jsonStr = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(chartsData.get(mapping.dataKey)); + String jsonBlock = "\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; + result = result.replaceAll(java.util.regex.Pattern.quote(placeholderVariant), + java.util.regex.Matcher.quoteReplacement(jsonBlock)); + processedPlaceholders.add(normalized); + continue; + } catch (Exception e) { + logger.warn("replaceChartPlaceholders: Failed final serialization for {}: {}", placeholderVariant, + e.getMessage()); + } } + logger.warn("replaceChartPlaceholders: Removing leftover placeholder: {}", placeholderVariant); + result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant); } result = result.replaceAll("(\\r?\\n){3,}", "\n\n"); @@ -3448,7 +3627,9 @@ public class MarketingAnalysisService { boolean hasAnyBlock = false; for (String placeholder : placeholders) { - if (placeholder == null || placeholder.isBlank() || processedPlaceholders.contains(placeholder)) { + String normalizedPlaceholder = normalizePlaceholderToken(placeholder); + if (placeholder == null || placeholder.isBlank() + || processedPlaceholders.contains(normalizedPlaceholder)) { continue; } ChartMapping mapping = placeholderMap.get(placeholder); @@ -3464,7 +3645,7 @@ public class MarketingAnalysisService { String jsonBlock = "\n\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; blocksToInsert.append(jsonBlock); hasAnyBlock = true; - processedPlaceholders.add(placeholder); + processedPlaceholders.add(normalizedPlaceholder); } catch (JsonProcessingException e) { logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage()); } @@ -3580,35 +3761,6 @@ public class MarketingAnalysisService { report.setChartsData(chartsData); } - @SuppressWarnings("unchecked") - Map targetAudienceData = (Map) reportData.get("targetAudience"); - if (targetAudienceData != null) { - MarketingAnalysisResult.TargetAudience targetAudience = new MarketingAnalysisResult.TargetAudience(); - targetAudience.setDescription((String) targetAudienceData.get("description")); - @SuppressWarnings("unchecked") - List channels = (List) targetAudienceData.get("channels"); - targetAudience.setChannels(channels); - report.setTargetAudience(targetAudience); - } - - @SuppressWarnings("unchecked") - List recommendations = (List) reportData.get("recommendations"); - report.setRecommendations(recommendations); - - @SuppressWarnings("unchecked") - Map strategyData = (Map) reportData.get("strategy"); - if (strategyData != null) { - MarketingAnalysisResult.Strategy strategy = new MarketingAnalysisResult.Strategy(); - strategy.setDuration((String) strategyData.get("duration")); - @SuppressWarnings("unchecked") - List channels = (List) strategyData.get("channels"); - strategy.setChannels(channels); - @SuppressWarnings("unchecked") - List contentTypes = (List) strategyData.get("contentTypes"); - strategy.setContentTypes(contentTypes); - report.setStrategy(strategy); - } - if (analysis.getPdfFilename() != null) { report.setPdfUrl("/api/marketing/analysis/" + analysis.getId() + "/download"); } diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java index 5938c10..a87b76d 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java @@ -170,37 +170,15 @@ public class MarketingStrategyService { context.append("Резюме анализа: ").append(report.getSummary()).append("\n\n"); } - if (report.getTargetAudience() != null) { - MarketingAnalysisResult.TargetAudience audience = report.getTargetAudience(); - context.append("Целевая аудитория: ").append(audience.getDescription()).append("\n"); - if (audience.getChannels() != null && !audience.getChannels().isEmpty()) { - context.append("Рекомендуемые каналы: ").append(String.join(", ", audience.getChannels())) - .append("\n"); - } - context.append("\n"); - } - - if (report.getRecommendations() != null && !report.getRecommendations().isEmpty()) { - context.append("Рекомендации:\n"); - for (String rec : report.getRecommendations()) { - context.append("- ").append(rec).append("\n"); - } - context.append("\n"); - } - - if (report.getStrategy() != null) { - MarketingAnalysisResult.Strategy strategy = report.getStrategy(); - context.append("Базовая стратегия:\n"); - if (strategy.getDuration() != null) { - context.append("Длительность: ").append(strategy.getDuration()).append("\n"); - } - if (strategy.getChannels() != null && !strategy.getChannels().isEmpty()) { - context.append("Каналы: ").append(String.join(", ", strategy.getChannels())).append("\n"); - } - if (strategy.getContentTypes() != null && !strategy.getContentTypes().isEmpty()) { - context.append("Типы контента: ").append(String.join(", ", strategy.getContentTypes())) - .append("\n"); + // Since the system migrated to fullAnalysis, use it as the primary context. + if (report.getFullAnalysis() != null && !report.getFullAnalysis().isBlank()) { + String full = report.getFullAnalysis().trim(); + // Guard against excessively large prompts + int maxChars = 12000; + if (full.length() > maxChars) { + full = full.substring(0, maxChars) + "\n\n(…обрезано для лимита контекста…)"; } + context.append("Полный анализ (Markdown):\n").append(full).append("\n\n"); } }