.
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user