This commit is contained in:
root
2025-12-20 19:03:24 +05:00
parent f6c50cdab7
commit a237e5fdbc
@@ -31,6 +31,14 @@ import java.util.concurrent.atomic.AtomicInteger;
@Service
public class MarketingAnalysisService {
private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. "
+ "Никогда не пиши, что данных нет, если JSON не пустой.\n\n";
private void appendMandatoryJsonUsageWarning(StringBuilder promptBuilder) {
if (promptBuilder != null) {
promptBuilder.append(AI_MANDATORY_JSON_USAGE_WARNING);
}
}
private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class);
private static final int MAX_RETRY_ATTEMPTS = 3;
@@ -1209,6 +1217,7 @@ public class MarketingAnalysisService {
}
break;
case "market":
// ОБЯЗАТЕЛЬНО: seasonality + market должны доходить до секции рынка
if (fullJson.containsKey("seasonality")) {
context.put("seasonality", fullJson.get("seasonality"));
}
@@ -1220,6 +1229,7 @@ public class MarketingAnalysisService {
// Новая структура: audienceAnalysis (ageGroups, genderDistribution, segments,
// keyTakeaways, etc.)
if (fullJson.containsKey("audienceAnalysis")) {
// ОБЯЗАТЕЛЬНО: audienceAnalysis должен доходить до секции аудитории
context.put("audienceAnalysis", fullJson.get("audienceAnalysis"));
} else if (fullJson.containsKey("audienceSegments")) {
// Backward compatibility (старая структура)
@@ -1231,6 +1241,7 @@ public class MarketingAnalysisService {
// competitorsDetails, swotCompetitors, etc.)
if (fullJson.containsKey("competitorAnalysis")) {
Object competitorAnalysisObj = fullJson.get("competitorAnalysis");
// ОБЯЗАТЕЛЬНО: competitorAnalysis должен доходить до секции конкурентов
context.put("competitorAnalysis", competitorAnalysisObj);
// Aliases для совместимости: плоские ключи для секции конкурентов
// (данные физически лежат в competitorAnalysis.*)
@@ -1410,6 +1421,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder
.append("На основе следующего JSON напиши РАЗДЕЛ I (Краткое резюме) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1459,6 +1471,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder
.append("На основе следующего JSON напиши РАЗДЕЛ II (Анализ продукта) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1513,6 +1526,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ III (Анализ рынка и сезонности) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1570,6 +1584,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ IV (Анализ целевой аудитории) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1651,6 +1666,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ V (Анализ конкурентов) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1737,6 +1753,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append("На основе следующего JSON напиши РАЗДЕЛ VI (SWOT-анализ) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
promptBuilder.append("1. Объем: минимум 500-700 слов\n");
@@ -1801,6 +1818,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ VII (Каналы продвижения и их потенциал) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1864,6 +1882,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder
.append("На основе следующего JSON напиши РАЗДЕЛ VIII (Воронка спроса) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1933,6 +1952,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ IX (Рекомендации по позиционированию и УТП) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -1987,6 +2007,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ X (Рекомендации по контенту) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -2039,6 +2060,7 @@ public class MarketingAnalysisService {
String jsonString = objectMapper.writeValueAsString(contextJson);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
appendMandatoryJsonUsageWarning(promptBuilder);
promptBuilder.append(
"На основе следующего JSON напиши РАЗДЕЛ XI (Итоговая стратегия: что делать в первую очередь) маркетингового отчета.\n\n");
promptBuilder.append("ТРЕБОВАНИЯ:\n");
@@ -2255,6 +2277,13 @@ public class MarketingAnalysisService {
@SuppressWarnings("unchecked")
Map<String, Object> ageGroupsMap = (Map<String, Object>) audienceAnalysis.get("ageGroups");
if (ageGroupsMap != null) {
// CRITICAL: отдельный ключ для графика возраста (для
// [[CHART_AGE_DISTRIBUTION]])
// Дальше он будет преобразован в Chart.js формат в transformToChartConfig()
if (!ageGroupsMap.isEmpty()) {
chartsData.put("audienceAge", ageGroupsMap);
}
List<Map<String, String>> ageGroupsList = new ArrayList<>();
for (Map.Entry<String, Object> entry : ageGroupsMap.entrySet()) {
Map<String, String> ageGroup = new HashMap<>();
@@ -2270,6 +2299,13 @@ public class MarketingAnalysisService {
Map<String, Object> genderDistributionMap = (Map<String, Object>) audienceAnalysis
.get("genderDistribution");
if (genderDistributionMap != null) {
// CRITICAL: отдельный ключ для графика гендера (для
// [[CHART_GENDER_DISTRIBUTION]])
// Дальше он будет преобразован в Chart.js формат в transformToChartConfig()
if (!genderDistributionMap.isEmpty()) {
chartsData.put("audienceGender", genderDistributionMap);
}
List<Map<String, String>> gendersList = new ArrayList<>();
for (Map.Entry<String, Object> entry : genderDistributionMap.entrySet()) {
Map<String, String> genderData = new HashMap<>();
@@ -2607,6 +2643,73 @@ public class MarketingAnalysisService {
}
}
// 3b. Audience Age (CRITICAL): прямой источник из audienceAnalysis.ageGroups
// Формат rawData.audienceAge: Map<String, Object> ("18-24"->число, ...)
if (rawData.containsKey("audienceAge") && !formattedCharts.containsKey("audienceAge")) {
Object ageObj = rawData.get("audienceAge");
if (ageObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> ageMap = (Map<String, Object>) ageObj;
if (ageMap != null && !ageMap.isEmpty()) {
List<String> labels = new ArrayList<>(ageMap.keySet());
List<Number> data = new ArrayList<>();
for (String k : labels) {
Object v = ageMap.get(k);
if (v instanceof Number) {
data.add(((Number) v).intValue());
} else {
try {
data.add(Integer.parseInt(String.valueOf(v)));
} catch (Exception e) {
data.add(0);
}
}
}
Map<String, Object> dataset = new HashMap<>();
dataset.put("label", "Возрастные группы");
dataset.put("data", data);
Map<String, Object> chart = new HashMap<>();
chart.put("labels", labels);
chart.put("datasets", List.of(dataset));
formattedCharts.put("audienceAge", chart);
}
}
}
// 3c. Audience Gender (CRITICAL): прямой источник из
// audienceAnalysis.genderDistribution
// Формат rawData.audienceGender: Map<String, Object> ("Женщины"->число, ...)
if (rawData.containsKey("audienceGender") && !formattedCharts.containsKey("audienceGender")) {
Object genderObj = rawData.get("audienceGender");
if (genderObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> genderMap = (Map<String, Object>) genderObj;
if (genderMap != null && !genderMap.isEmpty()) {
List<String> labels = new ArrayList<>(genderMap.keySet());
List<Number> data = new ArrayList<>();
for (String k : labels) {
Object v = genderMap.get(k);
if (v instanceof Number) {
data.add(((Number) v).intValue());
} else {
try {
data.add(Integer.parseInt(String.valueOf(v)));
} catch (Exception e) {
data.add(0);
}
}
}
Map<String, Object> dataset = new HashMap<>();
dataset.put("label", "Гендерное распределение");
dataset.put("data", data);
Map<String, Object> chart = new HashMap<>();
chart.put("labels", labels);
chart.put("datasets", List.of(dataset));
formattedCharts.put("audienceGender", chart);
}
}
}
// 4. Conversion Funnel (List<Map>)
if (rawData.containsKey("conversionFunnel")) {
@SuppressWarnings("unchecked")
@@ -2679,7 +2782,36 @@ public class MarketingAnalysisService {
formattedCharts.put("comparisonTable", rawData.get("comparisonTable"));
}
if (rawData.containsKey("marketShareChart")) {
formattedCharts.put("marketShareChart", rawData.get("marketShareChart"));
// CRITICAL: marketShareChart должен быть Chart.js config для вставки в
// fullAnalysis
// raw формат: List<{name,value}>. Преобразуем в {labels,datasets}
@SuppressWarnings("unchecked")
List<Map<String, Object>> marketShare = (List<Map<String, Object>>) rawData.get("marketShareChart");
if (marketShare != null && !marketShare.isEmpty()) {
List<String> labels = new ArrayList<>();
List<Number> data = new ArrayList<>();
for (Map<String, Object> item : marketShare) {
labels.add(String.valueOf(item.getOrDefault("name", "Unknown")));
Object v = item.get("value");
if (v instanceof Number) {
data.add(((Number) v).intValue());
} else {
try {
data.add(Integer.parseInt(String.valueOf(v)));
} catch (Exception e) {
data.add(0);
}
}
}
Map<String, Object> dataset = new HashMap<>();
dataset.put("label", "Доли рынка (%)");
dataset.put("data", data);
Map<String, Object> chart = new HashMap<>();
chart.put("labels", labels);
chart.put("datasets", List.of(dataset));
formattedCharts.put("marketShareChart", chart);
}
}
if (rawData.containsKey("swotCompetitors")) {
formattedCharts.put("swotCompetitors", rawData.get("swotCompetitors"));
@@ -3212,8 +3344,6 @@ public class MarketingAnalysisService {
if (!result.contains(placeholder)) {
continue;
}
processedPlaceholders.add(placeholder);
Object chartData = chartsData.get(mapping.dataKey);
if (!isEmptyChartData(chartData)) {
@@ -3222,6 +3352,9 @@ public class MarketingAnalysisService {
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) {
@@ -3291,6 +3424,8 @@ public class MarketingAnalysisService {
String result = text;
Map<String, List<String>> sectionToPlaceholders = new HashMap<>();
// NOTE: Используем regex-паттерны, чтобы совпасть с вариациями заголовков,
// которые может генерировать модель
sectionToPlaceholders.put("III\\.\\s*Анализ рынка и сезонности", List.of("[[CHART_SEASONALITY]]"));
sectionToPlaceholders.put("IV\\.\\s*Анализ целевой аудитории",
List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]"));
@@ -3298,7 +3433,9 @@ public class MarketingAnalysisService {
List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]"));
sectionToPlaceholders.put("VII\\.\\s*Каналы продвижения", List.of("[[CHART_CHANNELS]]"));
sectionToPlaceholders.put("VIII\\.\\s*Воронка спроса", List.of("[[CHART_FUNNEL]]"));
sectionToPlaceholders.put("VI\\.\\s*SWOT-анализ", List.of("[[CHART_SWOT]]"));
// SWOT может быть написан как SWOT-анализ / SWOT анализ / SWОT-анализ, поэтому
// делаем мягче
sectionToPlaceholders.put("VI\\.\\s*SWOT[-\\s]?анализ", List.of("[[CHART_SWOT]]"));
for (Map.Entry<String, List<String>> sectionEntry : sectionToPlaceholders.entrySet()) {
String sectionPattern = sectionEntry.getKey();