From e7c0a1e0d64e4c40b2112b749a6742b661d246bc Mon Sep 17 00:00:00 2001 From: arys Date: Sat, 17 Jan 2026 14:55:27 +0500 Subject: [PATCH] hit --- .../controller/MarketingController.java | 3 - .../service/MarketingAnalysisService.java | 138 +++++++++--------- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/src/main/java/kz/konturai/parser/controller/MarketingController.java b/src/main/java/kz/konturai/parser/controller/MarketingController.java index b1c2ebf..dec9b2d 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingController.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingController.java @@ -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 анализ не создался } } diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index f6004af..a2b8f66 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -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 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 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 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()) {