diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java index c814b97..44b33fc 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisV3Service.java @@ -75,12 +75,23 @@ public class MarketingAnalysisV3Service { } } + 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) { Map pack = new ConcurrentHashMap<>(); List queries = new ArrayList<>(); - String niche = request.getBusinessNiche() != null ? request.getBusinessNiche() : ""; - String product = request.getProductName() != null ? request.getProductName() : ""; + String niche = sanitizeInput(request.getBusinessNiche()); + String product = sanitizeInput(request.getProductName()); List cities = request.getPromotionCities() != null && !request.getPromotionCities().isEmpty() ? request.getPromotionCities() @@ -90,7 +101,9 @@ public class MarketingAnalysisV3Service { String geoContext = String.join(" ", cities); String topic = (niche + " " + product).trim(); - if (topic.isEmpty()) topic = 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)"; @@ -102,10 +115,7 @@ public class MarketingAnalysisV3Service { queries.add(String.format("кейс продвижение SMM %s казахстан", niche)); queries.parallelStream().forEach(q -> { - // КРИТИЧЕСКИ ВАЖНО: MongoDB не поддерживает точки (.) в названиях ключей! - // Поэтому заменяем точки на нижнее подчеркивание перед сохранением в Map. String safeMongoKey = q.replace(".", "_").replace("$", ""); - try { log.debug("Executing search query: {}", q); pack.put(safeMongoKey, searchService.search(q)); @@ -139,7 +149,6 @@ public class MarketingAnalysisV3Service { String cleaned = cleanJson(response); if (cleaned != null && cleaned.startsWith("{") && cleaned.endsWith("}")) { - // Проверка валидности JSON objectMapper.readTree(cleaned); return cleaned; } else { @@ -160,7 +169,6 @@ public class MarketingAnalysisV3Service { } 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); @@ -196,7 +204,7 @@ public class MarketingAnalysisV3Service { }); } 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; // Пробрасываем дальше, чтобы уйти в статус FAILED + throw e; } } @@ -233,10 +241,11 @@ public class MarketingAnalysisV3Service { КРИТИЧЕСКИЕ ПРАВИЛА (ANTI-HALLUCINATION PROTOCOL): 1. ТОЛЬКО РЕАЛЬНЫЕ ДАННЫЕ: Вся аналитика строится на переданном SEARCH EVIDENCE. - 2. РЕАЛЬНЫЕ КОНКУРЕНТЫ: В блоке `3_competitor_map` ОБЯЗАНО быть от 3 до 5 реально существующих компаний, найденных в SEARCH EVIDENCE (например "Invictus", "Sulpak", "Korean House"). КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО писать "Конкурент 1", "Компания А", "Пример". - 3. ОЦИФРОВКА: В Казахстане все измеряется в KZT. Приложение Kaspi.kz и 2GIS — основа рынка. Учитывай это в анализе. - 4. СВЯЗЬ СО СТРАТЕГИЕЙ: Твой анализ — это фундамент для будущего SMM-Scoring Model (Entry/Authority/Trust/Conversion). Анализируй данные так, чтобы выявить доминирующий барьер аудитории. - 5. НИКАКИХ NULL: Заполни абсолютно все поля JSON. Если точной цифры нет, примени метод Ферми и сделай экстраполяцию, характерную для рынка РК. + 2. РЕАЛЬНЫЕ КОНКУРЕНТЫ: В блоке `3_competitor_map` ОБЯЗАНО быть строго от 5 до 10 РЕАЛЬНЫХ компаний из предоставленных данных. Ровно 3 — ЗАПРЕЩЕНО. Использовать плейсхолдеры — ЗАПРЕЩЕНО. + 3. ПОЛНАЯ РУСИФИКАЦИЯ: Все значения статусов, уровней и трендов (businessStage, competitionLevel, demandTrend, intensityLabel, status) СТРОГО на русском языке. Запрещено копировать английские слова (JUST_STARTING, HIGH, STABLE, NONE) из DTO. Переводи их по смыслу (Например: 'Запуск', 'Высокая', 'Стабильный', 'Отсутствует'). + 4. ОЦИФРОВКА: В Казахстане все измеряется в KZT. Приложение Kaspi.kz и 2GIS — основа рынка. Учитывай это в анализе. + 5. СВЯЗЬ СО СТРАТЕГИЕЙ: Твой анализ — это фундамент для будущего SMM-Scoring Model. + 6. НИКАКИХ NULL: Заполни абсолютно все поля JSON. Если точной цифры нет, примени метод Ферми и сделай экстраполяцию, характерную для рынка РК. """; } @@ -246,6 +255,13 @@ public class MarketingAnalysisV3Service { String evidenceJson = objectMapper.writeValueAsString(researchPack); String schemaTemplate = getJsonStructureTemplate(); + List targetCitiesList = request.getPromotionCities() != null && !request.getPromotionCities().isEmpty() + ? request.getPromotionCities() + : (request.getPresenceCities() != null && !request.getPresenceCities().isEmpty() + ? request.getPresenceCities() + : List.of("Казахстан")); + String targetCities = String.join(", ", targetCitiesList); + return """ СФОРМИРУЙ ОТЧЕТ "MARKETING ANALYSIS V4.0" ДЛЯ РЫНКА КАЗАХСТАНА. @@ -258,19 +274,19 @@ public class MarketingAnalysisV3Service { ИНСТРУКЦИИ ПО СЕКЦИЯМ (СТРОГО): [0_executive_summary] - - activeCompetitors: Реальное количество найденных в поиске игроков. + - activeCompetitors: Реальное количество найденных в поиске игроков (минимум 5). - keyFigures: 5-7 мощных метрик. Обязательно укажи оценку объема рынка/спроса. - [1_market_landscape] - - demandDynamics: 12 объектов. Поле "period" СТРОГО на русском (Январь, Февраль...). "value" отражает сезонность. + [1_market_landscape] и [2_geo_structure] + - ОБЯЗАТЕЛЬНО выведи метрики в этих блоках для КАЖДОГО из этих городов: %s. Пропуск любого города — критическая ошибка. + - demandDynamics: 12 объектов. Поле "period" СТРОГО на русском (Январь, Февраль...). [3_competitor_map] - - ВЫВЕДИ 3, 4 ИЛИ 5 РЕАЛЬНЫХ КОНКУРЕНТОВ. - - Вытащи их сильные/слабые стороны из отзывов в SEARCH EVIDENCE. + - ОБЯЗАНО быть строго от 5 до 10 РЕАЛЬНЫХ компаний. Ровно 3 — ЗАПРЕЩЕНО. - Рассчитай реалистичный ER (от 0.01 до 0.08). [4_content_profile] - - Сумма всех ContentPercent должна быть ровно 100.0. Распредели в зависимости от ниши (если визуал важен - демо/видео выше; если B2B - экспертность выше). + - Сумма всех ContentPercent должна быть ровно 100.0. [5_competition_intensity] - ciiIndex: 0-100. Оцени по плотности выдачи 2GIS и Google в EVIDENCE. @@ -279,14 +295,17 @@ public class MarketingAnalysisV3Service { - Отрази привычки КЗ: Kaspi Red, WhatsApp, чувствительность к скидкам. [9_user_positioning] - - Если DTO smmStatus == "NONE", радар-чарт по нулям. + - Если DTO smmStatus == "NONE", радар-чарт по нулям, а статус напиши "Отсутствует". [10_structured_conclusions] - - Подведи итог: Какой фактор доминирует? (Быстрая покупка, долгий цикл, перегретый рынок, или запуск). Это нужно для будущей скоринг-модели. + - Подведи итог: Какой фактор доминирует? + + [11_smm_strategy_rationale] + - МИНИМУМ 50 СЛОВ. Обоснуй стратегию на основе реальных данных из блоков 5 и 7. Свяжи барьеры аудитории с выбором SMM. ЭТАЛОННЫЙ JSON: %s - """.formatted(requestJson, evidenceJson, schemaTemplate); + """.formatted(requestJson, evidenceJson, targetCities, schemaTemplate); } catch (Exception e) { throw new RuntimeException("Error building prompt", e); } @@ -296,13 +315,13 @@ public class MarketingAnalysisV3Service { return """ { "0_executive_summary": { - "businessStage": "string", - "geography": "string", + "businessStage": "СТРОГО НА РУССКОМ (например 'На старте', 'Действующий бизнес')", + "geography": "СТРОГО НА РУССКОМ (например 'Несколько городов', 'Один город')", "activeCompetitors": 0, - "competitionLevel": "string", + "competitionLevel": "СТРОГО НА РУССКОМ (например 'Высокий', 'Средний')", "averageNicheEr": "string", "averageRating": 0.0, - "demandTrend": "string", + "demandTrend": "СТРОГО НА РУССКОМ (например 'Стабильный', 'Растущий')", "keyFigures": ["string", "string", "string"] }, "1_market_landscape": { @@ -353,7 +372,7 @@ public class MarketingAnalysisV3Service { }, "5_competition_intensity": { "ciiIndex": 0, - "intensityLabel": "string", + "intensityLabel": "СТРОГО НА РУССКОМ (например 'Высокая', 'Низкая')", "contributingFactors": ["string"] }, "6_reputation_analysis": { @@ -368,11 +387,11 @@ public class MarketingAnalysisV3Service { "bookingFrequency": "string", "dmRequestFrequency": "string", "priceVisibility": "string", - "avgCycleDuration": "string", + "avgCycleDuration": "СТРОГО числовой диапазон, например '5-7 дней', НИКАКИХ абстрактных слов", "commonCta": ["string"] }, "8_search_demand": { - "avgFrequency": "string", + "avgFrequency": "СТРОГО числовой диапазон, например '10000-15000', НИКАКИХ абстрактных слов", "seasonality": [ {"period": "Январь", "value": 0.0}, {"period": "Февраль", "value": 0.0}, @@ -398,11 +417,11 @@ public class MarketingAnalysisV3Service { "reputation": 0, "frequency": 0 }, - "status": "string", + "status": "СТРОГО НА РУССКОМ (например 'Отсутствует', 'Активный')", "gaps": ["string"] }, "10_structured_conclusions": ["string", "string", "string"], - "11_smm_strategy_rationale": "string" + "11_smm_strategy_rationale": "МИНИМУМ 50 СЛОВ. Обосновать стратегию на основе данных конкуренции и паттернов поведения." } """; }