hit
This commit is contained in:
@@ -135,16 +135,13 @@ public class MarketingController {
|
||||
if (generateV2) {
|
||||
try {
|
||||
v2AnalysisId = marketingAnalysisService.generateAndSaveAnalysisV2(request, userId);
|
||||
// Связываем обычный анализ с v2
|
||||
analysis.setV2AnalysisId(v2AnalysisId);
|
||||
marketingAnalysisService.updateAnalysis(analysis);
|
||||
// Связываем v2 анализ с обычным
|
||||
marketingAnalysisService.linkV2AnalysisToAnalysis(v2AnalysisId, analysis.getId());
|
||||
logger.info("Created and linked v2 analysis {} to analysis {}", v2AnalysisId,
|
||||
analysis.getId());
|
||||
} catch (Exception e) {
|
||||
logger.error("Error creating v2 analysis: {}", e.getMessage(), e);
|
||||
// Продолжаем выполнение даже если v2 анализ не создался
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
|
||||
@Service
|
||||
public class MarketingAnalysisService {
|
||||
|
||||
private static final String KZ_MARKET_CONTEXT =
|
||||
"РОЛЬ: Ты — Партнер международной консалтинговой фирмы (Big 4: McKinsey/KPMG) с 50-летним опытом в маркетинге и стратегии.\n" +
|
||||
"Твоя специализация: Рынки Центральной Азии (особенно Казахстан).\n" +
|
||||
"СТИЛЬ: Уверенный, профессиональный, доказательный, без 'воды'. Используй термины (LTV, CAC, Retention, Unit-economics).\n" +
|
||||
"КОНТЕКСТ РЫНКА:\n" +
|
||||
"- Страна: Казахстан (Алматы, Астана, Шымкент - ключевые хабы).\n" +
|
||||
"- Валюта: Тенге (KZT). Все цены и доходы — только в тенге.\n" +
|
||||
"- Инфляция: Реальная потребительская инфляция ~10-12% (официальная ~8%).\n" +
|
||||
"- Поведение: Высокая роль Kaspi (QR, рассрочка 0-0-12), Instagram (витрина), WhatsApp (коммуникация).\n" +
|
||||
"- Менталитет: 'Уят' (репутация), семейные ценности, склонность к статусному потреблению, чувствительность к цене (акции).\n";
|
||||
private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. "
|
||||
+ "Никогда не пиши, что данных нет, если JSON не пустой.\n\n";
|
||||
private static final String AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION = ""
|
||||
@@ -465,21 +475,20 @@ public class MarketingAnalysisService {
|
||||
|
||||
// Prefer the requested domains but keep query broad enough to return something.
|
||||
String preferredSites = "(site:stat.gov.kz OR site:kapital.kz OR site:kursiv.media OR site:forbes.kz OR site:pwc.kz OR site:kpmg.kz)";
|
||||
// Added for real estate/auto context if needed
|
||||
String extraSites = "OR site:kolesa.kz OR site:krisha.kz OR site:market.kz OR site:hh.kz";
|
||||
|
||||
List<Object> general = new ArrayList<>();
|
||||
String query1 = topic + " рынок " + regionStr + " 2024 2025 статистика объем рост " + preferredSites;
|
||||
try {
|
||||
general.add(serperSearchService.search(query1));
|
||||
} catch (Exception e) {
|
||||
// Handle DNS/network errors and other exceptions from Serper API
|
||||
// Handle DNS/network errors... (keeping existing error handling)
|
||||
String errorType = "Unexpected error";
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
|
||||
errorType = "DNS/Network failure";
|
||||
} else if (e instanceof java.util.concurrent.TimeoutException) {
|
||||
errorType = "Timeout";
|
||||
} else if (e instanceof java.io.IOException) {
|
||||
errorType = "IO error";
|
||||
}
|
||||
else if (e instanceof java.util.concurrent.TimeoutException) errorType = "Timeout";
|
||||
else if (e instanceof java.io.IOException) errorType = "IO error";
|
||||
logger.warn("Serper API {} for query '{}': {}", errorType, query1, e.getMessage());
|
||||
general.add(new SerperSearchResult(query1, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
}
|
||||
@@ -488,74 +497,74 @@ public class MarketingAnalysisService {
|
||||
try {
|
||||
general.add(serperSearchService.search(query2));
|
||||
} catch (Exception e) {
|
||||
// Handle DNS/network errors and other exceptions from Serper API
|
||||
// Keeping existing error handling
|
||||
String errorType = "Unexpected error";
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
|
||||
errorType = "DNS/Network failure";
|
||||
} else if (e instanceof java.util.concurrent.TimeoutException) {
|
||||
errorType = "Timeout";
|
||||
} else if (e instanceof java.io.IOException) {
|
||||
errorType = "IO error";
|
||||
}
|
||||
else if (e instanceof java.util.concurrent.TimeoutException) errorType = "Timeout";
|
||||
else if (e instanceof java.io.IOException) errorType = "IO error";
|
||||
logger.warn("Serper API {} for query '{}': {}", errorType, query2, e.getMessage());
|
||||
general.add(new SerperSearchResult(query2, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
}
|
||||
|
||||
// Lightweight section followups (stored for auditing/citations). Prompts for
|
||||
// III/V/XI may further use these.
|
||||
// Lightweight section followups (stored for auditing/citations). Prompts for III/V/XI may further use these.
|
||||
Map<String, Object> followups = new LinkedHashMap<>();
|
||||
String marketQuery = topic + " объем рынка Казахстан 2024 2025 отчет " + preferredSites;
|
||||
try {
|
||||
followups.put("market", serperSearchService.search(marketQuery));
|
||||
} catch (Exception e) {
|
||||
// Handle DNS/network errors and other exceptions from Serper API
|
||||
// Keeping existing error handling
|
||||
String errorType = "Unexpected error";
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
|
||||
errorType = "DNS/Network failure";
|
||||
} else if (e instanceof java.util.concurrent.TimeoutException) {
|
||||
errorType = "Timeout";
|
||||
} else if (e instanceof java.io.IOException) {
|
||||
errorType = "IO error";
|
||||
}
|
||||
else if (e instanceof java.util.concurrent.TimeoutException) errorType = "Timeout";
|
||||
else if (e instanceof java.io.IOException) errorType = "IO error";
|
||||
logger.warn("Serper API {} for market query '{}': {}", errorType, marketQuery, e.getMessage());
|
||||
followups.put("market",
|
||||
new SerperSearchResult(marketQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
followups.put("market", new SerperSearchResult(marketQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
}
|
||||
|
||||
String competitorsQuery = topic + " конкуренты " + regionStr + " топ компании магазины";
|
||||
String competitorsQuery = topic + " конкуренты " + regionStr + " топ компании магазины цены";
|
||||
try {
|
||||
followups.put("competitors", serperSearchService.search(competitorsQuery));
|
||||
} catch (Exception e) {
|
||||
// Handle DNS/network errors and other exceptions from Serper API
|
||||
// Keeping existing error handling
|
||||
String errorType = "Unexpected error";
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
|
||||
errorType = "DNS/Network failure";
|
||||
} else if (e instanceof java.util.concurrent.TimeoutException) {
|
||||
errorType = "Timeout";
|
||||
} else if (e instanceof java.io.IOException) {
|
||||
errorType = "IO error";
|
||||
}
|
||||
else if (e instanceof java.util.concurrent.TimeoutException) errorType = "Timeout";
|
||||
else if (e instanceof java.io.IOException) errorType = "IO error";
|
||||
logger.warn("Serper API {} for competitors query '{}': {}", errorType, competitorsQuery, e.getMessage());
|
||||
followups.put("competitors",
|
||||
new SerperSearchResult(competitorsQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
followups.put("competitors", new SerperSearchResult(competitorsQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
}
|
||||
|
||||
String financeQuery = topic + " себестоимость логистика импорт курс тенге влияние 2024 2025 " + preferredSites;
|
||||
try {
|
||||
followups.put("finance", serperSearchService.search(financeQuery));
|
||||
} catch (Exception e) {
|
||||
// Handle DNS/network errors and other exceptions from Serper API
|
||||
String errorType = "Unexpected error";
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
|
||||
if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
|
||||
errorType = "DNS/Network failure";
|
||||
} else if (e instanceof java.util.concurrent.TimeoutException) {
|
||||
errorType = "Timeout";
|
||||
} else if (e instanceof java.io.IOException) {
|
||||
errorType = "IO error";
|
||||
}
|
||||
else if (e instanceof java.util.concurrent.TimeoutException) errorType = "Timeout";
|
||||
else if (e instanceof java.io.IOException) errorType = "IO error";
|
||||
logger.warn("Serper API {} for finance query '{}': {}", errorType, financeQuery, e.getMessage());
|
||||
followups.put("finance",
|
||||
new SerperSearchResult(financeQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
followups.put("finance", new SerperSearchResult(financeQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
|
||||
}
|
||||
|
||||
String incomeQuery = "средняя медианная зарплата по возрастам " + regionStr + " 2024 2025 статистика доходы населения бюро нацстатистики";
|
||||
try {
|
||||
followups.put("income", serperSearchService.search(incomeQuery));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Serper API error for income query: {}", e.getMessage());
|
||||
// Fail silently for supplementary metrics to avoid blocking main flow
|
||||
followups.put("income", new SerperSearchResult(incomeQuery, "ERROR", e.getMessage(), List.of()));
|
||||
}
|
||||
|
||||
String demandQuery = topic + " сезонность спроса динамика популярности " + regionStr + " когда покупают google trends";
|
||||
try {
|
||||
followups.put("demand", serperSearchService.search(demandQuery));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Serper API error for demand query: {}", e.getMessage());
|
||||
followups.put("demand", new SerperSearchResult(demandQuery, "ERROR", e.getMessage(), List.of()));
|
||||
}
|
||||
|
||||
pack.put("general", general);
|
||||
@@ -656,11 +665,13 @@ public class MarketingAnalysisService {
|
||||
buildAnalysisV2ExampleForPrompt(request, competitorCompanies));
|
||||
|
||||
String systemPrompt = ""
|
||||
+ "Ты — партнер Big Four (управленческий консалтинг) и senior marketing analyst.\n"
|
||||
+ "Пиши уверенно, структурно, с причинно-следственной логикой, без «воды».\n"
|
||||
+ "КРИТИЧЕСКИ ВАЖНО: Твой ответ должен быть ТОЛЬКО валидный JSON, без markdown, без ```.\n";
|
||||
+ "Ты — Партнер международной консалтинговой фирмы (Big 4) и ведущий маркетинговый стратег с 50-летним опытом.\n"
|
||||
+ "Твоя специализация: Рынок Казахстана. Ты знаешь всё про тенге, Kaspi, менталитет и реальную инфляцию.\n"
|
||||
+ "СТИЛЬ: Уверенный, профессиональный, структурный. Никакой воды. Только факты и обоснованные выводы.\n"
|
||||
+ "КРИТИЧЕСКИ ВАЖНО: Твой ответ должен быть ТОЛЬКО валидный JSON, без markdown, без ```.\n"
|
||||
+ "ЗАПРЕТ: Не выдумывай бренды (используй только найденные) и не генерируй случайные 'круглые' числа для графиков.\n";
|
||||
|
||||
// Извлекаем возрастные группы из запроса для инструкции
|
||||
// Извлекаем возрастные группы из запроса для инструкции (старая логика сохранена)
|
||||
String ageRangesInstruction = "";
|
||||
Map<String, Object> targetAudienceData = request.getTargetAudience();
|
||||
if (targetAudienceData != null && targetAudienceData.containsKey("ageRanges")) {
|
||||
@@ -670,8 +681,7 @@ public class MarketingAnalysisService {
|
||||
ageRangesInstruction = "\n"
|
||||
+ "8) КРИТИЧЕСКИ ВАЖНО для target_audience.age_structure: Используй ТОЛЬКО те возрастные группы, которые указаны в запросе (request.targetAudience.ageRanges). "
|
||||
+ "Указанные возрастные группы: " + String.join(", ", ageRanges) + ". "
|
||||
+ "НЕ добавляй возрастные группы, которых нет в запросе. "
|
||||
+ "Если в запросе указаны только \"25-34\" и \"35-44\", то в age_structure должны быть ТОЛЬКО эти две группы.\n";
|
||||
+ "НЕ добавляй возрастные группы, которых нет в запросе.\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,20 +691,19 @@ public class MarketingAnalysisService {
|
||||
+ "1) Верни ТОЛЬКО JSON (без текста до/после).\n"
|
||||
+ "2) Заполни ВСЕ вложенные поля: sections.market_overview, target_audience, competitive_environment, "
|
||||
+ "demand_and_behavior, customer_journey, acquisition_channels, marketing_economics, bottlenecks, recommendations.\n"
|
||||
+ "3) Используй реальные цифры/темпы роста/проценты из evidence (researchPack). Если конкретных цифр нет — сделай Analytical Extrapolation, "
|
||||
+ "но НИКОГДА не пиши «данных нет/не найдено/недоступно».\n"
|
||||
+ "4) Соблюдай типы:\n"
|
||||
+ " - проценты/рейтинги/коэффициенты (форматы: +N%, A/B, Kx) => string\n"
|
||||
+ " - monthly_online_index.* => number (double)\n"
|
||||
+ " - funnel_metrics.leads/bookings/payments => integer\n"
|
||||
+ " - market_overview.geography => object: ключ=город, значение=целое число (индекс интереса, 0..100)\n"
|
||||
+ "3) Используй реальные цифры из evidence (researchPack). Если конкретных цифр нет — сделай Analytical Extrapolation, "
|
||||
+ "но НИКОГДА не пиши «данных нет».\n"
|
||||
+ "4) Соблюдай типы данных (числа - number/integer, проценты - string).\n"
|
||||
+ "5) Пиши на русском языке.\n"
|
||||
+ "6) competitive_environment.digital_visibility_benchmarks и competitive_environment.competitor_strengths: "
|
||||
+ "ключи должны быть названиями РЕАЛЬНЫХ компаний/брендов (используй evidence.competitor_companies и evidence.followups.competitors). "
|
||||
+ "Запрещены ключи-заглушки: competitor_a/competitor_b/competitor_c/your_business/lead/payment/others.\n"
|
||||
+ "7) НИКОГДА не копируй значения из примера. В примере намеренно стоят плейсхолдеры, "
|
||||
+ "которые начинаются с 'EXAMPLE__' и числа -9999/-9999.0 — в твоем ответе их быть НЕ должно."
|
||||
+ "6) КОНКУРЕНТЫ: Используй РЕАЛЬНЫЕ названия из evidence.competitor_companies. Запрещены: competitor_a/b, your_business.\n"
|
||||
+ "7) НИКОГДА не копируй значения из примера 'EXAMPLE__'.\n"
|
||||
+ ageRangesInstruction
|
||||
+ "\nКРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ ПО ГРАФИКАМ (КАЗАХСТАН):\n"
|
||||
+ "A. **ДИНАМИКА СПРОСА (market_overview.monthly_online_index)**: НЕ генерируй случайные числа! Подумай о сезонности ниши в РК. Если это 'Фитнес' - пик январь/сентябрь. Если 'Шины' - октябрь/апрель. График должен быть логичным, не плоским. (0.0 - 1.0)\n"
|
||||
+ "B. **ГРАФИК ДОХОДОВ (target_audience.metrics)**: ОШИБКА - 'кривая доходности'. НУЖНО: Построить график 'Размер дохода (X) vs Возрастная категория (Y)'.\n"
|
||||
+ " - В поле `income_by_age` (или аналогичном map) укажи средний доход в ТЕНГЕ (KZT) для каждой возрастной группы из `age_structure`.\n"
|
||||
+ " - Используй данные из evidence.followups.income (Бюро нацстатистики/HH.kz). Пример: '25-34': 350000.\n"
|
||||
+ "C. **ИСТОЧНИКИ**: В конце поля `recommendations.conclusion` или `market_overview.conclusion` ОБЯЗАТЕЛЬНО добавь список использованных источников (ссылки из evidence) в формате Markdown.\n"
|
||||
+ "\nНиже пример правильной структуры (пример только для формы/полей; все значения должны быть заменены на значения из evidence):\n"
|
||||
+ exampleJson;
|
||||
|
||||
@@ -722,14 +731,13 @@ public class MarketingAnalysisService {
|
||||
+ " Верни ТОЛЬКО JSON.";
|
||||
}
|
||||
|
||||
// Для V2 всегда используется gpt-4o (textModelName), независимо от detailLevel
|
||||
String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||
contextJson,
|
||||
attemptInstruction,
|
||||
"ru",
|
||||
textModelName, // gpt-4o - всегда для V2
|
||||
textModelName,
|
||||
systemPrompt,
|
||||
15000,
|
||||
16000,
|
||||
180000L);
|
||||
|
||||
if (aiResponse == null || aiResponse.isBlank()) {
|
||||
|
||||
Reference in New Issue
Block a user