From a237e5fdbc162b5180c74426cfb63a619e127960 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Dec 2025 19:03:24 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 145 +++++++++++++++++- 1 file changed, 141 insertions(+), 4 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index e055ffb..1aeea96 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -31,6 +31,14 @@ import java.util.concurrent.atomic.AtomicInteger; @Service public class MarketingAnalysisService { + private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. " + + "Никогда не пиши, что данных нет, если JSON не пустой.\n\n"; + + private void appendMandatoryJsonUsageWarning(StringBuilder promptBuilder) { + if (promptBuilder != null) { + promptBuilder.append(AI_MANDATORY_JSON_USAGE_WARNING); + } + } private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class); private static final int MAX_RETRY_ATTEMPTS = 3; @@ -1209,6 +1217,7 @@ public class MarketingAnalysisService { } break; case "market": + // ОБЯЗАТЕЛЬНО: seasonality + market должны доходить до секции рынка if (fullJson.containsKey("seasonality")) { context.put("seasonality", fullJson.get("seasonality")); } @@ -1220,6 +1229,7 @@ public class MarketingAnalysisService { // Новая структура: audienceAnalysis (ageGroups, genderDistribution, segments, // keyTakeaways, etc.) if (fullJson.containsKey("audienceAnalysis")) { + // ОБЯЗАТЕЛЬНО: audienceAnalysis должен доходить до секции аудитории context.put("audienceAnalysis", fullJson.get("audienceAnalysis")); } else if (fullJson.containsKey("audienceSegments")) { // Backward compatibility (старая структура) @@ -1231,6 +1241,7 @@ public class MarketingAnalysisService { // competitorsDetails, swotCompetitors, etc.) if (fullJson.containsKey("competitorAnalysis")) { Object competitorAnalysisObj = fullJson.get("competitorAnalysis"); + // ОБЯЗАТЕЛЬНО: competitorAnalysis должен доходить до секции конкурентов context.put("competitorAnalysis", competitorAnalysisObj); // Aliases для совместимости: плоские ключи для секции конкурентов // (данные физически лежат в competitorAnalysis.*) @@ -1410,6 +1421,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder .append("На основе следующего JSON напиши РАЗДЕЛ I (Краткое резюме) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1459,6 +1471,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder .append("На основе следующего JSON напиши РАЗДЕЛ II (Анализ продукта) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1513,6 +1526,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ III (Анализ рынка и сезонности) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1570,6 +1584,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ IV (Анализ целевой аудитории) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1651,6 +1666,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ V (Анализ конкурентов) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1737,6 +1753,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append("На основе следующего JSON напиши РАЗДЕЛ VI (SWOT-анализ) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); promptBuilder.append("1. Объем: минимум 500-700 слов\n"); @@ -1801,6 +1818,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ VII (Каналы продвижения и их потенциал) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1864,6 +1882,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder .append("На основе следующего JSON напиши РАЗДЕЛ VIII (Воронка спроса) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1933,6 +1952,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ IX (Рекомендации по позиционированию и УТП) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -1987,6 +2007,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ X (Рекомендации по контенту) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -2039,6 +2060,7 @@ public class MarketingAnalysisService { String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); + appendMandatoryJsonUsageWarning(promptBuilder); promptBuilder.append( "На основе следующего JSON напиши РАЗДЕЛ XI (Итоговая стратегия: что делать в первую очередь) маркетингового отчета.\n\n"); promptBuilder.append("ТРЕБОВАНИЯ:\n"); @@ -2255,6 +2277,13 @@ public class MarketingAnalysisService { @SuppressWarnings("unchecked") Map ageGroupsMap = (Map) audienceAnalysis.get("ageGroups"); if (ageGroupsMap != null) { + // CRITICAL: отдельный ключ для графика возраста (для + // [[CHART_AGE_DISTRIBUTION]]) + // Дальше он будет преобразован в Chart.js формат в transformToChartConfig() + if (!ageGroupsMap.isEmpty()) { + chartsData.put("audienceAge", ageGroupsMap); + } + List> ageGroupsList = new ArrayList<>(); for (Map.Entry entry : ageGroupsMap.entrySet()) { Map ageGroup = new HashMap<>(); @@ -2270,6 +2299,13 @@ public class MarketingAnalysisService { Map genderDistributionMap = (Map) audienceAnalysis .get("genderDistribution"); if (genderDistributionMap != null) { + // CRITICAL: отдельный ключ для графика гендера (для + // [[CHART_GENDER_DISTRIBUTION]]) + // Дальше он будет преобразован в Chart.js формат в transformToChartConfig() + if (!genderDistributionMap.isEmpty()) { + chartsData.put("audienceGender", genderDistributionMap); + } + List> gendersList = new ArrayList<>(); for (Map.Entry entry : genderDistributionMap.entrySet()) { Map genderData = new HashMap<>(); @@ -2607,6 +2643,73 @@ public class MarketingAnalysisService { } } + // 3b. Audience Age (CRITICAL): прямой источник из audienceAnalysis.ageGroups + // Формат rawData.audienceAge: Map ("18-24"->число, ...) + if (rawData.containsKey("audienceAge") && !formattedCharts.containsKey("audienceAge")) { + Object ageObj = rawData.get("audienceAge"); + if (ageObj instanceof Map) { + @SuppressWarnings("unchecked") + Map ageMap = (Map) ageObj; + if (ageMap != null && !ageMap.isEmpty()) { + List labels = new ArrayList<>(ageMap.keySet()); + List data = new ArrayList<>(); + for (String k : labels) { + Object v = ageMap.get(k); + if (v instanceof Number) { + data.add(((Number) v).intValue()); + } else { + try { + data.add(Integer.parseInt(String.valueOf(v))); + } catch (Exception e) { + data.add(0); + } + } + } + Map dataset = new HashMap<>(); + dataset.put("label", "Возрастные группы"); + dataset.put("data", data); + Map chart = new HashMap<>(); + chart.put("labels", labels); + chart.put("datasets", List.of(dataset)); + formattedCharts.put("audienceAge", chart); + } + } + } + + // 3c. Audience Gender (CRITICAL): прямой источник из + // audienceAnalysis.genderDistribution + // Формат rawData.audienceGender: Map ("Женщины"->число, ...) + if (rawData.containsKey("audienceGender") && !formattedCharts.containsKey("audienceGender")) { + Object genderObj = rawData.get("audienceGender"); + if (genderObj instanceof Map) { + @SuppressWarnings("unchecked") + Map genderMap = (Map) genderObj; + if (genderMap != null && !genderMap.isEmpty()) { + List labels = new ArrayList<>(genderMap.keySet()); + List data = new ArrayList<>(); + for (String k : labels) { + Object v = genderMap.get(k); + if (v instanceof Number) { + data.add(((Number) v).intValue()); + } else { + try { + data.add(Integer.parseInt(String.valueOf(v))); + } catch (Exception e) { + data.add(0); + } + } + } + Map dataset = new HashMap<>(); + dataset.put("label", "Гендерное распределение"); + dataset.put("data", data); + Map chart = new HashMap<>(); + chart.put("labels", labels); + chart.put("datasets", List.of(dataset)); + formattedCharts.put("audienceGender", chart); + } + } + } + // 4. Conversion Funnel (List) if (rawData.containsKey("conversionFunnel")) { @SuppressWarnings("unchecked") @@ -2679,7 +2782,36 @@ public class MarketingAnalysisService { formattedCharts.put("comparisonTable", rawData.get("comparisonTable")); } if (rawData.containsKey("marketShareChart")) { - formattedCharts.put("marketShareChart", rawData.get("marketShareChart")); + // CRITICAL: marketShareChart должен быть Chart.js config для вставки в + // fullAnalysis + // raw формат: List<{name,value}>. Преобразуем в {labels,datasets} + @SuppressWarnings("unchecked") + List> marketShare = (List>) rawData.get("marketShareChart"); + if (marketShare != null && !marketShare.isEmpty()) { + List labels = new ArrayList<>(); + List data = new ArrayList<>(); + for (Map item : marketShare) { + labels.add(String.valueOf(item.getOrDefault("name", "Unknown"))); + Object v = item.get("value"); + if (v instanceof Number) { + data.add(((Number) v).intValue()); + } else { + try { + data.add(Integer.parseInt(String.valueOf(v))); + } catch (Exception e) { + data.add(0); + } + } + } + Map dataset = new HashMap<>(); + dataset.put("label", "Доли рынка (%)"); + dataset.put("data", data); + + Map chart = new HashMap<>(); + chart.put("labels", labels); + chart.put("datasets", List.of(dataset)); + formattedCharts.put("marketShareChart", chart); + } } if (rawData.containsKey("swotCompetitors")) { formattedCharts.put("swotCompetitors", rawData.get("swotCompetitors")); @@ -3212,8 +3344,6 @@ public class MarketingAnalysisService { if (!result.contains(placeholder)) { continue; } - - processedPlaceholders.add(placeholder); Object chartData = chartsData.get(mapping.dataKey); if (!isEmptyChartData(chartData)) { @@ -3222,6 +3352,9 @@ public class MarketingAnalysisService { 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) { @@ -3291,6 +3424,8 @@ public class MarketingAnalysisService { String result = text; Map> sectionToPlaceholders = new HashMap<>(); + // NOTE: Используем regex-паттерны, чтобы совпасть с вариациями заголовков, + // которые может генерировать модель sectionToPlaceholders.put("III\\.\\s*Анализ рынка и сезонности", List.of("[[CHART_SEASONALITY]]")); sectionToPlaceholders.put("IV\\.\\s*Анализ целевой аудитории", List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]")); @@ -3298,7 +3433,9 @@ public class MarketingAnalysisService { List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]")); sectionToPlaceholders.put("VII\\.\\s*Каналы продвижения", List.of("[[CHART_CHANNELS]]")); sectionToPlaceholders.put("VIII\\.\\s*Воронка спроса", List.of("[[CHART_FUNNEL]]")); - sectionToPlaceholders.put("VI\\.\\s*SWOT-анализ", List.of("[[CHART_SWOT]]")); + // SWOT может быть написан как SWOT-анализ / SWOT анализ / SWОT-анализ, поэтому + // делаем мягче + sectionToPlaceholders.put("VI\\.\\s*SWOT[-\\s]?анализ", List.of("[[CHART_SWOT]]")); for (Map.Entry> sectionEntry : sectionToPlaceholders.entrySet()) { String sectionPattern = sectionEntry.getKey();