.
This commit is contained in:
@@ -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)\\bк\\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:
|
||||
|
||||
+42
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user