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