This commit is contained in:
root
2025-12-20 22:46:38 +05:00
parent 9ffdcbcd3b
commit e5aa373c61
2 changed files with 202 additions and 15 deletions
@@ -32,6 +32,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_ULTIMATE_JSON_PRESENCE_INSTRUCTION = ""
+ "ИНСТРУКЦИЯ (ОБЯЗАТЕЛЬНО): Данные в JSON 100% присутствуют. Тебе ЗАПРЕЩЕНО писать, что данных нет/данные отсутствуют/нет информации.\n"
+ "Найди нужный объект в JSON, проанализируй каждую цифру/процент и вставь соответствующий тег [[CHART_...]] / [[TABLE_...]] рядом с тем абзацем, где ты обсуждаешь эти данные.\n"
+ "Если какая-то бизнес-метрика не представлена явно, используй аналитическую экстраполяцию и обозначь это как \"Экстраполяция\" (без фраз про отсутствие данных).\n\n";
private static final String AI_PREMIUM_CONSULTING_STYLE_GUIDE = ""
+ "СТИЛЬ (ОБЯЗАТЕЛЬНО): пиши как консалтинг «Большой четверки»: структурно, уверенно, с причинно-следственной логикой.\n"
+ "Используй профессиональную лексику и леммы: «аналитическая экстраполяция», «дифференциация предложения», «конверсионный слой», «кастомизация оффера».\n"
@@ -45,6 +49,12 @@ public class MarketingAnalysisService {
}
}
private void appendUltimateJsonPresenceInstruction(StringBuilder promptBuilder) {
if (promptBuilder != null) {
promptBuilder.append(AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION);
}
}
private void appendPremiumConsultingStyleGuide(StringBuilder promptBuilder) {
if (promptBuilder != null) {
promptBuilder.append(AI_PREMIUM_CONSULTING_STYLE_GUIDE);
@@ -1337,6 +1347,44 @@ public class MarketingAnalysisService {
return cleaned;
}
/**
* Жесткая санитизация: убирает запрещённые фразы про отсутствие данных и, при
* необходимости, хвостовые плейсхолдеры [[CHART_*]]/[[TABLE_*]].
*
* Цель: в финальном отчёте не должно быть ни одной фразы "данные отсутствуют"/"нет данных"
* и не должно оставаться токенов [[CHART_...]].
*/
private String hardSanitizeReportText(String text, boolean removePlaceholders) {
if (text == null || text.isBlank()) {
return text;
}
String cleaned = text;
// 1) Replace "no data" sentences with a neutral analytical extrapolation statement.
String[] noDataSentencePatterns = new String[] {
"(?i)\\\\s*сожалению\\b[^\\n\\.\\!\\?]*\\bданн[а-яё]*\\b[^\\n\\.\\!\\?]*(нет|не\\s*представлен[а-яё]*)[^\\n\\.\\!\\?]*[\\.\\!\\?]?",
"(?i)\\bданные\\s*(отсутствуют|не\\s*представлен[а-яё]*|не\\s*доступн[а-яё]*)\\b[^\\n\\.\\!\\?]*[\\.\\!\\?]?",
"(?i)\\bнет\\s*данных\\b[^\\n\\.\\!\\?]*[\\.\\!\\?]?",
"(?i)\\bнет\\s*информации\\b[^\\n\\.\\!\\?]*[\\.\\!\\?]?"
};
for (String p : noDataSentencePatterns) {
cleaned = cleaned.replaceAll(p, "Экстраполяция: применяем консервативные допущения и методику расчета.");
}
// 2) Remove lingering short phrases (in case model wrote it without punctuation)
cleaned = cleaned.replaceAll("(?i)\\b(нет\\s*данных|данные\\s*отсутствуют|нет\\s*информации)\\b", "");
// 3) Optionally remove any leftover placeholders
if (removePlaceholders) {
cleaned = cleaned.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", "");
}
// Normalize whitespace
cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n").trim();
return cleaned;
}
/**
* Удаляет «пустые» фразы-заглушки про графики, если рядом нет плейсхолдера
* [[CHART_...]] / [[TABLE_...]] или ```json:<key> блока.
@@ -1547,6 +1595,11 @@ public class MarketingAnalysisService {
}
}
}
// Для канальной матрицы (heatmap) нужен аудиторийный контекст (segments +
// segmentsChannelMatrix)
if (fullJson.containsKey("audienceAnalysis")) {
context.put("audienceAnalysis", fullJson.get("audienceAnalysis"));
}
// Даже если channelsPotential отсутствует/пустой даём опорный контекст (для
// гипотез)
if (fullJson.containsKey("strategyAnalysis")) {
@@ -1567,6 +1620,43 @@ public class MarketingAnalysisService {
if (fullJson.containsKey("conversionFunnel")) {
context.put("conversionFunnel", fullJson.get("conversionFunnel"));
}
// Alias: если conversionFunnel отсутствует в полном JSON, но есть
// strategyAnalysis.funnel.stages
// собираем conversionFunnel в формате, ожидаемом графиком [[CHART_FUNNEL]].
if (!context.containsKey("conversionFunnel") && fullJson.containsKey("strategyAnalysis")) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> strategyAnalysis = (Map<String, Object>) fullJson.get("strategyAnalysis");
if (strategyAnalysis != null && strategyAnalysis.get("funnel") instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> funnel = (Map<String, Object>) strategyAnalysis.get("funnel");
if (funnel != null && funnel.get("stages") instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
if (stages != null && !stages.isEmpty()) {
List<Map<String, Object>> funnelList = new ArrayList<>();
for (Map<String, Object> stage : stages) {
Map<String, Object> stageData = new HashMap<>();
stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
stageData.put("value", stage.getOrDefault("value", 0));
Object conversionObj = stage.get("conversionPercent");
if (conversionObj instanceof Number) {
stageData.put("conversion", ((Number) conversionObj).intValue());
} else {
stageData.put("conversion", 0);
}
funnelList.add(stageData);
}
if (!funnelList.isEmpty()) {
context.put("conversionFunnel", funnelList);
}
}
}
}
} catch (Exception ignored) {
// Safe fallback: do not fail section context preparation
}
}
// Даже если funnel отсутствует/пустой даём опорный контекст (для гипотез)
if (fullJson.containsKey("strategyAnalysis")) {
context.put("strategyAnalysis", fullJson.get("strategyAnalysis"));
@@ -1660,6 +1750,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder
@@ -1713,6 +1804,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder
@@ -1729,7 +1821,7 @@ public class MarketingAnalysisService {
promptBuilder.append(
"6. Объясни, что эти цифры значат для окупаемости фитнес-центра (логика окупаемости/скорость набора клиентов/масштаб спроса),\n");
promptBuilder.append(
" но НЕ придумывай новые финансовые цифры, если их нет в JSON — используй качественные выводы + \"нет данных в JSON\".\n\n");
" но НЕ придумывай новые финансовые цифры: если в JSON нет чисел для части расчета — используй качественные выводы + аналитическую экстраполяцию (пометь как \"Экстраполяция\").\n\n");
promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
promptBuilder.append("II. Анализ продукта\n\n");
promptBuilder.append("Детально проанализируй:\n");
@@ -1771,6 +1863,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder.append(
@@ -1816,7 +1909,7 @@ public class MarketingAnalysisService {
promptBuilder.append("- Используй данные из полей seasonality, market, regions\n");
promptBuilder.append("- Пиши максимально подробно, анализируй тренды\n");
promptBuilder.append(
"- Каждое ключевое утверждение подтверждай конкретной цифрой из JSON (или явно помечай \"нет данных в JSON\")\n\n");
"- Каждое ключевое утверждение подтверждай конкретной цифрой из JSON; если нужна связующая метрика — делай экстраполяцию и помечай как \"Экстраполяция\".\n\n");
promptBuilder.append("JSON ДАННЫЕ:\n");
promptBuilder.append(jsonString);
@@ -1844,6 +1937,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
promptBuilder.append(
"КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. "
+ "Ты обязан найти объект `audienceAnalysis` в переданном тебе тексте. "
@@ -1885,7 +1979,7 @@ public class MarketingAnalysisService {
promptBuilder.append(
"- Объясни, почему именно эти группы принесут больше денег (через частоту, удержание, вероятность апсейла, LTV).\n");
promptBuilder.append(
"- Если финансовых метрик нет в JSON — прямо пиши \"нет данных в JSON\" и объясняй методику расчета.\n");
"- Если финансовые метрики не представлены явно: выполни аналитическую экстраполяцию (LTV/AvgCheck/частота) и опиши методику, пометив блок как \"Экстраполяция\".\n");
promptBuilder.append("- Демографические характеристики (возраст, пол, доход)\n");
promptBuilder.append("- Психографический портрет (потребности, мотивация, ценности)\n");
promptBuilder.append(
@@ -1948,6 +2042,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
promptBuilder.append(
"КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. "
+ "Ты обязан найти объект `competitorAnalysis` в переданном тебе тексте. "
@@ -1960,7 +2055,7 @@ public class MarketingAnalysisService {
promptBuilder.append("ТРЕБОВАНИЯ:\n");
promptBuilder.append(
"ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
promptBuilder.append("1. Объем: минимум 500-700 слов\n");
promptBuilder.append("1. Объем: минимум 800-1100 слов\n");
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
@@ -1977,7 +2072,11 @@ public class MarketingAnalysisService {
promptBuilder.append(
"7. Проведи детальный баттл: Наш бренд vs Конкурент А vs Конкурент B. Используй данные из [[TABLE_COMPARISON]].\n");
promptBuilder.append(
"8. Проанализируй график долей рынка [[CHART_MARKET_SHARE]] и предложи стратегию по \"откусыванию\" доли у лидера (что именно меняем: оффер/цена/продукт/каналы/контент).\n\n");
"8. Ты ОБЯЗАН сравнить наш бренд с Конкурентом A по КАЖДОЙ строке таблицы и сделать вывод (где мы сильнее/слабее, чем это монетизировать).\n");
promptBuilder.append(
"9. РАСЧЕТ (ОБЯЗАТЕЛЬНО): рассчитай разрыв в охвате в процентах относительно НАС: $$ gap\\% = \\frac{Reach_{A}-Reach_{Us}}{Reach_{Us}}\\times 100\\% $$. Подставь числа из competitorAnalysis.competitorsDetails.digitalMetrics.reach (или ближайший числовой прокси из JSON) и интерпретируй бизнес-эффект.\n\n");
promptBuilder.append(
"10. Проанализируй график долей рынка [[CHART_MARKET_SHARE]] и предложи стратегию по \"откусыванию\" доли у лидера (что именно меняем: оффер/цена/продукт/каналы/контент).\n\n");
promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
promptBuilder.append("V. Анализ конкурентов\n\n");
promptBuilder.append("Детально проанализируй:\n");
@@ -2012,15 +2111,13 @@ 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");
promptBuilder.append(
"- Используй данные из competitorAnalysis.marketShareChart и competitorAnalysis.comparisonTable\n");
promptBuilder.append(
"- Не выдумывай цифры: если по строке сравнения нет данных — явно пометь это\n\n");
"- Не выдумывай цифры. Если в конкретной строке/метрике нет явного значения — делай консервативную экстраполяцию и помечай как \"Экстраполяция\" (без фраз про отсутствие данных).\n\n");
promptBuilder.append("JSON ДАННЫЕ:\n");
promptBuilder.append(jsonString);
@@ -2048,6 +2145,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder.append("На основе следующего JSON напиши РАЗДЕЛ VI (SWOT-анализ) маркетингового отчета.\n\n");
@@ -2114,12 +2212,13 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ VII (Каналы продвижения и их потенциал) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
promptBuilder.append("1. Объем: минимум 500-700 слов\n");
promptBuilder.append("1. Объем: минимум 600-900 слов\n");
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
@@ -2127,7 +2226,7 @@ public class MarketingAnalysisService {
promptBuilder.append(
" - Если в JSON отсутствуют channels/channelsPotential: составь список каналов как ГИПОТЕЗЫ, опираясь на нишу/регион/цель, аудиторию (audienceAnalysis), сезонность и конкурентов.\n");
promptBuilder.append(
" - Если по каналу нет чисел: явно укажи \"нет данных в JSON\" и перечисли, какие метрики нужно собрать (CPC/CPM/CTR/CR/CPL/CPA/LTV).\n");
" - Если по каналу нет чисел: делай консервативную экстраполяцию (помечай как \"Экстраполяция\") и перечисли, какие метрики нужно собрать (CPC/CPM/CTR/CR/CPL/CPA/LTV).\n");
promptBuilder.append(
" - Если channelsPotential присутствует: обязательно используй potentialIndex/priority/обоснование.\n\n");
promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
@@ -2140,6 +2239,11 @@ public class MarketingAnalysisService {
promptBuilder.append("- Рекомендации по использованию каждого канала\n");
promptBuilder.append("- Бюджетные рекомендации и ожидаемая эффективность\n");
promptBuilder.append("- Интеграция каналов в единую маркетинговую стратегию\n\n");
promptBuilder.append("КРИТИЧНО (ОБЯЗАТЕЛЬНЫЕ ТЕГИ):\n");
promptBuilder.append(
"- Ты ОБЯЗАН вставить [[CHART_CHANNELS]] и [[CHART_CHANNEL_MATRIX]] внутри текста (рядом с выводами по цифрам).\n");
promptBuilder.append(
"- [[CHART_CHANNEL_MATRIX]] относится к segmentsChannelMatrix (heatmap сегмент×канал). Используй её для причинно-следственных выводов.\n\n");
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
promptBuilder.append(
"1. Сразу после абзаца, где ты сравниваешь/ранжируешь каналы с конкретными аргументами, вставь тег [[CHART_CHANNELS]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
@@ -2147,10 +2251,16 @@ public class MarketingAnalysisService {
"2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике показывает...\"\n");
promptBuilder.append(
"3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
promptBuilder.append("ОБЯЗАТЕЛЬНЫЙ АНАЛИЗ ПО МАТРИЦЕ (SEGMENTS×CHANNELS):\n");
promptBuilder.append(
"- Вставь [[CHART_CHANNEL_MATRIX]] рядом с абзацем, где ты объясняешь поведение сегментов в каналах.\n");
promptBuilder.append(
"- Объясни на основе матрицы, почему для сегмента \"Молодежь\" приоритет Instagram выше, чем Facebook (с привязкой к значениям matrix).\n\n");
promptBuilder.append("ВАЖНО:\n");
promptBuilder.append("- Начинай сразу с заголовка \"VII. Каналы продвижения и их потенциал\"\n");
promptBuilder.append("- Не добавляй вступления или заключения\n");
promptBuilder.append("- Используй данные из полей channels, channelsPotential\n");
promptBuilder.append("- Используй audienceAnalysis.segmentsChannelMatrix (если есть) для обоснования приоритетов.\n");
promptBuilder.append("- Для каждого канала дай конкретные рекомендации\n\n");
promptBuilder.append("JSON ДАННЫЕ:\n");
promptBuilder.append(jsonString);
@@ -2179,6 +2289,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder
@@ -2186,20 +2297,25 @@ public class MarketingAnalysisService {
promptBuilder.append("ТРЕБОВАНИЯ:\n");
promptBuilder.append(
"ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
promptBuilder.append("1. Объем: минимум 500-700 слов\n");
promptBuilder.append("1. Объем: минимум 600-900 слов\n");
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
promptBuilder.append(
"- Обязательно вставь тег [[CHART_FUNNEL]] непосредственно рядом с описанием этапов/конверсий (внутри абзаца или сразу после него).\n\n");
promptBuilder.append("МАТЕМАТИКА И LaTeX (ОБЯЗАТЕЛЬНО):\n");
promptBuilder.append(
"- Все формулы в этом разделе пиши ТОЛЬКО в LaTeX.\n");
promptBuilder.append(
"- Обязательно вставь фразу: \"Вероятная прибыль рассчитывается по формуле: $$ P = V \\times CR \\times AvgCheck $$\" и используй её в расчетах.\n\n");
promptBuilder.append(
"ВНИМАНИЕ: Проверяй математическую логику. Количество покупок НЕ МОЖЕТ быть больше количества заявок. "
+ "Конверсия не может превышать 100%. Если данные в JSON противоречивы, используй логические значения "
+ "(например, конверсия 10%) и сделай пометку 'Требуется уточнение данных'.\n");
promptBuilder.append("5. РАЗДЕЛ НЕЛЬЗЯ ПРОПУСКАТЬ:\n");
promptBuilder.append(
" - Если в JSON нет funnel/conversionFunnel: построй воронку как ГИПОТЕЗУ (этапы + логика), но все числа замени на переменные (X1, X2...) и подпиши \"нет данных в JSON\".\n");
" - Если в JSON нет funnel/conversionFunnel: построй воронку как ГИПОТЕЗУ (этапы + логика), но все числа замени на переменные (X1, X2...) и пометь блок как \"Экстраполяция\".\n");
promptBuilder.append(
" - Обязательно добавь блок: какие данные нужно собрать для расчета (трафик, лиды, заявки, оплаты, CR по этапам, CPL/CPA).\n");
promptBuilder.append(
@@ -2214,6 +2330,11 @@ public class MarketingAnalysisService {
promptBuilder.append("- Как оптимизировать узкое место для увеличения общего конверсионного потока\n");
promptBuilder.append("- Ожидаемый эффект от оптимизации (количественные оценки)\n");
promptBuilder.append("- Интеграция воронки с каналами продвижения\n\n");
promptBuilder.append("ОБЯЗАТЕЛЬНЫЙ РАСЧЕТ ПО ПЕРЕХОДУ \"Интерес → Заявка\":\n");
promptBuilder.append(
"- Найди этапы \"Интерес\" и \"Заявка\" (или ближайшие по смыслу) и посчитай, сколько человек теряем: $$ Lost = V_{Interest} - V_{Lead} $$. \n");
promptBuilder.append(
"- Оцени денежную стоимость потерь через экстраполяцию (например, Lost × CR_toPurchase × AvgCheck) и интерпретируй управленчески.\n\n");
promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
promptBuilder.append(
"1. Сразу после описания этапов и ключевых выводов по узким местам вставь тег [[CHART_FUNNEL]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
@@ -2261,6 +2382,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder.append(
@@ -2317,6 +2439,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder.append(
@@ -2371,6 +2494,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendUltimateJsonPresenceInstruction(promptBuilder);
appendMandatoryJsonUsageWarning(promptBuilder);
appendPremiumConsultingStyleGuide(promptBuilder);
promptBuilder.append(
@@ -2392,6 +2516,10 @@ public class MarketingAnalysisService {
promptBuilder.append("- Бюджетные рекомендации и распределение ресурсов\n");
promptBuilder.append("- Критические точки контроля и корректировки стратегии\n\n");
promptBuilder.append("КРИТИЧНО - ПРОГНОЗ И РАСЧЕТЫ НА ОСНОВЕ ВОРОНКИ:\n");
promptBuilder.append("МАТЕМАТИКА И LaTeX (ОБЯЗАТЕЛЬНО):\n");
promptBuilder.append("- Все формулы в этом разделе пиши ТОЛЬКО в LaTeX.\n");
promptBuilder.append(
"- Обязательно вставь фразу: \"Вероятная прибыль рассчитывается по формуле: $$ P = V \\times CR \\times AvgCheck $$\".\n\n");
promptBuilder.append(
"1. Возьми воронку из strategyAnalysis.funnel (или из funnel, если strategyAnalysis отсутствует) и выполни расчеты.\n");
promptBuilder.append(
@@ -2403,7 +2531,7 @@ public class MarketingAnalysisService {
promptBuilder.append(
"3. Сделай 3 сценария: консервативный / базовый / оптимистичный (допуски только как проценты к значениям из JSON; не придумывай абсолютные значения).\n");
promptBuilder.append(
"4. Если данных для метрики нет в JSON — прямо напиши \"нет данных в JSON\" и не рассчитывай её.\n\n");
"4. Если в JSON нет явного значения для части метрики — делай аккуратную экстраполяцию (пометь как \"Экстраполяция\") и показывай допущения.\n\n");
promptBuilder.append("ВАЖНО:\n");
promptBuilder
.append("- Начинай сразу с заголовка \"XI. Итоговая стратегия: что делать в первую очередь\"\n");
@@ -3464,6 +3592,8 @@ public class MarketingAnalysisService {
String textReport = fullReportBuilder.toString();
logger.info("generateMarketingReport: Modular text report generated. Length: {} chars",
textReport != null ? textReport.length() : 0);
// Hard sanitize forbidden "no data" wording before placeholder replacement
textReport = hardSanitizeReportText(textReport, false);
// Извлекаем данные для графиков из JSON
logger.info("generateMarketingReport: Starting chart data parsing from JSON");
@@ -3496,8 +3626,11 @@ public class MarketingAnalysisService {
.replace("[[CHART_AGE_DISTRIBUTION]]", "")
.replace("[[CHART_GENDER_DISTRIBUTION]]", "")
.replace("[[CHART_CHANNELS]]", "")
.replace("[[CHART_CHANNEL_MATRIX]]", "")
.replace("[[CHART_FUNNEL]]", "")
.replace("[[CHART_SWOT]]", "");
processedTextReport = processedTextReport.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]",
"");
}
// Сохраняем данные анализа
@@ -3507,6 +3640,8 @@ public class MarketingAnalysisService {
// Очищаем текст: ошибки/отказы + "пустые" фразы про графики без реального блока
String cleanedTextReport = processedTextReport != null ? removeErrorMessages(processedTextReport) : null;
cleanedTextReport = cleanedTextReport != null ? removeDanglingChartNarration(cleanedTextReport) : null;
// Final hard sanitize: remove forbidden "no data" wording and any leftover placeholders
cleanedTextReport = cleanedTextReport != null ? hardSanitizeReportText(cleanedTextReport, true) : null;
if (cleanedTextReport != null && cleanedTextReport.trim().isEmpty()) {
cleanedTextReport = "Анализ не удалось сгенерировать.";
}
@@ -3544,7 +3679,11 @@ public class MarketingAnalysisService {
for (String roman : SECTION_ROMANS) {
String id = ROMAN_TO_ANCHOR_ID.get(roman);
String label = sectionTitles.getOrDefault(roman, "Раздел " + roman);
if ("V".equals(roman) && ("Раздел V".equals(label) || label == null || label.isBlank())) {
if ("V".equals(roman)
&& (label == null || label.isBlank()
|| "Раздел V".equals(label)
|| "V. Раздел V".equals(label)
|| label.toLowerCase(Locale.ROOT).contains("раздел v"))) {
label = "V. Анализ конкурентов";
}
markdown.append("- [").append(label).append("](#").append(id).append(")\n");
@@ -3661,8 +3800,11 @@ public class MarketingAnalysisService {
.replace("[[CHART_AGE_DISTRIBUTION]]", "")
.replace("[[CHART_GENDER_DISTRIBUTION]]", "")
.replace("[[CHART_CHANNELS]]", "")
.replace("[[CHART_CHANNEL_MATRIX]]", "")
.replace("[[CHART_FUNNEL]]", "")
.replace("[[CHART_SWOT]]", "");
// Safety net: remove any unknown/variant placeholders too
cleaned = cleaned.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", "");
cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n");
return cleaned;
}
@@ -3686,6 +3828,7 @@ public class MarketingAnalysisService {
placeholderMap.put("[[CHART_AGE_DISTRIBUTION]]", new ChartMapping("audienceAge", "audienceAge"));
placeholderMap.put("[[CHART_GENDER_DISTRIBUTION]]", new ChartMapping("audienceGender", "audienceGender"));
placeholderMap.put("[[CHART_CHANNELS]]", new ChartMapping("channelsPotential", "channelsPotential"));
placeholderMap.put("[[CHART_CHANNEL_MATRIX]]", new ChartMapping("segmentsChannelMatrix", "segmentsChannelMatrix"));
placeholderMap.put("[[CHART_FUNNEL]]", new ChartMapping("conversionFunnel", "funnel"));
placeholderMap.put("[[CHART_SWOT]]", new ChartMapping("swot", "swot"));
@@ -3781,6 +3924,8 @@ public class MarketingAnalysisService {
result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant);
}
// Final safety net: remove any leftover placeholders (known or unknown)
result = result.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", "");
result = result.replaceAll("(\\r?\\n){3,}", "\n\n");
logger.info(
@@ -3816,7 +3961,7 @@ public class MarketingAnalysisService {
romanToPlaceholders.put("IV", List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]"));
romanToPlaceholders.put("V", List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]"));
romanToPlaceholders.put("VI", List.of("[[CHART_SWOT]]"));
romanToPlaceholders.put("VII", List.of("[[CHART_CHANNELS]]"));
romanToPlaceholders.put("VII", List.of("[[CHART_CHANNELS]]", "[[CHART_CHANNEL_MATRIX]]"));
romanToPlaceholders.put("VIII", List.of("[[CHART_FUNNEL]]"));
// DEDUP RULE:
@@ -33,6 +33,7 @@ class MarketingAnalysisServiceChartPlacementTest {
placeholderMap.put("[[TABLE_COMPARISON]]", ctor.newInstance("comparisonTable", "comparisonTable"));
placeholderMap.put("[[CHART_SWOT]]", ctor.newInstance("swot", "swot"));
placeholderMap.put("[[CHART_CHANNELS]]", ctor.newInstance("channelsPotential", "channelsPotential"));
placeholderMap.put("[[CHART_CHANNEL_MATRIX]]", ctor.newInstance("segmentsChannelMatrix", "segmentsChannelMatrix"));
placeholderMap.put("[[CHART_FUNNEL]]", ctor.newInstance("conversionFunnel", "funnel"));
return placeholderMap;
}
@@ -43,6 +44,15 @@ class MarketingAnalysisServiceChartPlacementTest {
return chartsData;
}
private Map<String, Object> buildChartsDataChannelsAndMatrix() {
Map<String, Object> chartsData = new HashMap<>();
chartsData.put("channelsPotential", Map.of("labels", java.util.List.of("Instagram"), "datasets",
java.util.List.of(Map.of("label", "Потенциал", "data", java.util.List.of(80)))));
chartsData.put("segmentsChannelMatrix",
java.util.List.of(Map.of("segmentName", "Молодежь", "instagram", "high", "facebook", "medium")));
return chartsData;
}
@Test
void addMissingChartsToSections_shouldIgnoreTocListItems_andUseRealMarkdownHeading() throws Exception {
String report = ""
@@ -109,4 +119,36 @@ class MarketingAnalysisServiceChartPlacementTest {
assertEquals(report, out, "If any placeholder was processed, must not append any missing placeholders");
}
@Test
void addMissingChartsToSections_shouldInsertChannelsAndMatrixIntoSectionVII() throws Exception {
String report = ""
+ "## Оглавление\n"
+ "- [VII. Каналы продвижения и их потенциал](#section-vii)\n"
+ "\n"
+ "## VII. Каналы продвижения и их потенциал\n"
+ "Тут есть текст про каналы.\n"
+ "\n"
+ "## VIII. Воронка спроса\n"
+ "Тут есть текст про воронку.\n";
Method m = MarketingAnalysisService.class.getDeclaredMethod("addMissingChartsToSections", String.class,
Map.class,
Map.class, Set.class);
m.setAccessible(true);
String out = (String) m.invoke(
marketingAnalysisService,
report,
buildChartsDataChannelsAndMatrix(),
buildPlaceholderMap(),
new HashSet<String>());
int s7Start = out.indexOf("## VII.");
int s8Start = out.indexOf("## VIII.");
assertTrue(s7Start >= 0 && s8Start > s7Start, "Sanity check: VII before VIII");
String section7Body = out.substring(s7Start, s8Start);
assertTrue(section7Body.contains("[[CHART_CHANNELS]]"), "Must insert CHART_CHANNELS into section VII");
assertTrue(section7Body.contains("[[CHART_CHANNEL_MATRIX]]"), "Must insert CHART_CHANNEL_MATRIX into section VII");
}
}