This commit is contained in:
root
2025-12-20 19:30:19 +05:00
parent a237e5fdbc
commit f8d8e9c235
3 changed files with 236 additions and 133 deletions
@@ -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");
}
}