From 5c0f569fc290b1c4a5f9d8be4668772b275b4282 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Dec 2025 16:03:40 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 1151 ++--------------- 1 file changed, 97 insertions(+), 1054 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 1668a36..9b467cb 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -1007,440 +1007,11 @@ public class MarketingAnalysisService { } } - /** - * Промпт №2: Генерация текстового отчёта на основе JSON - */ - private String generateTextReportFromJson(Map analysisJson) { - logger.info("generateTextReportFromJson: Starting text report generation from JSON"); - if (analysisJson == null || analysisJson.isEmpty()) { - logger.warn("generateTextReportFromJson: Empty analysisJson provided for text report generation"); - return "Не удалось сгенерировать текстовый отчёт: отсутствуют данные JSON-анализа."; - } - logger.debug("generateTextReportFromJson: AnalysisJson keys: {}", analysisJson.keySet()); - - try { - // Преобразование JSON в строку для промпта - logger.debug("generateTextReportFromJson: Converting JSON to string"); - String jsonString = objectMapper.writeValueAsString(analysisJson); - logger.debug("generateTextReportFromJson: JSON string length: {} chars", jsonString.length()); - - // Построение промпта №2 - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n"); - promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа в следующей структуре:\n"); - promptBuilder.append(jsonString).append("\n\n"); - promptBuilder.append( - "На основе этих данных подготовь ПОНЯТНЫЙ для предпринимателя малого/среднего бизнеса текстовый отчёт.\n\n"); - promptBuilder.append("ТРЕБОВАНИЯ К ОТЧЁТУ:\n\n"); - promptBuilder.append("1. Структура разделов:\n"); - promptBuilder.append(" I. Краткое резюме (что за бизнес, какая цель, общий вывод)\n"); - promptBuilder.append(" II. Анализ продукта\n"); - promptBuilder.append(" III. Анализ рынка и сезонности\n"); - promptBuilder.append(" IV. Анализ целевой аудитории\n"); - promptBuilder.append(" V. Анализ конкурентов\n"); - promptBuilder.append(" VI. SWOT-анализ (в виде 4 списков)\n"); - promptBuilder.append(" VII. Каналы продвижения и их потенциал\n"); - promptBuilder.append(" VIII. Воронка спроса (с пояснением, где \"узкое место\")\n"); - promptBuilder.append(" IX. Рекомендации по позиционированию и УТП\n"); - promptBuilder.append(" X. Рекомендации по контенту\n"); - promptBuilder.append(" XI. Итоговая стратегия: что делать в первую очередь\n\n"); - promptBuilder.append("2. Используй данные ТОЛЬКО из JSON:\n\n"); - promptBuilder.append(" - seasonality → для описания сезонности\n"); - promptBuilder.append(" - market → для объёма рынка, темпов роста, трендов\n"); - promptBuilder.append(" - audienceAnalysis → для портрета ЦА (segments, ageGroups, genderDistribution)\n"); - promptBuilder - .append(" - competitorAnalysis → для анализа конкурентов (marketShareChart, comparisonTable)\n"); - promptBuilder.append(" - strategyAnalysis → для воронки, каналов и SWOT (если присутствует)\n"); - promptBuilder.append(" - valueProposition, positioning, contentRecommendations → для рекомендаций.\n\n"); - promptBuilder.append("3. Пиши простым, деловым, понятным языком:\n\n"); - promptBuilder.append(" - без академического жаргона,\n"); - promptBuilder.append(" - без лишней теории маркетинга,\n"); - promptBuilder.append(" - с акцентом на практическую пользу.\n\n"); - promptBuilder.append("4. Вставляй специальные метки плейсхолдеров там, где должны быть графики:\n\n"); - promptBuilder.append(" - [[CHART_SEASONALITY]] — внутри раздела \"III. Анализ рынка и сезонности\"\n"); - promptBuilder.append(" - [[CHART_AGE_DISTRIBUTION]] — внутри раздела \"IV. Анализ целевой аудитории\"\n"); - promptBuilder - .append(" - [[CHART_GENDER_DISTRIBUTION]] — внутри раздела \"IV. Анализ целевой аудитории\"\n"); - promptBuilder.append(" - [[CHART_MARKET_SHARE]] — внутри раздела \"V. Анализ конкурентов\"\n"); - promptBuilder.append(" - [[TABLE_COMPARISON]] — внутри раздела \"V. Анализ конкурентов\"\n"); - promptBuilder - .append(" - [[CHART_CHANNELS]] — внутри раздела \"VII. Каналы продвижения и их потенциал\"\n"); - promptBuilder.append(" - [[CHART_FUNNEL]] — внутри раздела \"VIII. Воронка спроса\"\n"); - promptBuilder.append(" - [[CHART_SWOT]] — внутри раздела \"VI. SWOT-анализ\"\n\n"); - promptBuilder.append( - " Вставляй метки в логичных местах текста, где график будет уместен для иллюстрации данных.\n\n"); - promptBuilder.append("5. Не выдумывай новые цифры — используй только те, что есть в JSON.\n"); - - String prompt = promptBuilder.toString(); - String context = jsonString; // Используем JSON как контекст - - logger.info("generateTextReportFromJson: Calling OpenAI API for text report generation"); - String textReport = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); - logger.info("generateTextReportFromJson: Text report generated. Length: {} chars", - textReport != null ? textReport.length() : 0); - return textReport != null ? textReport : "Не удалось сгенерировать текстовый отчёт."; - } catch (Exception e) { - logger.error("generateTextReportFromJson: Error generating text report from JSON: {}", e.getMessage(), e); - logger.error("generateTextReportFromJson: Stack trace: ", e); - return "Ошибка при генерации текстового отчёта: " + e.getMessage(); - } - } - - /** - * Генерация текстового отчёта на основе JSON (простое объединение без - * AI-суммирования) - */ - private String generateDeepTextReport(Map analysisJson) { - logger.info("generateDeepTextReport: Starting deep text report generation"); - if (analysisJson == null || analysisJson.isEmpty()) { - logger.warn("generateDeepTextReport: Empty analysisJson provided for deep text report generation"); - return "Не удалось сгенерировать текстовый отчёт: отсутствуют данные JSON-анализа."; - } - logger.debug("generateDeepTextReport: AnalysisJson keys: {}", analysisJson.keySet()); - - try { - StringBuilder fullReport = new StringBuilder(); - - // I. Краткое резюме - fullReport.append("I. Краткое резюме\n\n"); - fullReport.append("Данный маркетинговый анализ содержит детальную информацию о бизнесе, " - + "целевой аудитории, конкурентах, рыночной ситуации и рекомендациях по продвижению.\n\n"); - - // II. Анализ продукта - fullReport.append("II. Анализ продукта\n\n"); - if (analysisJson.containsKey("product") || analysisJson.containsKey("businessNiche")) { - Object product = analysisJson.get("product"); - Object niche = analysisJson.get("businessNiche"); - if (product != null || niche != null) { - fullReport.append("Продукт/услуга: ").append(product != null ? product : "").append("\n"); - fullReport.append("Ниша бизнеса: ").append(niche != null ? niche : "").append("\n\n"); - } - } else { - fullReport.append("Информация о продукте доступна в детальном JSON-анализе.\n\n"); - } - - // III. Анализ рынка и сезонности - fullReport.append("III. Анализ рынка и сезонности\n\n"); - @SuppressWarnings("unchecked") - Map seasonality = (Map) analysisJson.get("seasonality"); - @SuppressWarnings("unchecked") - Map market = (Map) analysisJson.get("market"); - - if (seasonality != null) { - Object description = seasonality.get("description"); - if (description != null) { - fullReport.append(description).append("\n\n"); - } - fullReport.append("[[CHART_SEASONALITY]]\n\n"); - } - - if (market != null) { - Object marketSize = market.get("marketSize"); - Object growthRate = market.get("growthRate"); - if (marketSize != null) { - fullReport.append("Объем рынка: ").append(marketSize).append("\n"); - } - if (growthRate != null) { - fullReport.append("Темп роста: ").append(growthRate).append("\n"); - } - @SuppressWarnings("unchecked") - List trends = (List) market.get("trends"); - if (trends != null && !trends.isEmpty()) { - fullReport.append("\nТренды рынка:\n"); - for (String trend : trends) { - fullReport.append("- ").append(trend).append("\n"); - } - } - @SuppressWarnings("unchecked") - List opportunities = (List) market.get("opportunities"); - if (opportunities != null && !opportunities.isEmpty()) { - fullReport.append("\nВозможности:\n"); - for (String opp : opportunities) { - fullReport.append("- ").append(opp).append("\n"); - } - } - @SuppressWarnings("unchecked") - List threats = (List) market.get("threats"); - if (threats != null && !threats.isEmpty()) { - fullReport.append("\nУгрозы:\n"); - for (String threat : threats) { - fullReport.append("- ").append(threat).append("\n"); - } - } - } - fullReport.append("\n"); - - // IV. Анализ целевой аудитории - fullReport.append("IV. Анализ целевой аудитории\n\n"); - @SuppressWarnings("unchecked") - Map audienceAnalysis = (Map) analysisJson.get("audienceAnalysis"); - if (audienceAnalysis != null) { - @SuppressWarnings("unchecked") - List> segments = (List>) audienceAnalysis.get("segments"); - if (segments != null && !segments.isEmpty()) { - for (Map segment : segments) { - Object name = segment.get("name"); - Object sharePercent = segment.get("sharePercent"); - Object ageRange = segment.get("ageRange"); - Object motivation = segment.get("motivation"); - Object triggers = segment.get("triggers"); - Object barriers = segment.get("barriers"); - - if (name != null) { - fullReport.append("### Сегмент: ").append(name); - if (sharePercent != null) { - fullReport.append(" (").append(sharePercent).append("%)\n"); - } else { - fullReport.append("\n"); - } - } - if (ageRange != null) { - fullReport.append("Возраст: ").append(ageRange).append("\n"); - } - if (motivation != null) { - fullReport.append("\nМотивация:\n").append(motivation).append("\n"); - } - if (triggers != null) { - fullReport.append("\nТриггеры:\n").append(triggers).append("\n"); - } - if (barriers != null) { - fullReport.append("\nБарьеры:\n").append(barriers).append("\n"); - } - fullReport.append("\n"); - } - } - fullReport.append("[[CHART_AGE_DISTRIBUTION]]\n\n"); - fullReport.append("[[CHART_GENDER_DISTRIBUTION]]\n\n"); - } - - // V. Анализ конкурентов - fullReport.append("V. Анализ конкурентов\n\n"); - @SuppressWarnings("unchecked") - Map competitorAnalysis = (Map) analysisJson.get("competitorAnalysis"); - if (competitorAnalysis != null) { - Object marketShareComment = competitorAnalysis.get("marketShareComment"); - if (marketShareComment != null) { - fullReport.append(marketShareComment).append("\n\n"); - } - - @SuppressWarnings("unchecked") - List> competitorsDetails = (List>) competitorAnalysis - .get("competitorsDetails"); - if (competitorsDetails != null && !competitorsDetails.isEmpty()) { - for (Map competitor : competitorsDetails) { - Object compName = competitor.get("name"); - Object priceStrategy = competitor.get("priceStrategy"); - Object contentStrategy = competitor.get("contentStrategy"); - @SuppressWarnings("unchecked") - Map swot = (Map) competitor.get("swot"); - - if (compName != null) { - fullReport.append("### ").append(compName).append("\n"); - } - if (priceStrategy != null) { - fullReport.append("Ценовая стратегия: ").append(priceStrategy).append("\n"); - } - if (contentStrategy != null) { - fullReport.append("\nКонтент-стратегия:\n").append(contentStrategy).append("\n"); - } - if (swot != null) { - @SuppressWarnings("unchecked") - List strengths = (List) swot.get("strengths"); - @SuppressWarnings("unchecked") - List weaknesses = (List) swot.get("weaknesses"); - if (strengths != null && !strengths.isEmpty()) { - fullReport.append("\nСильные стороны:\n"); - for (String strength : strengths) { - fullReport.append("- ").append(strength).append("\n"); - } - } - if (weaknesses != null && !weaknesses.isEmpty()) { - fullReport.append("\nСлабые стороны:\n"); - for (String weakness : weaknesses) { - fullReport.append("- ").append(weakness).append("\n"); - } - } - } - fullReport.append("\n"); - } - } - fullReport.append("[[CHART_MARKET_SHARE]]\n\n"); - fullReport.append("[[TABLE_COMPARISON]]\n\n"); - } - - // VI. SWOT-анализ - fullReport.append("VI. SWOT-анализ\n\n"); - @SuppressWarnings("unchecked") - Map swot = (Map) analysisJson.get("swot"); - if (swot != null) { - @SuppressWarnings("unchecked") - List strengths = (List) swot.get("strengths"); - @SuppressWarnings("unchecked") - List weaknesses = (List) swot.get("weaknesses"); - @SuppressWarnings("unchecked") - List opportunities = (List) swot.get("opportunities"); - @SuppressWarnings("unchecked") - List threats = (List) swot.get("threats"); - - if (strengths != null && !strengths.isEmpty()) { - fullReport.append("Сильные стороны:\n"); - for (String strength : strengths) { - fullReport.append("- ").append(strength).append("\n"); - } - fullReport.append("\n"); - } - if (weaknesses != null && !weaknesses.isEmpty()) { - fullReport.append("Слабые стороны:\n"); - for (String weakness : weaknesses) { - fullReport.append("- ").append(weakness).append("\n"); - } - fullReport.append("\n"); - } - if (opportunities != null && !opportunities.isEmpty()) { - fullReport.append("Возможности:\n"); - for (String opp : opportunities) { - fullReport.append("- ").append(opp).append("\n"); - } - fullReport.append("\n"); - } - if (threats != null && !threats.isEmpty()) { - fullReport.append("Угрозы:\n"); - for (String threat : threats) { - fullReport.append("- ").append(threat).append("\n"); - } - fullReport.append("\n"); - } - } - fullReport.append("[[CHART_SWOT]]\n\n"); - - // VII. Каналы продвижения - fullReport.append("VII. Каналы продвижения и их потенциал\n\n"); - @SuppressWarnings("unchecked") - Map channels = (Map) analysisJson.get("channels"); - if (channels != null) { - @SuppressWarnings("unchecked") - List> channelList = (List>) channels.get("recommended"); - if (channelList != null && !channelList.isEmpty()) { - for (Map channel : channelList) { - Object name = channel.get("name"); - Object potential = channel.get("potential"); - Object description = channel.get("description"); - if (name != null) { - fullReport.append("### ").append(name).append("\n"); - } - if (potential != null) { - fullReport.append("Потенциал: ").append(potential).append("\n"); - } - if (description != null) { - fullReport.append(description).append("\n"); - } - fullReport.append("\n"); - } - } - } - fullReport.append("[[CHART_CHANNELS]]\n\n"); - - // VIII. Воронка спроса - fullReport.append("VIII. Воронка спроса\n\n"); - @SuppressWarnings("unchecked") - Map funnel = (Map) analysisJson.get("funnel"); - if (funnel != null) { - @SuppressWarnings("unchecked") - List> stages = (List>) funnel.get("stages"); - if (stages != null && !stages.isEmpty()) { - for (Map stage : stages) { - Object stageName = stage.get("name"); - Object conversion = stage.get("conversion"); - Object description = stage.get("description"); - if (stageName != null) { - fullReport.append("### ").append(stageName); - if (conversion != null) { - fullReport.append(" (").append(conversion).append(")\n"); - } else { - fullReport.append("\n"); - } - } - if (description != null) { - fullReport.append(description).append("\n"); - } - fullReport.append("\n"); - } - } - } - fullReport.append("[[CHART_FUNNEL]]\n\n"); - - // IX. Рекомендации по позиционированию и УТП - fullReport.append("IX. Рекомендации по позиционированию и УТП\n\n"); - Object positioning = analysisJson.get("positioning"); - Object valueProposition = analysisJson.get("valueProposition"); - if (positioning != null) { - fullReport.append("Позиционирование:\n").append(positioning).append("\n\n"); - } - if (valueProposition != null) { - fullReport.append("Уникальное торговое предложение:\n").append(valueProposition).append("\n\n"); - } - - // X. Рекомендации по контенту - fullReport.append("X. Рекомендации по контенту\n\n"); - @SuppressWarnings("unchecked") - Map contentRecommendations = (Map) analysisJson - .get("contentRecommendations"); - if (contentRecommendations != null) { - @SuppressWarnings("unchecked") - List salesContent = (List) contentRecommendations.get("sales"); - @SuppressWarnings("unchecked") - List expertContent = (List) contentRecommendations.get("expertise"); - @SuppressWarnings("unchecked") - List trustContent = (List) contentRecommendations.get("trust"); - @SuppressWarnings("unchecked") - List entertainmentContent = (List) contentRecommendations.get("entertainment"); - - if (salesContent != null && !salesContent.isEmpty()) { - fullReport.append("Контент для продаж:\n"); - for (String content : salesContent) { - fullReport.append("- ").append(content).append("\n"); - } - fullReport.append("\n"); - } - if (expertContent != null && !expertContent.isEmpty()) { - fullReport.append("Контент для экспертизы:\n"); - for (String content : expertContent) { - fullReport.append("- ").append(content).append("\n"); - } - fullReport.append("\n"); - } - if (trustContent != null && !trustContent.isEmpty()) { - fullReport.append("Контент для доверия:\n"); - for (String content : trustContent) { - fullReport.append("- ").append(content).append("\n"); - } - fullReport.append("\n"); - } - if (entertainmentContent != null && !entertainmentContent.isEmpty()) { - fullReport.append("Развлекательный контент:\n"); - for (String content : entertainmentContent) { - fullReport.append("- ").append(content).append("\n"); - } - fullReport.append("\n"); - } - } - - // XI. Итоговая стратегия - fullReport.append("XI. Итоговая стратегия: что делать в первую очередь\n\n"); - fullReport.append("На основе проведенного анализа рекомендуется:\n\n"); - fullReport.append("1. Сфокусироваться на ключевых сегментах целевой аудитории\n"); - fullReport.append("2. Использовать наиболее эффективные каналы продвижения\n"); - fullReport.append("3. Разработать контент-стратегию с учетом потребностей аудитории\n"); - fullReport.append("4. Усилить позиционирование и уникальное торговое предложение\n"); - fullReport.append("5. Мониторить конкурентов и адаптировать стратегию\n\n"); - - logger.info("Text report generated successfully from JSON without AI summarization"); - return fullReport.toString(); - } catch (Exception e) { - logger.error("Error generating text report from JSON: {}", e.getMessage(), e); - return "Ошибка при генерации текстового отчёта: " + e.getMessage(); - } - } + // Удалены старые/неиспользуемые генераторы отчёта: + // - generateTextReportFromJson(...) + // - generateDeepTextReport(...) + // Актуальная генерация отчёта выполняется через модульные genSection* методы + + // сборку в generateMarketingReport(). /** * Проверяет, является ли ответ от AI валидным (не является отказом или ошибкой) @@ -1491,14 +1062,6 @@ public class MarketingAnalysisService { return true; } - private boolean hasNonEmptyValue(Map map, String key) { - if (map == null || key == null) { - return false; - } - Object v = map.get(key); - return !isEmptyChartData(v); - } - private String generateWithRetry(String jsonContext, String prompt, String lang, String primaryModel, String fallbackModel, int attempts) { int maxAttempts = Math.max(1, attempts); @@ -1566,7 +1129,14 @@ public class MarketingAnalysisService { "I cannot assist with that request.", "I cannot assist with that request", "I can't assist with that request.", - "I can't assist with that request" + "I can't assist with that request", + // Русские заглушки/ошибки генерации + "Не удалось сгенерировать раздел.", + "Не удалось сгенерировать раздел", + "Ошибка при генерации раздела.", + "Ошибка при генерации раздела", + "Раздел пропущен:", + "Раздел пропущен" }; for (String pattern : errorPatterns) { @@ -1884,7 +1454,13 @@ public class MarketingAnalysisService { promptBuilder.append("1. Объем: минимум 500-700 слов\n"); promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n"); promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n"); - promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n"); + promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n"); + promptBuilder.append( + "5. Обязательно назови объем рынка (15 млрд тенге) и темпы роста (12%).\n"); + promptBuilder.append( + "6. Объясни, что эти цифры значат для окупаемости фитнес-центра (логика окупаемости/скорость набора клиентов/масштаб спроса),\n"); + promptBuilder.append( + " но НЕ придумывай новые финансовые цифры, если их нет в JSON — используй качественные выводы + \"нет данных в JSON\".\n\n"); promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("II. Анализ продукта\n\n"); promptBuilder.append("Детально проанализируй:\n"); @@ -1959,8 +1535,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = openAIAnalyticsService.generateWithInstructionWithModel( - jsonString, promptBuilder.toString(), "ru", textModelName); + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName, + 2); if (!isValidAiResponse(result)) { logger.warn("genSectionMarket: AI returned invalid or refusal response"); @@ -1980,15 +1556,6 @@ public class MarketingAnalysisService { */ private String genSectionAudience(Map contextJson) { try { - // Если нет ключевых данных — не дергаем модель, чтобы не получать "короткие" - // ответы - boolean hasAudienceAnalysis = hasNonEmptyValue(contextJson, "audienceAnalysis"); - boolean hasAudienceSegments = hasNonEmptyValue(contextJson, "audienceSegments"); - if (!hasAudienceAnalysis && !hasAudienceSegments) { - return "IV. Анализ целевой аудитории\n\n" - + "Раздел пропущен: отсутствуют данные аудитории в JSON (audienceAnalysis/audienceSegments)."; - } - String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); @@ -2005,6 +1572,12 @@ public class MarketingAnalysisService { promptBuilder.append("IV. Анализ целевой аудитории\n\n"); promptBuilder.append( "Детально проанализируй аудиторию на основе audienceAnalysis (segments, ageGroups, genderDistribution):\n"); + promptBuilder.append( + "- Не просто опиши сегменты, а проанализируй данные графиков [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]].\n"); + promptBuilder.append( + "- Объясни, почему именно эти группы принесут больше денег (через частоту, удержание, вероятность апсейла, LTV).\n"); + promptBuilder.append( + "- Если финансовых метрик нет в JSON — прямо пиши \"нет данных в JSON\" и объясняй методику расчета.\n"); promptBuilder.append("- Демографические характеристики (возраст, пол, доход)\n"); promptBuilder.append("- Психографический портрет (потребности, мотивация, ценности)\n"); promptBuilder.append( @@ -2029,6 +1602,8 @@ public class MarketingAnalysisService { "ОБЯЗАТЕЛЬНО ПРО ГЕНДЕР:\n- Явно укажи долю женщин по данным genderDistribution (в типовом кейсе около 60%) и объясни, почему аудитория смещена в сторону женщин.\n"); promptBuilder.append( "- Дай практические рекомендации, как использовать это: месседжи, офферы, визуалы, каналы, время коммуникации.\n\n"); + promptBuilder.append( + "ОБЯЗАТЕЛЬНО ПРО ДЕНЬГИ:\n- После каждого крупного вывода добавляй мини-блок (1-2 предложения): \"Как это влияет на выручку\".\n\n"); promptBuilder.append("ВАЖНО:\n"); promptBuilder.append("- Начинай сразу с заголовка \"IV. Анализ целевой аудитории\"\n"); promptBuilder.append("- Не добавляй вступления или заключения\n"); @@ -2060,16 +1635,6 @@ public class MarketingAnalysisService { */ private String genSectionCompetitors(Map contextJson) { try { - boolean hasCompetitorAnalysis = hasNonEmptyValue(contextJson, "competitorAnalysis"); - boolean hasLegacyCompetitors = hasNonEmptyValue(contextJson, "competitors") - || hasNonEmptyValue(contextJson, "comparisonTable") - || hasNonEmptyValue(contextJson, "marketShareChart"); - if (!hasCompetitorAnalysis && !hasLegacyCompetitors) { - return "V. Анализ конкурентов\n\n" - + "Раздел пропущен: отсутствуют данные конкурентов в JSON (competitorAnalysis/competitors). " - + "Нужно сгенерировать competitorAnalysis.marketShareChart и competitorAnalysis.comparisonTable."; - } - String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); @@ -2082,6 +1647,11 @@ public class MarketingAnalysisService { promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n"); promptBuilder.append( "5. Это должен быть самый мощный раздел отчёта: каждое утверждение подтверждай цифрами из competitorAnalysis.marketShareChart / competitorAnalysis.comparisonTable.\n\n"); + promptBuilder.append("6. Сделай этот раздел самым длинным во всём отчёте.\n"); + promptBuilder.append( + "7. Проведи детальный баттл: Наш бренд vs Конкурент А vs Конкурент B. Используй данные из [[TABLE_COMPARISON]].\n"); + promptBuilder.append( + "8. Проанализируй график долей рынка [[CHART_MARKET_SHARE]] и предложи стратегию по \"откусыванию\" доли у лидера (что именно меняем: оффер/цена/продукт/каналы/контент).\n\n"); promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("V. Анализ конкурентов\n\n"); promptBuilder.append("Детально проанализируй:\n"); @@ -2114,6 +1684,8 @@ public class MarketingAnalysisService { "- Возьми двух главных конкурентов из competitorAnalysis (по доле рынка или по описанию) и назови их в тексте как \"Конкурент A\" и \"Конкурент B\".\n"); promptBuilder.append( "- Пройди ПО КАЖДОЙ строке competitorAnalysis.comparisonTable: Мы vs Конкурент A vs Конкурент B (с цифрами/формулировками из JSON) + практический вывод.\n\n"); + promptBuilder.append( + "ЕСЛИ ДАННЫХ НЕ ХВАТАЕТ:\n- Если marketShareChart/comparisonTable отсутствуют или пустые: явно напиши \"нет данных в JSON\" и предложи план сбора данных + гипотезы (с пометкой \"Гипотеза:\").\n\n"); promptBuilder.append("ВАЖНО:\n"); promptBuilder.append("- Начинай сразу с заголовка \"V. Анализ конкурентов\"\n"); promptBuilder.append("- Не добавляй вступления или заключения\n"); @@ -2338,16 +1910,8 @@ public class MarketingAnalysisService { // Этот раздел может быть качественным. Если полей positioning/valueProposition // нет, // разрешаем только гипотезы + план тестирования, не выдавая их за факты. - boolean hasPositioning = hasNonEmptyValue(contextJson, "positioning"); - boolean hasValueProp = hasNonEmptyValue(contextJson, "valueProposition"); - boolean hasAnyBase = hasNonEmptyValue(contextJson, "product") - || hasNonEmptyValue(contextJson, "businessNiche") - || hasNonEmptyValue(contextJson, "audienceAnalysis") - || hasNonEmptyValue(contextJson, "competitorAnalysis"); - if (!hasPositioning && !hasValueProp && !hasAnyBase) { - return "IX. Рекомендации по позиционированию и УТП\n\n" - + "Раздел пропущен: в JSON отсутствуют данные для позиционирования (positioning/valueProposition) и нет базового контекста для гипотез."; - } + // Не пропускаем раздел: если данных нет, будет гипотезирование + план проверки + // (см. промпт ниже) String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); @@ -2513,314 +2077,9 @@ public class MarketingAnalysisService { } } - /** - * Этап 1: Рынок и Продукт - * Разделы: I. Краткое резюме, II. Анализ продукта, III. Анализ рынка и - * сезонности - */ - private String generateStage1MarketAndProduct(String jsonContext) { - try { - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n"); - promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа.\n\n"); - promptBuilder - .append("ТВОЯ ЗАДАЧА: Напиши ТОЛЬКО следующие разделы отчета (без вступлений, без пояснений):\n\n"); - promptBuilder.append("I. Краткое резюме\n"); - promptBuilder.append(" Опиши бизнес, его цель и общий вывод (2-3 абзаца).\n\n"); - promptBuilder.append("II. Анализ продукта\n"); - promptBuilder.append( - " Детально проанализируй продукт/услугу, его особенности, преимущества и позиционирование.\n"); - promptBuilder.append(" Пиши максимально подробно, анализируй тренды.\n\n"); - promptBuilder.append("III. Анализ рынка и сезонности\n"); - promptBuilder.append(" Проанализируй рынок, его динамику, сезонные колебания спроса.\n"); - promptBuilder.append(" Используй данные из поля \"seasonality\" JSON.\n"); - promptBuilder.append( - " Вставь плейсхолдер [[CHART_SEASONALITY]] в логичном месте раздела, где график будет уместен.\n"); - promptBuilder.append(" Пиши максимально подробно, анализируй тренды.\n\n"); - promptBuilder.append("ВАЖНО:\n"); - promptBuilder.append("- Верни ТОЛЬКО эти три раздела, без вступлений типа \"Конечно, вот ваш отчет...\"\n"); - promptBuilder.append("- Начинай сразу с \"I. Краткое резюме\"\n"); - promptBuilder.append("- Используй данные ТОЛЬКО из переданного JSON\n"); - promptBuilder.append("- Пиши простым, деловым языком, с акцентом на практическую пользу\n"); - promptBuilder.append("- Не выдумывай новые цифры — используй только те, что есть в JSON\n"); - - String prompt = promptBuilder.toString(); - String result = openAIAnalyticsService.generateWithInstruction(jsonContext, prompt, "ru"); - - if (result == null || result.trim().isEmpty()) { - logger.warn("Stage 1 returned null or empty"); - return null; - } - - // Очистка от возможных вступлений - String cleaned = result.trim(); - if (cleaned.toLowerCase().startsWith("конечно") || - cleaned.toLowerCase().startsWith("вот") || - cleaned.toLowerCase().startsWith("ниже")) { - // Пытаемся найти начало первого раздела - int firstSection = cleaned.indexOf("I. "); - if (firstSection > 0) { - cleaned = cleaned.substring(firstSection); - } - } - - return cleaned; - } catch (Exception e) { - logger.error("Error generating stage 1 (Market and Product): {}", e.getMessage(), e); - return null; - } - } - - /** - * Этап 2: Аудитория и Конкуренты - * Разделы: IV. Анализ целевой аудитории, V. Анализ конкурентов - */ - private String generateStage2AudienceAndCompetitors(String jsonContext) { - try { - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n"); - promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа.\n\n"); - promptBuilder - .append("ТВОЯ ЗАДАЧА: Напиши ТОЛЬКО следующие разделы отчета (без вступлений, без пояснений):\n\n"); - promptBuilder.append("IV. Анализ целевой аудитории\n"); - promptBuilder.append(" Проведи глубокий психологический разбор целевой аудитории.\n"); - promptBuilder.append(" Используй данные из поля \"audienceSegments\" JSON.\n"); - promptBuilder.append( - " Опиши демографические характеристики, психографический профиль, потребности, поведенческие паттерны.\n"); - promptBuilder.append( - " Вставь плейсхолдер [[CHART_AUDIENCE]] в логичном месте раздела, где график будет уместен.\n"); - promptBuilder.append(" Сделай глубокий психологический разбор аудитории.\n\n"); - promptBuilder.append("V. Анализ конкурентов\n"); - promptBuilder.append(" Детально сравни конкурентов.\n"); - promptBuilder.append(" Используй данные из поля \"competitors\" JSON.\n"); - promptBuilder.append(" Опиши их сильные и слабые стороны, позиционирование, стратегии.\n"); - promptBuilder.append( - " Вставь плейсхолдер [[CHART_COMPETITORS]] в логичном месте раздела, где график будет уместен.\n"); - promptBuilder.append(" Детально сравни конкурентов.\n\n"); - promptBuilder.append("ВАЖНО:\n"); - promptBuilder.append("- Верни ТОЛЬКО эти два раздела, без вступлений типа \"Конечно, вот ваш отчет...\"\n"); - promptBuilder.append("- Начинай сразу с \"IV. Анализ целевой аудитории\"\n"); - promptBuilder.append("- Используй данные ТОЛЬКО из переданного JSON\n"); - promptBuilder.append("- Пиши простым, деловым языком, с акцентом на практическую пользу\n"); - promptBuilder.append("- Не выдумывай новые цифры — используй только те, что есть в JSON\n"); - - String prompt = promptBuilder.toString(); - String result = openAIAnalyticsService.generateWithInstruction(jsonContext, prompt, "ru"); - - if (result == null || result.trim().isEmpty()) { - logger.warn("Stage 2 returned null or empty"); - return null; - } - - // Очистка от возможных вступлений - String cleaned = result.trim(); - if (cleaned.toLowerCase().startsWith("конечно") || - cleaned.toLowerCase().startsWith("вот") || - cleaned.toLowerCase().startsWith("ниже")) { - // Пытаемся найти начало первого раздела - int firstSection = cleaned.indexOf("IV. "); - if (firstSection > 0) { - cleaned = cleaned.substring(firstSection); - } - } - - return cleaned; - } catch (Exception e) { - logger.error("Error generating stage 2 (Audience and Competitors): {}", e.getMessage(), e); - return null; - } - } - - /** - * Этап 3: Стратегия и Воронка - * Разделы: VI. SWOT-анализ, VII. Каналы продвижения, VIII. Воронка спроса - */ - private String generateStage3StrategyAndFunnel(String jsonContext) { - try { - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n"); - promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа.\n\n"); - promptBuilder - .append("ТВОЯ ЗАДАЧА: Напиши ТОЛЬКО следующие разделы отчета (без вступлений, без пояснений):\n\n"); - promptBuilder.append("VI. SWOT-анализ\n"); - promptBuilder.append( - " Представь SWOT-анализ в виде 4 списков: Сильные стороны, Слабые стороны, Возможности, Угрозы.\n"); - promptBuilder.append(" Используй данные из поля \"swot\" JSON.\n"); - promptBuilder.append( - " Вставь плейсхолдер [[CHART_SWOT]] в логичном месте раздела, где график/таблица будет уместна.\n\n"); - promptBuilder.append("VII. Каналы продвижения и их потенциал\n"); - promptBuilder.append(" Опиши рекомендуемые каналы продвижения.\n"); - promptBuilder.append(" Используй данные из поля \"channels\" JSON.\n"); - promptBuilder.append(" Дай обоснование каждому каналу (почему он важен, как его использовать).\n"); - promptBuilder.append( - " Вставь плейсхолдер [[CHART_CHANNELS]] в логичном месте раздела, где график будет уместен.\n\n"); - promptBuilder.append("VIII. Воронка спроса\n"); - promptBuilder.append(" Опиши этапы воронки спроса.\n"); - promptBuilder.append(" Используй данные из поля \"funnel\" JSON.\n"); - promptBuilder.append(" Опиши узкие места воронки (где происходят наибольшие потери конверсии).\n"); - promptBuilder.append( - " Вставь плейсхолдер [[CHART_FUNNEL]] в логичном месте раздела, где график будет уместен.\n\n"); - promptBuilder.append("ВАЖНО:\n"); - promptBuilder.append("- Верни ТОЛЬКО эти три раздела, без вступлений типа \"Конечно, вот ваш отчет...\"\n"); - promptBuilder.append("- Начинай сразу с \"VI. SWOT-анализ\"\n"); - promptBuilder.append("- Используй данные ТОЛЬКО из переданного JSON\n"); - promptBuilder.append("- Пиши простым, деловым языком, с акцентом на практическую пользу\n"); - promptBuilder.append("- Не выдумывай новые цифры — используй только те, что есть в JSON\n"); - - String prompt = promptBuilder.toString(); - String result = openAIAnalyticsService.generateWithInstruction(jsonContext, prompt, "ru"); - - if (result == null || result.trim().isEmpty()) { - logger.warn("Stage 3 returned null or empty"); - return null; - } - - // Очистка от возможных вступлений - String cleaned = result.trim(); - if (cleaned.toLowerCase().startsWith("конечно") || - cleaned.toLowerCase().startsWith("вот") || - cleaned.toLowerCase().startsWith("ниже")) { - // Пытаемся найти начало первого раздела - int firstSection = cleaned.indexOf("VI. "); - if (firstSection > 0) { - cleaned = cleaned.substring(firstSection); - } - } - - return cleaned; - } catch (Exception e) { - logger.error("Error generating stage 3 (Strategy and Funnel): {}", e.getMessage(), e); - return null; - } - } - - /** - * Этап 4: Рекомендации и Контент - * Разделы: IX. Позиционирование и УТП, X. Рекомендации по контенту, XI. - * Итоговая стратегия (Roadmap) - */ - private String generateStage4RecommendationsAndContent(String jsonContext) { - try { - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n"); - promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа.\n\n"); - promptBuilder - .append("ТВОЯ ЗАДАЧА: Напиши ТОЛЬКО следующие разделы отчета (без вступлений, без пояснений):\n\n"); - promptBuilder.append("IX. Рекомендации по позиционированию и УТП\n"); - promptBuilder.append(" Опиши позиционирование бренда и уникальное торговое предложение.\n"); - promptBuilder.append(" Используй данные из полей \"positioning\" и \"valueProposition\" JSON.\n"); - promptBuilder.append(" Представь практические рекомендации по позиционированию.\n\n"); - promptBuilder.append("X. Рекомендации по контенту\n"); - promptBuilder.append(" Дай практические рекомендации по типам контента для маркетинга.\n"); - promptBuilder.append(" Используй данные из поля \"contentRecommendations\" JSON.\n"); - promptBuilder - .append(" Опиши, какой контент использовать для продаж, экспертизы, доверия, развлечения.\n\n"); - promptBuilder.append("XI. Итоговая стратегия: что делать в первую очередь\n"); - promptBuilder.append(" Создай практический Roadmap (дорожную карту) действий.\n"); - promptBuilder.append(" Укажи конкретные шаги, приоритеты, последовательность действий.\n"); - promptBuilder.append( - " Пиши практические шаги, бери данные из полей \"positioning\", \"valueProposition\", \"contentRecommendations\" JSON.\n\n"); - promptBuilder.append("ВАЖНО:\n"); - promptBuilder.append("- Верни ТОЛЬКО эти три раздела, без вступлений типа \"Конечно, вот ваш отчет...\"\n"); - promptBuilder.append("- Начинай сразу с \"IX. Рекомендации по позиционированию и УТП\"\n"); - promptBuilder.append("- Используй данные ТОЛЬКО из переданного JSON\n"); - promptBuilder.append("- Пиши простым, деловым языком, с акцентом на практическую пользу\n"); - promptBuilder.append("- Не выдумывай новые цифры — используй только те, что есть в JSON\n"); - promptBuilder.append( - "- Пиши практические шаги, бери данные из полей positioning, valueProposition, contentRecommendations\n"); - - String prompt = promptBuilder.toString(); - String result = openAIAnalyticsService.generateWithInstruction(jsonContext, prompt, "ru"); - - if (result == null || result.trim().isEmpty()) { - logger.warn("Stage 4 returned null or empty"); - return null; - } - - // Очистка от возможных вступлений - String cleaned = result.trim(); - if (cleaned.toLowerCase().startsWith("конечно") || - cleaned.toLowerCase().startsWith("вот") || - cleaned.toLowerCase().startsWith("ниже")) { - // Пытаемся найти начало первого раздела - int firstSection = cleaned.indexOf("IX. "); - if (firstSection > 0) { - cleaned = cleaned.substring(firstSection); - } - } - - return cleaned; - } catch (Exception e) { - logger.error("Error generating stage 4 (Recommendations and Content): {}", e.getMessage(), e); - return null; - } - } - - /** - * Валидация JSON-анализа - */ - private boolean validateJsonAnalysis(Map jsonMap) { - if (jsonMap == null || jsonMap.isEmpty()) { - return false; - } - - try { - // Проверка обязательных полей - if (!jsonMap.containsKey("meta") || !jsonMap.containsKey("seasonality") || - !jsonMap.containsKey("competitors") || !jsonMap.containsKey("audienceSegments") || - !jsonMap.containsKey("channels") || !jsonMap.containsKey("funnel") || - !jsonMap.containsKey("swot")) { - logger.warn("Missing required fields in JSON analysis"); - return false; - } - - // Проверка суммы sharePercent = 100 - @SuppressWarnings("unchecked") - Map audienceSegments = (Map) jsonMap.get("audienceSegments"); - if (audienceSegments != null && audienceSegments.containsKey("segments")) { - @SuppressWarnings("unchecked") - List> segments = (List>) audienceSegments.get("segments"); - if (segments != null) { - int totalPercent = 0; - for (Map segment : segments) { - if (segment.containsKey("sharePercent")) { - Object percentObj = segment.get("sharePercent"); - if (percentObj instanceof Number) { - totalPercent += ((Number) percentObj).intValue(); - } - } - } - if (totalPercent != 100) { - logger.warn("Sum of sharePercent is {} instead of 100", totalPercent); - // Не считаем это критической ошибкой, продолжаем работу - } - } - } - - // Проверка диапазонов значений - @SuppressWarnings("unchecked") - Map seasonality = (Map) jsonMap.get("seasonality"); - if (seasonality != null && seasonality.containsKey("demandIndex")) { - @SuppressWarnings("unchecked") - List demandIndex = (List) seasonality.get("demandIndex"); - if (demandIndex != null) { - for (Object value : demandIndex) { - if (value instanceof Number) { - int intValue = ((Number) value).intValue(); - if (intValue < 0 || intValue > 100) { - logger.warn("demandIndex value {} is out of range [0-100]", intValue); - } - } - } - } - } - - return true; - } catch (Exception e) { - logger.error("Error validating JSON analysis: {}", e.getMessage(), e); - return false; - } - } + // Удалены неиспользуемые stage-генераторы (Stage 1-4) и + // validateJsonAnalysis(...). + // Используется модульная генерация разделов I–XI (genSection*). /** * Построение строки контекста из запроса @@ -3801,153 +3060,6 @@ public class MarketingAnalysisService { return report; } - private String generateAnalysisByType(String context, AnalysisType analysisType, DetailLevel detailLevel) { - if (analysisType == null) { - analysisType = AnalysisType.РЫНОК; // Default - } - if (detailLevel == null) { - detailLevel = DetailLevel.СТАНДАРТНО; // Default - } - - String prompt; - String lengthInstruction; - - // Adjust length based on detail level - if (detailLevel == DetailLevel.КРАТКО) { - lengthInstruction = "Ответ должен быть кратким структурированным текстом на русском языке (1-2 абзаца)."; - } else if (detailLevel == DetailLevel.ПОДРОБНО) { - lengthInstruction = "Ответ должен быть подробным структурированным текстом на русском языке (5-8 абзацев) с глубоким анализом."; - } else { - lengthInstruction = "Ответ должен быть структурированным текстом на русском языке (3-5 абзацев)."; - } - - switch (analysisType) { - case РЫНОК: - prompt = "Проведи анализ рынка на основе предоставленной информации о бизнесе. " + - "Включи: размер рынка, динамику роста, основные сегменты, тренды и перспективы развития. " + - lengthInstruction + "\n\n" + context; - break; - case КОНКУРЕНТЫ: - prompt = "Проведи анализ конкурентов на основе предоставленной информации о бизнесе. " + - "Включи: основных конкурентов, их сильные и слабые стороны, позиционирование, " + - "ценовую политику и маркетинговые стратегии. " + lengthInstruction + "\n\n" + context; - break; - case ЦА: - prompt = "Проведи анализ целевой аудитории на основе предоставленной информации о бизнесе. " + - "Включи: демографические характеристики, психографический профиль, потребности и боли, " + - "поведенческие паттерны и предпочтения. " + lengthInstruction + "\n\n" + context; - break; - case КАНАЛЫ: - prompt = "Проведи анализ маркетинговых каналов на основе предоставленной информации о бизнесе. " + - "Включи: оценку эффективности различных каналов коммуникации, рекомендации по выбору каналов, " - + - "особенности использования каждого канала и бюджетные рекомендации. " + lengthInstruction - + "\n\n" + context; - break; - case SWOT: - prompt = "Проведи SWOT-анализ на основе предоставленной информации о бизнесе. " + - "Включи анализ: Сильных сторон (Strengths), Слабых сторон (Weaknesses), " + - "Возможностей (Opportunities) и Угроз (Threats). " + - "Ответ должен быть структурированным текстом на русском языке, " + - "с четким разделением по каждому разделу SWOT. " + lengthInstruction + "\n\n" + context; - break; - default: - prompt = "На основе следующей информации о бизнесе создай резюме маркетингового анализа. " + - "Выдели ключевые возможности и особенности бизнеса. " + - lengthInstruction + "\n\n" + context; - } - - return openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); - } - - private List parseChannels(String channelsStr) { - if (channelsStr == null || channelsStr.trim().isEmpty()) { - return new ArrayList<>(); - } - List channels = new ArrayList<>(); - String[] parts = channelsStr.split("[,;\\n]"); - for (String part : parts) { - String trimmed = part.trim(); - if (!trimmed.isEmpty() && trimmed.length() > 1) { - channels.add(trimmed); - } - } - return channels; - } - - private List parseRecommendations(String recommendationsStr) { - if (recommendationsStr == null || recommendationsStr.trim().isEmpty()) { - return new ArrayList<>(); - } - List recommendations = new ArrayList<>(); - String[] lines = recommendationsStr.split("\\n"); - for (String line : lines) { - String trimmed = line.trim(); - // Remove numbering and bullets - trimmed = trimmed.replaceAll("^[\\d\\-\\.\\*]+\\s*", ""); - if (!trimmed.isEmpty() && trimmed.length() > 10) { - recommendations.add(trimmed); - } - } - return recommendations; - } - - private Map parseStrategy(String strategyText, List defaultChannels) { - Map strategy = new HashMap<>(); - - // Try to extract duration - String duration = "2 недели"; // default - if (strategyText != null) { - if (strategyText.contains("неделя") || strategyText.contains("недели") || strategyText.contains("недель")) { - // Extract duration pattern - java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)\\s*(неделя|недели|недель)"); - java.util.regex.Matcher matcher = pattern.matcher(strategyText); - if (matcher.find()) { - duration = matcher.group(1) + " " + matcher.group(2); - } - } else if (strategyText.contains("месяц") || strategyText.contains("месяца") - || strategyText.contains("месяцев")) { - java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d+)\\s*(месяц|месяца|месяцев)"); - java.util.regex.Matcher matcher = pattern.matcher(strategyText); - if (matcher.find()) { - duration = matcher.group(1) + " " + matcher.group(2); - } - } - } - strategy.put("duration", duration); - - // Extract channels from strategy text or use default - List channels = parseChannels(strategyText); - if (channels.isEmpty()) { - channels = defaultChannels.isEmpty() ? Arrays.asList("Instagram", "Telegram", "21MC") : defaultChannels; - } - strategy.put("channels", channels); - - // Extract content types - List contentTypes = new ArrayList<>(); - if (strategyText != null) { - String lowerText = strategyText.toLowerCase(); - if (lowerText.contains("пост") || lowerText.contains("посты")) { - contentTypes.add("посты"); - } - if (lowerText.contains("сторис") || lowerText.contains("stories")) { - contentTypes.add("сторис"); - } - if (lowerText.contains("баннер") || lowerText.contains("banner")) { - contentTypes.add("баннеры"); - } - if (lowerText.contains("видео") || lowerText.contains("video")) { - contentTypes.add("видео"); - } - } - if (contentTypes.isEmpty()) { - contentTypes = Arrays.asList("посты", "сторис", "баннеры"); - } - strategy.put("contentTypes", contentTypes); - - return strategy; - } - private String buildMarkdownFromReport(Map reportData, MarketingAnalysisRequest request) { logger.info("buildMarkdownFromReport: Starting markdown generation for product: {}", request.getProduct()); logger.debug("buildMarkdownFromReport: ReportData keys: {}", reportData != null ? reportData.keySet() : "null"); @@ -4001,21 +3113,6 @@ public class MarketingAnalysisService { return markdown.toString(); } - private void appendJsonChart(StringBuilder markdown, String heading, String chartKey, Object data) { - if (isEmptyChartData(data)) { - return; - } - try { - String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); - markdown.append(heading).append("\n\n"); - markdown.append("```json:").append(chartKey).append("\n"); - markdown.append(jsonStr); - markdown.append("\n```\n\n"); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to serialize chart data for " + chartKey, e); - } - } - private boolean isEmptyChartData(Object data) { if (data == null) { return true; @@ -4031,7 +3128,7 @@ public class MarketingAnalysisService { /** * Заменяет плейсхолдеры в тексте отчета на JSON-блоки с данными графиков - * + * * @param fullAnalysis текст отчета с плейсхолдерами * @param chartsData данные для графиков * @return текст отчета с замененными плейсхолдерами на JSON-блоки @@ -4054,7 +3151,6 @@ public class MarketingAnalysisService { if (chartsData == null || chartsData.isEmpty()) { logger.warn("replaceChartPlaceholders: chartsData is null or empty, removing all placeholders"); - // Если данных нет, удаляем все плейсхолдеры и очищаем пустые строки String cleaned = fullAnalysis .replace("[[CHART_SEASONALITY]]", "") .replace("[[CHART_AUDIENCE]]", "") @@ -4066,7 +3162,6 @@ public class MarketingAnalysisService { .replace("[[CHART_CHANNELS]]", "") .replace("[[CHART_FUNNEL]]", "") .replace("[[CHART_SWOT]]", ""); - // Удаляем множественные пустые строки (более 2 подряд) cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n"); return cleaned; } @@ -4074,7 +3169,6 @@ public class MarketingAnalysisService { String result = fullAnalysis; Set processedPlaceholders = new HashSet<>(); - // Маппинг плейсхолдеров на ключи данных и chartKey Map placeholderMap = new HashMap<>(); placeholderMap.put("[[CHART_SEASONALITY]]", new ChartMapping("seasonality", "seasonality")); placeholderMap.put("[[CHART_AUDIENCE]]", new ChartMapping("audienceSegmentation", "audienceSegmentation")); @@ -4087,90 +3181,70 @@ 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(); - // Используем Pattern.quote, чтобы спецсимволы в плейсхолдере не ломали regex String regex = java.util.regex.Pattern.quote(placeholder); - if (result.contains(placeholder)) { - processedPlaceholders.add(placeholder); - Object chartData = chartsData != null ? chartsData.get(mapping.dataKey) : null; + if (!result.contains(placeholder)) { + continue; + } - if (!isEmptyChartData(chartData)) { - try { - String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData); - // Используем Matcher.quoteReplacement для безопасной вставки JSON - String jsonBlock = "\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n"; - String replacement = java.util.regex.Matcher.quoteReplacement(jsonBlock); - result = result.replaceAll(regex, replacement); - 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"); - } 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"); - } - } else { - logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.", - placeholder, mapping.dataKey); - // Если данных нет, удаляем плейсхолдер и окружающие пустые строки - // Удаляем плейсхолдер вместе с предшествующим и последующим переносом строки, - // если они есть + processedPlaceholders.add(placeholder); + 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); + 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"); + } 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"); } + } else { + logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.", placeholder, + mapping.dataKey); + String placeholderWithNewlines = "(\\r?\\n)?\\s*" + regex + "\\s*(\\r?\\n)?"; + result = result.replaceAll(placeholderWithNewlines, "\n"); } } - // Fallback: добавляем графики в конец соответствующих разделов, если - // плейсхолдер отсутствовал logger.debug("replaceChartPlaceholders: Adding missing charts to sections"); result = addMissingChartsToSections(result, chartsData, placeholderMap, processedPlaceholders); - // Финальная очистка: удаляем все оставшиеся не замененные плейсхолдеры - // (если AI вернул их, но данных в chartsData нет) 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()) { - String placeholder = matcher.group(); - remainingPlaceholders.add(placeholder); + remainingPlaceholders.add(matcher.group()); } for (String placeholder : remainingPlaceholders) { - // Проверяем, есть ли данные для этого плейсхолдера boolean hasData = false; ChartMapping mapping = placeholderMap.get(placeholder); - if (mapping != null) { - Object chartData = chartsData.get(mapping.dataKey); - if (!isEmptyChartData(chartData)) { - hasData = true; - } + 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"); } } - // Финальная очистка: удаляем множественные пустые строки (более 2 подряд) result = result.replaceAll("(\\r?\\n){3,}", "\n\n"); logger.info( @@ -4179,9 +3253,6 @@ public class MarketingAnalysisService { return result; } - /** - * Вспомогательный класс для маппинга плейсхолдеров - */ private static class ChartMapping { final String dataKey; final String chartKey; @@ -4192,16 +3263,10 @@ public class MarketingAnalysisService { } } - /** - * Добавляет графики в конец соответствующих разделов, если плейсхолдер - * отсутствовал, но данные есть - */ private String addMissingChartsToSections(String text, Map chartsData, Map placeholderMap, Set processedPlaceholders) { String result = text; - // Маппинг разделов на плейсхолдеры (может быть несколько плейсхолдеров в одном - // разделе) Map> sectionToPlaceholders = new HashMap<>(); sectionToPlaceholders.put("III\\.\\s*Анализ рынка и сезонности", List.of("[[CHART_SEASONALITY]]")); sectionToPlaceholders.put("IV\\.\\s*Анализ целевой аудитории", @@ -4215,7 +3280,6 @@ public class MarketingAnalysisService { for (Map.Entry> sectionEntry : sectionToPlaceholders.entrySet()) { String sectionPattern = sectionEntry.getKey(); List placeholders = sectionEntry.getValue(); - if (placeholders == null || placeholders.isEmpty()) { continue; } @@ -4224,30 +3288,22 @@ public class MarketingAnalysisService { boolean hasAnyBlock = false; for (String placeholder : placeholders) { - if (placeholder == null || placeholder.isBlank()) { + if (placeholder == null || placeholder.isBlank() || processedPlaceholders.contains(placeholder)) { continue; } - // Если плейсхолдер уже был обработан (присутствовал в тексте), пропускаем его - if (processedPlaceholders.contains(placeholder)) { - continue; - } - ChartMapping mapping = placeholderMap.get(placeholder); if (mapping == null) { continue; } - Object chartData = chartsData.get(mapping.dataKey); if (isEmptyChartData(chartData)) { 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(placeholder); } catch (JsonProcessingException e) { logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage()); @@ -4258,16 +3314,12 @@ public class MarketingAnalysisService { continue; } - // Ищем раздел в тексте: сначала по markdown заголовкам, затем по римским цифрам 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(); - // Если не нашли по markdown, ищем по римским цифрам (I., II., III. и т.д.) if (!found) { - // Паттерн для поиска раздела по римским цифрам: I. Название, II. Название и - // т.д. pattern = java.util.regex.Pattern .compile("(^|\\n)\\s*" + sectionPattern + "[^\\n]*(\\n|$)", java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); @@ -4275,46 +3327,37 @@ public class MarketingAnalysisService { found = matcher.find(); } - if (found) { - // Вставляем после найденного заголовка раздела, перед следующим разделом или - // концом текста - int insertPos = matcher.end(); + if (!found) { + logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern); + continue; + } - // Ищем конец текущего раздела (начало следующего раздела или конец текста) - String remainingText = result.substring(insertPos); + int insertPos = matcher.end(); + String remainingText = result.substring(insertPos); - // Паттерн для поиска следующего раздела: может быть markdown заголовок или - // римская цифра - 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); + 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(); + String blocks = blocksToInsert.toString(); - if (nextSectionMatcher.find()) { - // Вставляем перед следующим разделом - int nextSectionPos = insertPos + nextSectionMatcher.start(); + 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 { - // Ищем следующий раздел по римской цифре - 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; - } + result = result + blocks; } - logger.debug("addMissingChartsToSections: Added fallback blocks to section matching pattern: {}", - sectionPattern); - } else { - logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern); } + logger.debug("addMissingChartsToSections: Added fallback blocks to section matching pattern: {}", + sectionPattern); } return result;