From 0ba65b0de90e68512fd20394e802aa5ac6adf43a Mon Sep 17 00:00:00 2001 From: arys Date: Sun, 22 Feb 2026 16:16:17 +0500 Subject: [PATCH] fix --- .../service/MarketingAnalysisV3Service.java | 156 +++++++----------- 1 file changed, 60 insertions(+), 96 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java index bb66b0f..7f27767 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java @@ -56,6 +56,11 @@ public class MarketingAnalysisV3Service { Map researchPack = executeDeepResearch(request); saveResearchMetaData(docId, researchPack); + // Защита: Если поиск упал, но мы продолжаем, чтобы выдать хоть что-то + if (!hasValidData(researchPack)) { + log.warn("[Analysis ID: {}] SEARCH FAILED. Forcing AI fallback protocol.", docId); + } + log.info("[Analysis ID: {}] Step 2/3: Building Prompts", docId); String systemPrompt = buildKazakhstanSystemPrompt(); String userPrompt = buildDataDrivenUserPrompt(request, researchPack); @@ -75,15 +80,25 @@ public class MarketingAnalysisV3Service { } } - private String sanitizeInput(String input) { - if (input == null || input.isBlank()) { - return ""; + private boolean hasValidData(Map researchPack) { + if (researchPack == null || researchPack.isEmpty()) return false; + for (Map.Entry entry : researchPack.entrySet()) { + if (entry.getKey().equals("generatedAt")) continue; + Object value = entry.getValue(); + if (value instanceof Map) { + Map mapValue = (Map) value; + if ("OK".equals(mapValue.get("status"))) { + Object items = mapValue.get("items"); + if (items instanceof List && !((List) items).isEmpty()) return true; + } + } } - return input.trim() - .replaceAll("\\s+", " ") - .replaceAll("(?i)залл\\b", "зал") - .replaceAll("(?i)салонн\\b", "салон") - .replaceAll("(?i)бизнес\\b", "бизнес"); + return false; + } + + private String sanitizeInput(String input) { + if (input == null || input.isBlank()) return ""; + return input.trim().replaceAll("\\s+", " ").replaceAll("(?i)залл\\b", "зал").replaceAll("(?i)салонн\\b", "салон").replaceAll("(?i)бизнес\\b", "бизнес"); } private Map executeDeepResearch(MarketingAnalysisV3Request request) { @@ -101,9 +116,7 @@ public class MarketingAnalysisV3Service { String geoContext = String.join(" ", cities); String topic = (niche + " " + product).trim(); - if (topic.isEmpty()) { - topic = sanitizeInput(request.getProductDescription()); - } + if (topic.isEmpty()) topic = sanitizeInput(request.getProductDescription()); String kzSites = "(site:stat.gov.kz OR site:kapital.kz OR site:kursiv.media OR site:forbes.kz OR site:ranking.kz)"; String retailSites = "(site:2gis.kz OR site:kaspi.kz OR site:kolesa.kz OR site:krisha.kz OR site:chocofood.kz OR site:instagram.com)"; @@ -117,10 +130,8 @@ public class MarketingAnalysisV3Service { queries.parallelStream().forEach(q -> { String safeMongoKey = q.replace(".", "_").replace("$", ""); try { - log.debug("Executing search query: {}", q); pack.put(safeMongoKey, searchService.search(q)); } catch (Exception e) { - log.warn("Search failed for query: {}. Error: {}", q, e.getMessage()); pack.put(safeMongoKey, Map.of("error", e.getMessage(), "status", "ERROR")); } }); @@ -136,48 +147,32 @@ public class MarketingAnalysisV3Service { while (attempts < maxAttempts) { try { - log.info("[Analysis ID: {}] AI Generation attempt {}/{}", docId, attempts + 1, maxAttempts); - String response = aiService.generateWithInstructionWithModel( - "{}", - userPrompt, - "ru", - highIntelligenceModel, - systemPrompt, - 16000, - 240000L - ); - + String response = aiService.generateWithInstructionWithModel("{}", userPrompt, "ru", highIntelligenceModel, systemPrompt, 16000, 240000L); String cleaned = cleanJson(response); if (cleaned != null && cleaned.startsWith("{") && cleaned.endsWith("}")) { objectMapper.readTree(cleaned); return cleaned; } else { lastError = "Response is not a valid JSON structure"; - log.warn("[Analysis ID: {}] Invalid JSON received from AI", docId); } - } catch (JsonProcessingException e) { - lastError = "JSON Parse Error: " + e.getMessage(); - log.warn("[Analysis ID: {}] Failed to parse AI JSON response", docId); } catch (Exception e) { lastError = "API Error: " + e.getMessage(); - log.warn("[Analysis ID: {}] AI API Call failed: {}", docId, e.getMessage()); } attempts++; try { Thread.sleep(3000L * attempts); } catch (InterruptedException ignored) {} } - throw new RuntimeException("Failed to generate valid JSON after " + maxAttempts + " attempts. Last error: " + lastError); + throw new RuntimeException("Failed to generate valid JSON: " + lastError); } private MarketingAnalysisV3Result parseAndValidateResult(String json) throws Exception { ObjectMapper safeMapper = this.objectMapper.copy(); + // ВАЖНО: Мы игнорируем старые поля, которые ИИ может по привычке вернуть safeMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return safeMapper.readValue(json, MarketingAnalysisV3Result.class); } private String cleanJson(String response) { - if (response == null || response.trim().isEmpty()) { - return null; - } + if (response == null || response.trim().isEmpty()) return null; String cleaned = response.trim(); int firstBrace = cleaned.indexOf("{"); int lastBrace = cleaned.lastIndexOf("}"); @@ -200,10 +195,8 @@ public class MarketingAnalysisV3Service { repository.findById(id).ifPresent(doc -> { doc.setResearchMetaData(researchPack); repository.save(doc); - log.debug("[Analysis ID: {}] Research metadata successfully saved to DB", id); }); } catch (Exception e) { - log.error("[Analysis ID: {}] CRITICAL: Failed to save research metadata to MongoDB. Check for invalid characters in keys. Error: {}", id, e.getMessage()); throw e; } } @@ -236,18 +229,15 @@ public class MarketingAnalysisV3Service { private String buildKazakhstanSystemPrompt() { return """ - РОЛЬ: Ты — Chief Data Officer и Senior Стратег Big 4 (PwC, McKinsey), эксперт по рынку Казахстана. - Твоя задача — не просто сгенерировать JSON, а подготовить ИДЕАЛЬНЫЙ датасет для интерактивного аналитического дашборда на фронтенде (React/Vue). - От твоих данных будут строиться реальные графики (Radar, Scatter, Pie, Line charts). Данные должны быть математически корректны и логически связаны. + РОЛЬ: Ты — Chief Data Officer и Senior Стратег Big 4. + Твоя задача — сгенерировать ИДЕАЛЬНЫЙ датасет для дашборда. - КРИТИЧЕСКИЕ ПРАВИЛА (ANTI-HALLUCINATION PROTOCOL): - 1. АБСОЛЮТНАЯ РУСИФИКАЦИЯ: Все статусы, уровни, тренды и перечисления (businessStage, competitionLevel, demandTrend, intensityLabel) СТРОГО НА РУССКОМ ЯЗЫКЕ. Никаких 'JUST_STARTING', 'HIGH', 'NONE'. - 2. ГЕОГРАФИЯ И ГОРОДА: Ты ОБЯЗАН включить КАЖДЫЙ город, переданный в запросе, в блоки аналитики (1_market_landscape и 2_geo_structure). Пропуск даже одного города — критический провал системы. - 3. КОНКУРЕНТЫ: В блоке `3_competitor_map` ОБЯЗАНО быть строго от 5 до 10 реально существующих компаний из SEARCH EVIDENCE. Ровно 3 — ЗАПРЕЩЕНО. - 4. ЧИСЛА, А НЕ СЛОВА: Если фронт ждет int или double (например, avgMonthlySearches, avgCycleDays), выдавай СТРОГО числа. Никаких слов "высокая" или "короткий". - 5. НИКАКИХ NULL И ПУСТОТ: Заполни абсолютно все поля. Используй экстраполяцию рынка РК (Kaspi, 2GIS, менталитет), если точных данных нет. - 6. ЛОГИКА ГРАФИКОВ: Суммы процентов (например, в content_profile или starDistribution) должны давать ровно 100%. - 7. РЕАЛИСТИЧНОСТЬ ДАННЫХ (КРИТИЧНО): Избегай круглых чисел и ленивых средних значений (типа ровно 50% или везде 0). Генерируй реалистичный математический разброс для показателей, основываясь на данных парсинга. + КРИТИЧЕСКИЕ ПРАВИЛА (СМЕРТЕЛЬНО ВАЖНО): + 1. НОВЫЕ КЛЮЧИ JSON: Используй ТОЛЬКО ключи из эталонного JSON ниже. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО использовать старые ключи (avgTextLength, ctaFrequency, promoFrequency и т.д.). Используй ТОЛЬКО новые (avgTextLengthCharacters, ctaUsagePercent, promoUsagePercent). + 2. АБСОЛЮТНО НИКАКИХ NULL: Заполни КАЖДОЕ поле JSON. Если точной цифры нет — сгенерируй реалистичную оценку. НИ ОДНО ЗНАЧЕНИЕ НЕ ДОЛЖНО БЫТЬ null. + 3. ПРОТОКОЛ ВЫЖИВАНИЯ: Если SEARCH EVIDENCE пуст или содержит ошибки, ТЕБЕ ЗАПРЕЩАЕТСЯ писать об этом. Ты ОБЯЗАН сгенерировать 100% валидный JSON, используя метод Ферми и знания рынка. + 4. РУСИФИКАЦИЯ: Все текстовые значения (businessStage, demandTrend) СТРОГО на русском языке. + 5. ГЕОГРАФИЯ: Ты ОБЯЗАН включить КАЖДЫЙ город, переданный в запросе, в блоки 1_market_landscape и 2_geo_structure. """; } @@ -273,55 +263,29 @@ public class MarketingAnalysisV3Service { РАЗВЕДДАННЫЕ (SEARCH EVIDENCE): %s - ОБЯЗАТЕЛЬНЫЕ ЦЕЛЕВЫЕ ГОРОДА (СТРОГО ИСПОЛЬЗУЙ ВСЕ ГОРОДА В АНАЛИЗЕ!): %s + ЦЕЛЕВЫЕ ГОРОДА: %s - ИНСТРУКЦИИ ДЛЯ ПОДГОТОВКИ ДАННЫХ К ДАШБОРДУ (СТРОГО): + ИНСТРУКЦИИ (СТРОГО СОБЛЮДАТЬ НОВЫЕ КЛЮЧИ БЕЗ ЕДИНОГО NULL): - [0_executive_summary] -> Виджеты KPI - - Все текстовые поля (businessStage, geography, competitionLevel, demandTrend) СТРОГО НА РУССКОМ. - - averageNicheEr: верни реалистичное число (double, например 0.045). + [4_content_profile] + - avgTextLengthCharacters: число (int). + - ctaUsagePercent: процент (double). - [1_market_landscape] -> Pie Chart (Платформы) и Heatmap (Города) - - Распредели игроков по платформам адекватно. - - В cityDistribution ОБЯЗАТЕЛЬНО укажи распределение по КАЖДОМУ целевому городу. + [6_reputation_analysis] + - avgOwnerResponseTimeHours: часы (int). - [2_geo_structure] -> Сравнительная гистограмма - - В cityComparison ОБЯЗАНЫ быть ВСЕ целевые города с метриками. + [7_behavioral_pattern] + - promoUsagePercent, directBookingPercent, priceVisibilityPercent: (double). + - avgCycleDays: (int). - [3_competitor_map] -> Scatter Plot (Пузырьковая диаграмма) - - СТРОГО 5-10 РЕАЛЬНЫХ конкурентов. Меньше 5 недопустимо. - - ER, followers, postsPerMonth должны быть числовыми и реалистичными. + [8_search_demand] + - avgMonthlySearches: точное число запросов (int). - [4_content_profile] -> Donut Chart - - Сумма всех Percent строго 100.0. Сделай реалистичный разброс (например 35, 20, 15, 25, 5). - - avgTextLengthCharacters: верни число символов (int, например 350). - - ctaUsagePercent: процент постов с призывом (double, например 85.5). + [9_user_positioning] + - Если DTO smmStatus == "NONE": mode="BENCHMARK", userMetrics=0, marketMetrics=реальные числа. + - Если DTO smmStatus != "NONE": mode="COMPARISON", userMetrics=оценка, marketMetrics=реальные числа. - [5_competition_intensity] -> Gauge Chart (Спидометр) - - ciiIndex: 0-100. - - intensityLabel: СТРОГО НА РУССКОМ (например "Высокая", "Свободный рынок"). - - [6_reputation_analysis] -> Bar Chart (Рейтинги) - - starDistribution: сумма долей строго 100.0. Имитируй реальные отзывы (много 5, немного 1, почти нет 2 и 3). - - avgOwnerResponseTimeHours: часы (int, например 2 или 24). - - [7_behavioral_pattern] -> Горизонтальный Bar Chart и Карточки - - Показатели (promoUsagePercent и т.д.) верни в процентах (double, 0.0 - 100.0). Сделай цифры разными и реалистичными. - - avgCycleDays: цикл сделки в днях (int, например 7). - - [8_search_demand] -> Line Chart (Сезонность) - - avgMonthlySearches: точное число запросов (int, например 15000). Никаких слов! - - seasonality: ровно 12 объектов. Периоды строго на русском (Январь, Февраль...). - - [9_user_positioning] -> Radar Chart (Паутинка) - ВАЖНАЯ ЛОГИКА: - - Если DTO smmStatus == "NONE": поле mode СТРОГО "BENCHMARK". userMetrics везде 0. marketMetrics заполни РЕАЛЬНЫМИ средними требованиями ниши (от 0 до 100, с разбросом, например activity 75, engagement 45, reputation 80). - - Если DTO smmStatus != "NONE": поле mode СТРОГО "COMPARISON". Оцени userMetrics на основе данных, marketMetrics — реалистичное среднее по рынку. - - strategicFocus: Напиши 3 конкретных шага (стратегический фокус). - - [10_structured_conclusions] & [11_smm_strategy_rationale] -> Текстовые выводы - - МИНИМУМ 50 СЛОВ в блоке 11. Обоснуй SMM стратегию глубоко, опираясь на конкретные числа из графиков. - - ЭТАЛОННЫЙ JSON (СТРОГО СОБЛЮДАЙ ФОРМАТ, КЛЮЧИ И ТИПЫ ДАННЫХ): + ЭТАЛОННЫЙ JSON (СКОПИРУЙ КЛЮЧИ ТОЧЬ-В-ТОЧЬ, ЗАПОЛНИ ЧИСЛАМИ, НИКАКИХ NULL): %s """.formatted(requestJson, evidenceJson, targetCities, schemaTemplate); } catch (Exception e) { @@ -333,18 +297,18 @@ public class MarketingAnalysisV3Service { return """ { "0_executive_summary": { - "businessStage": "СТРОГО НА РУССКОМ (например: 'На этапе запуска', 'Действующий бизнес')", - "geography": "СТРОГО НА РУССКОМ (например: 'Один город', 'Вся страна')", + "businessStage": "СТРОГО НА РУССКОМ", + "geography": "СТРОГО НА РУССКОМ", "activeCompetitors": 0, - "competitionLevel": "СТРОГО НА РУССКОМ (например: 'Высокий', 'Средний')", + "competitionLevel": "СТРОГО НА РУССКОМ", "averageNicheEr": 0.0, "averageRating": 0.0, - "demandTrend": "СТРОГО НА РУССКОМ (например: 'Растущий', 'Стабильный')", + "demandTrend": "СТРОГО НА РУССКОМ", "keyFigures": ["string", "string", "string"] }, "1_market_landscape": { "activePlayersByPlatform": {"Instagram": 0, "TikTok": 0, "GoogleMaps": 0}, - "cityDistribution": {"Город 1": 0, "Город 2": 0}, + "cityDistribution": {"Город 1": 0}, "nicheReputationLevel": 0.0 }, "2_geo_structure": { @@ -376,7 +340,7 @@ public class MarketingAnalysisV3Service { }, "5_competition_intensity": { "ciiIndex": 0, - "intensityLabel": "СТРОГО НА РУССКОМ (например: 'Высокая плотность')", + "intensityLabel": "СТРОГО НА РУССКОМ", "contributingFactors": ["string"] }, "6_reputation_analysis": { @@ -415,11 +379,11 @@ public class MarketingAnalysisV3Service { "9_user_positioning": { "marketMetrics": {"activity": 0, "engagement": 0, "video": 0, "reputation": 0, "frequency": 0}, "userMetrics": {"activity": 0, "engagement": 0, "video": 0, "reputation": 0, "frequency": 0}, - "mode": "BENCHMARK или COMPARISON", + "mode": "BENCHMARK", "strategicFocus": ["string", "string", "string"] }, "10_structured_conclusions": ["string", "string", "string"], - "11_smm_strategy_rationale": "МИНИМУМ 50 СЛОВ. Экспертный текст, связывающий данные графиков с выбором SMM." + "11_smm_strategy_rationale": "МИНИМУМ 50 СЛОВ. Обоснование стратегии." } """; }