diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 07b6db2..ca9613f 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -639,53 +639,38 @@ public class MarketingAnalysisService { request.getBusinessNiche(), request.getRegion()); try { - // ------------------------------------------------------- - // 1. Сбор данных (Research Phase) - // ------------------------------------------------------- - // Собираем данные из Google/SerpApi, чтобы у GPT был контекст (реальные сайты, статьи) + // 1. Сбор данных (оставляем как есть, это работает нормально) Map researchPack = buildResearchPack(request); - // Пытаемся найти 3-5 реальных конкурентов через поиск для подстановки в промпт + // Получаем список конкурентов для контекста List competitorCompanies = resolveCompetitorCompaniesForBenchmarks(request, researchPack, 5); - researchPack.put("competitor_companies", competitorCompanies); - // ------------------------------------------------------- - // 2. Формирование "Идеального примера" (Structure & Benchmarks) - // ------------------------------------------------------- - // Мы создаем Java-объект с "умными" рыбами (бенчмарками), который GPT будет использовать как шаблон. - // Это гарантирует, что структура JSON будет 1-в-1 как ждет фронтенд. - MarketingAnalysisResponseV2 exampleDto = buildAnalysisV2ExampleForPrompt(request, competitorCompanies); + // 2. Формирование Промпта + // ВАЖНО: Мы больше не передаем заполненный пример (exampleDto). + // Мы передаем только структуру (Schema) и жесткие инструкции. - // Сериализуем пример в строку, чтобы вставить в промпт - String structureExampleJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(exampleDto); - - // ------------------------------------------------------- - // 3. Формирование Промпта (System & User Instructions) - // ------------------------------------------------------- String systemPrompt = buildSystemPromptForKazakhstan(); - String instruction = buildDetailedInstruction(request, structureExampleJson); + String instruction = buildDetailedInstruction(request, competitorCompanies); - // Собираем весь контекст для отправки + // Собираем контекст (запрос + найденные данные) String contextJson = objectMapper.writeValueAsString(Map.of( - "user_request", request, - "market_evidence_from_search", researchPack + "user_input", request, + "search_data", researchPack, // Данные из Google/SerpApi + "identified_competitors", competitorCompanies )); - // ------------------------------------------------------- - // 4. Вызов AI с механизмом Retry (на случай битого JSON) - // ------------------------------------------------------- + // 3. Вызов OpenAI for (int attempt = 1; attempt <= 2; attempt++) { try { - logger.info("Sending request to OpenAI (Attempt {}/2)...", attempt); - + // Используем модель с большим контекстом (gpt-4o или gpt-4-turbo) String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel( contextJson, instruction, "ru", - textModelName, // GPT-4o или o1-preview + "gpt-4o", // Обязательно мощная модель для аналитики systemPrompt, - 16000, // Max tokens (увеличенный лимит для детального ответа) - 180000L // Timeout (3 минуты) + 16000, + 180000L ); if (aiResponse == null || aiResponse.isBlank()) { @@ -693,120 +678,265 @@ public class MarketingAnalysisService { } String cleaned = cleanJsonFromMarkdown(aiResponse); - if (cleaned != null) { - cleaned = cleaned.replace("\uFEFF", "").trim(); - } MarketingAnalysisResponseV2 parsed = objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class); - replacePlaceholderCompanyKeysIfNeeded(parsed, competitorCompanies); - logger.info("<<< Analysis V2 generated successfully."); + // Финальная валидация (если вдруг вернул фитнес данные - можно тут добавить проверку) return parsed; } catch (Exception e) { logger.error("JSON Parsing/Generation failed on attempt {}: {}", attempt, e.getMessage()); - if (attempt == 2) throw e; // Если второй раз не вышло — пробрасываем ошибку - Thread.sleep(2000); // Пауза перед повтором + if (attempt == 2) throw e; + Thread.sleep(2000); } } - throw new IllegalStateException("Failed to generate valid analysis after retries."); + throw new IllegalStateException("Failed to generate analysis."); } catch (Exception e) { - logger.error("generateAnalysisV2: Fatal error: {}", e.getMessage(), e); - throw new RuntimeException("Не удалось сгенерировать маркетинговый анализ V2: " + e.getMessage(), e); + logger.error("Fatal error: {}", e.getMessage(), e); + throw new RuntimeException(e); } } private String buildSystemPromptForKazakhstan() { - return KZ_MARKET_CONTEXT + - "\nФОРМАТ ОТВЕТА:\n" + - "- ТОЛЬКО валидный JSON.\n" + - "- Никакого Markdown форматирования (```json ... ```) вокруг ответа.\n" + - "- Ключи JSON должны быть в snake_case (например, market_overview, monthly_online_index).\n" + - "- Запрещено возвращать null в полях (особенно income_by_age) — делай экстраполяцию если нет точных данных.\n"; + return """ + РОЛЬ И МИССИЯ: + Ты — Ведущий Партнер (Senior Partner) стратегического консалтинга уровня Big 4 (McKinsey, BCG, PwC), специализирующийся на рынке Центральной Азии и Казахстана. + Твой клиент — требовательный инвестор, который вкладывает свои деньги. Твоя задача — дать ему сухую, аналитическую, ПРАВДИВУЮ оценку рынка, а не "воду" или выдумки. + + ТЫ МЫСЛИШЬ В ВАЛЮТЕ: KZT (Казахстанский Тенге). + + =============== 1. БАЗА ЗНАНИЙ О РЫНКЕ КАЗАХСТАНА (КОНТЕКСТ) =============== + Ты обязан использовать эти факты при генерации стратегии: + + 1. **Экосистема Kaspi.kz — это "воздух" для бизнеса:** + * Kaspi Pay/QR — стандарт де-факто для приема оплат (даже на базаре). + * Kaspi Магазин — главный маркетплейс для электроники, дома, одежды. + * Kaspi Red/Kredit — драйвер продаж. Люди покупают в рассрочку даже кроссовки и еду. Если чек > 10 000 KZT, рассрочка обязательна. + + 2. **Поисковое поведение:** + * Google — для "горячего" спроса и B2B услуг ("купить бетон", "юрист Алматы"). + * 2GIS — ГЛАВНЫЙ поисковик для офлайн-точек (рестораны, СТО, салоны). Отзывы там важнее, чем на сайте. + * Instagram — это "второй Google". Люди ищут товары и услуги через поиск в Инсте. Визуал критичен. + * TikTok — главный источник дешёвого охвата и виральности, но конверсия в продажу холоднее. + + 3. **Региональная специфика (География):** + * *Алматы:* Высокая платежеспособность, креативный класс, высокая конкуренция, пробки (важна доставка). + * *Астана:* Госсектор, клерки, холодно (сезонность!), высокий чек, но население меньше Алматы. + * *Шымкент/Юг:* Традиционный уклад, важны семейные связи, "сарафанное радио", торговля, гостеприимство. Высокая чувствительность к цене. + * *Запад (Атырау, Актау):* Нефтяной сектор, высокие зарплаты, но дорогая логистика. + + 4. **Экономика:** + * Реальная инфляция потребительской корзины: ~10-15%. + * Средняя ЗП: официально ~350-400к, медианная ~250к. Учитывай это при оценке емкости рынка! + * Язык: Двуязычие. Реклама на казахском языке растет в эффективности (особенно на Юге и Западе). + + =============== 2. ПРОТОКОЛ ЗАЩИТЫ ОТ ШАБЛОНОВ (ANTI-HALLUCINATION) =============== + СТРОГО ЗАПРЕЩЕНО: + 1. Использовать кейс "Фитнес" (1Fit, Sport Zal), если пользователь спрашивает про другую нишу. + 2. Писать "+18.5% YoY", если у тебя нет данных. Генерируй реалистичный тренд для КОНКРЕТНОЙ ниши (для хлеба это +2%, для IT +25%). + 3. Выдумывать несуществующие законы РК. + 4. Оставлять поля `null`. Если данных нет — используй метод Ферми (логическую оценку) и пиши "Оценочно ~X". + + =============== 3. АНАЛИТИЧЕСКИЙ ФРЕЙМВОРК (КАК ДУМАТЬ) =============== + Перед генерацией JSON, прогони запрос через этот алгоритм: + + ШАГ 1: Пойми юнит-экономику ниши. + - Если это "Недвижимость": LTV = 1-2 сделки, CAC = 100-500$+, цикл = месяцы. + - Если это "Доставка пиццы": LTV = высокий (годами), CAC = дешевый, цикл = минуты. + - Если это "Одежда": Важна сезонность, возвраты, визуальный контент. + + ШАГ 2: Определи конкурентов. + - Не пиши "Сетевые лидеры". Назови реальные бренды в KZ, если знаешь (Sulpak, Technodom для техники; Magnum для еды). Если не знаешь — пиши "Локальные игроки малого бизнеса". + + ШАГ 3: Рассчитай метрики. + - ROI 250% возможен не везде. В стройке маржа меньше, в инфобизнесе больше. Адаптируй цифры! + + =============== 4. ТЕХНИЧЕСКИЙ ФОРМАТ ОТВЕТА =============== + 1. Выдай ТОЛЬКО чистый JSON. Без ```json ... ```. + 2. Все ключи в `snake_case`. + 3. Поле `income_by_age` должно содержать ЧИСЛА (Integer), а не строки. + 4. Структура JSON должна строго соответствовать запрошенному примеру (но с ТВОИМИ данными). + + Твоя репутация зависит от точности цифр и глубины инсайтов. Действуй. + """; } - private String buildDetailedInstruction(MarketingAnalysisRequest request, String structureExampleJson) { - String regionsList; - if (request.getRegion() != null && !request.getRegion().isEmpty()) { - regionsList = String.join(", ", request.getRegion()); - } else { - regionsList = "Алматы, Астана, Шымкент, Караганда"; - } + private String buildDetailedInstruction(MarketingAnalysisRequest request, List competitors) { + String niche = request.getBusinessNiche(); + String region = (request.getRegion() != null && !request.getRegion().isEmpty()) + ? String.join(", ", request.getRegion()) + : "Казахстан"; - // Логика возрастных групп - String ageRangesInstruction = ""; - if (request.getTargetAudience() != null && request.getTargetAudience().containsKey("ageRanges")) { - @SuppressWarnings("unchecked") - List ranges = (List) request.getTargetAudience().get("ageRanges"); - if (ranges != null && !ranges.isEmpty()) { - ageRangesInstruction = "В полях 'age_structure' и 'income_by_age' используй СТРОГО только эти возрастные группы: " + String.join(", ", ranges) + "."; + return """ + Твоя задача — провести глубокий маркетинговый анализ для ниши: "%s" в регионе: "%s". + + ТЫ ОБЯЗАН ИСПОЛЬЗОВАТЬ ПРЕДОСТАВЛЕННЫЕ ДАННЫЕ ИЗ 'search_data'. + ЕСЛИ ДАННЫХ НЕТ — ИСПОЛЬЗУЙ ЛОГИКУ И ОБЩИЕ ЭКОНОМИЧЕСКИЕ ПОКАЗАТЕЛИ КАЗАХСТАНА (Инфляция, ВВП, Сезонность этой ниши). + + ЗАПРЕТ: + 1. ЗАПРЕЩЕНО использовать примеры из фитнеса, если ниша не фитнес. + 2. ЗАПРЕЩЕНО писать "+18.5%% YoY", если это не подтверждено данными. Генерируй реалистичные цифры для ЭТОЙ ниши. + 3. Не возвращай Markdown. Только чистый JSON. + + СТРУКТУРА ОТВЕТА (строго следуй этому формату JSON, поля должны быть заполнены аналитикой): + + { + "report_title": "Стратегический анализ рынка: [Название Ниши]", + "sections": { + "market_overview": { + "title": "Обзор рынка", + "metrics": { + "online_demand_index": "Оцени спрос от 1 до 10 (строка, например 'Высокий (8/10)') на основе популярности ниши.", + "online_potential": "Трендовое описание (например, 'Стабильный рост' или 'Сезонный спад').", + "demand_growth_rate_yoy": "Процент роста за год (реалистичный для %s, например +5%% для хлеба, +30%% для AI).", + "seasonality_coefficient_spring": "Коэффициент (например, 1.2). Учитывай, когда сезон у %s.", + "seasonality_range": "Месяцы высокого сезона (например, 'Май - Август' для кондиционеров)." + }, + "monthly_online_index": { + // Заполни Double значениями от 0.5 до 1.5 для каждого месяца (january...december). + // ВАЖНО: Эти цифры должны отражать РЕАЛЬНУЮ сезонность ниши "%s". + }, + "geography": { + // Map. Ключ - город Казахстана, Значение - индекс интереса (0-100). + // Учитывай регион "%s". Если регион вся страна - укажи топ городов. + }, + "conclusion": "Аналитический вывод о состоянии рынка (2-3 предложения)." + }, + "target_audience": { + "title": "Портрет целевой аудитории", + "metrics": { + "total_audience_size": "Оценка кол-ва потенциальных клиентов (число).", + "leading_segment_share": "Кто основной покупатель? (Пол/Возраст).", + "above_average_income_share": "Доля аудитории с доходом выше среднего (%%)." + }, + "income_by_age": { + // Map. Средний доход в KZT для групп: '18-24', '25-34', '35-44', '45+'. + // Используй реальные данные по зарплатам в Казахстане. + }, + "segments": [ + // List of objects {name, share}. Сегментируй аудиторию этой ниши (например 'Новички', 'Профи' или 'B2B', 'B2C'). + ], + "age_structure": { + // Map. Распределение по возрастам в процентах. + }, + "conclusion": "Вывод о портрете клиента." + }, + "competitive_environment": { + "title": "Конкурентный ландшафт", + "metrics": { + "leader_market_share": "Оценка доли лидера рынка.", + "competition_level": "Уровень конкуренции (Низкая/Средняя/Высокая/Красный океан).", + "ad_pressure": "Давление рекламы (CPC, активность).", + "market_leader_name": "Название типа лидеров (например, 'Маркетплейсы' или 'Сетевые игроки')." + }, + "competitors": [ + // Используй список конкурентов из context: %s. + // Для каждого создай объект: + // { + // "name": "Имя", + // "website": "Сайт (если есть) или заглушка", + // "visibility_score": Число 0-100, + // "key_strengths": ["Сила 1", "Сила 2"], + // "price_segment": "Economy/Middle/Premium" + // } + ], + "conclusion": "Вывод о конкуренции." + }, + "demand_and_behavior": { + "title": "Спрос и поведение", + "metrics": { + "search_volume_growth": "Рост поисковых запросов.", + "interactions_before_purchase": "Сколько касаний нужно для покупки в нише %s (например, недвижимость - 20, еда - 1).", + "repeat_refusal_probability": "Вероятность отказа." + }, + "search_query_types": { + // Типы запросов (ключ) и их доля (значение). Например: 'Информационные', 'Транзакционные'. + }, + "decision_factors": { + // Факторы принятия решения (Цена, Качество, Отзывы) и их вес. + }, + "reasons_for_refusal": { + // Почему не покупают? (Дорого, Долгая доставка и т.д.). + }, + "conclusion": "Вывод о поведении." + }, + "customer_journey": { + "title": "Путь клиента", + "funnel_metrics": { + "ad_traffic": "Примерный трафик (строка)", + "site_visitors": "Посетители", + "leads": "Лиды (число)", + "bookings": "Заказы/Записи", + "payments": "Оплаты" + // Рассчитай воронку с реалистичной конверсией для ниши %s. + }, + "conversion_rates": { + "lead_to_booking": "%% конверсии", + "booking_to_payment": "%% конверсии", + "lead_to_payment_total": "Общая конверсия" + }, + "funnel_bottlenecks": { + "lead_to_booking": "Главная проблема на этом этапе для ниши %s", + "booking_to_payment": "Главная проблема оплаты" + }, + "conclusion": "Вывод по воронке." + }, + "acquisition_channels": { + "title": "Каналы привлечения", + "metrics": { + "search_traffic_share": "%% поиска", + "average_lead_cost": "Стоимость лида в KZT (CPL). Реалистичная для ниши!", + "growth_potential": "Какой канал растет?" + }, + "channel_shares": { + // Доли каналов (Instagram, Google, TikTok, Kaspi). + // ДЛЯ B2B - LinkedIn/Google. ДЛЯ B2C ТОВАРОВ - Kaspi/Instagram. + }, + "channel_efficiency": { + // Map. Оцени эффективность каналов. + }, + "conclusion": "Вывод по каналам." + }, + "marketing_economics": { + "title": "Экономика маркетинга", + "metrics": { + "customer_acquisition_cost_cac": "CAC в KZT.", + "repeat_sales_conversion": "Retention rate %%.", + "roi": "Ожидаемый ROI %%." + }, + "financial_performance": { + // Заполни объекты MonthYearValue ("month": "...", "year": "...") + // marketing_expenses, clients, cac, revenue_from_client, profit_from_client. + // Цифры должны биться с нишей (недвижимость - миллионы, носки - тысячи). + }, + "conclusion": "Финансовый вывод." + }, + "bottlenecks": { + "title": "Узкие места", + "statistics": { + // Статистика потерь (%%). + }, + "details": { + "landing": ["Проблема 1", "Проблема 2"], + "lead_management": ["Проблема 1"], + "retention": ["Проблема 1"] + }, + "conclusion": "Вывод по проблемам." + }, + "recommendations": { + "title": "Рекомендации", + "core_pillars": [ + // Список стратегий {strategy, action}. + // ДАЙ КОНКРЕТНЫЕ СОВЕТЫ ДЛЯ НИШИ %s (Например, 'Запустить Kaspi Ads' для товаров). + ], + "conclusion": "Финальный совет." } + }, + "sources": [ + // Список URL из search_data + ] } - - String template = """ - Твоя роль: Ты — Ведущий Маркетинговый Стратег (Senior Strategy Consultant) по рынку Казахстана. - Задача: Провести глубокий анализ для РЕАЛЬНОГО бизнеса (B2C/B2B). Никакой воды. Только факты, цифры и применимые советы. - - ВХОДНЫЕ ДАННЫЕ: - Ниша: {{NICHE}} - Продукт: {{PRODUCT}} - Регионы для анализа: [{{REGIONS}}] - - ================================================================================ - КРИТИЧЕСКИЕ ИНСТРУКЦИИ (KZ STANDARDS & NO NULLS): - - 0. **ЗАПРЕЩЕНО ВОЗВРАЩАТЬ NULL**: - - Каждая секция (`demand_and_behavior`, `bottlenecks`, `recommendations`) ДОЛЖНА быть заполнена. - - Если данных нет, используй экспертную оценку или бенчмарки по нише. - - НИКАКИХ ПУСТЫХ ПОЛЕЙ. - - ВСЕГДА ИСПОЛЬЗУЙ ПРАВИЛЬНЫЙ JSON, ПО ПЛАНКЕ КОТОРУЮ МЫ СКИНУЛ, И ЧТО БЫ ВСЕ ПРАВАИЛЬНО - - 1. **ГЕОГРАФИЯ (Market Overview):** - - Ты ОБЯЗАН включить в анализ (`geography`) ТОЛЬКО указанные города: [{{REGIONS}}]. - - Если указан "Весь Казахстан" или список пуст — бери Топ-5 (Алматы, Астана, Шымкент...). - - Распределение: Алматы обычно лидер (40-50%), Астана (30%), регионы меньше. - - 2. **КОНКУРЕНТЫ (Competitive Environment):** - - Используй данные из `market_evidence_from_search`. Найди 3-5 РЕАЛЬНЫХ игроков. - - **ВАЖНО:** В поле `name` пиши ТОЛЬКО БРЕНД (например, "Fitnation", а не "ТОО Fitnation Group"). Без кавычек и юр. форм. - - В поле `website` попытайся найти реальный домен или ссылку на Instagram. - - Оценка видимости (`visibility_score`) должна быть разной (например: 95, 82, 60, 45). Не ставь всем 100. - - 3. **КАНАЛЫ (Acquisition Channels):** - - Приоритет в Казахстане (B2C): **Instagram (Reels/Stories), TikTok, Kaspi (магазин/платежи), 2GIS (карты)**. - - Google Ads — дорогой канал, ставь эффективность "Medium". - - `average_lead_cost`: Пиши текстом с валютой, диапазон (напр. "2500 - 4500 KZT"). - - 4. **ЭКОНОМИКА (Marketing Economics):** - - **БЮДЖЕТ:** Рассчитывай модель для малого/среднего бизнеса (SME). - - Бюджет на маркетинг: **500 000 - 3 000 000 KZT** в месяц. НЕ БОЛЬШЕ. - - `cac` (Стоимость привлечения): Адекватная (2000 - 7000 KZT для масс-маркета услуг). - - `roi`: Реалистичный (150% - 300%). Не пиши фантастические 1000% на старте. - - 5. **ТИПЫ ДАННЫХ (Строго соблюдай типы):** - - `income_by_age`: Значения - ТОЛЬКО ЧИСЛА (Integer). Пример: 350000. (Никаких кавычек, запятых, "KZT"). - - `monthly_online_index`: Дробные числа (0.0 - 1.0). - - 6. **ИСТОЧНИКИ:** - - Собери все ссылки из `evidence` и добавь авторитетные источники (stat.gov.kz, kase.kz, forbes.kz) в массив `sources` в самом конце. - - ================================================================================ - {{AGE_INSTRUCTION}} - - ФОРМАТ ОТВЕТА: - Верни СТРОГО валидный JSON, соответствующий структуре ниже. - Замени все значения `EXAMPLE__` и `-9999` на реальные аналитические данные, специфичные для ниши {{NICHE}}. - - ЭТАЛОННАЯ СТРУКТУРА JSON (Следуй ей неукоснительно): - {{JSON_STRUCTURE}} - """; - - return template - .replace("{{NICHE}}", request.getBusinessNiche()) - .replace("{{PRODUCT}}", request.getProduct()) - .replace("{{REGIONS}}", regionsList) - .replace("{{AGE_INSTRUCTION}}", ageRangesInstruction) - .replace("{{JSON_STRUCTURE}}", structureExampleJson); + """.formatted(niche, region, niche, niche, region, niche, competitors != null ? competitors.toString() : "[]", niche, niche, niche, niche, niche); } private String buildJsonStructureExample() { return """ @@ -1004,7 +1134,7 @@ public class MarketingAnalysisService { private List resolveCompetitorCompaniesForBenchmarks(MarketingAnalysisRequest request, Map researchPack, int limit) { - int max = Math.max(3, Math.min(limit, 15)); + int max = Math.max(5, Math.min(limit, 15)); LinkedHashSet names = new LinkedHashSet<>(); if (researchPack != null) { @@ -1308,7 +1438,7 @@ public class MarketingAnalysisService { MarketingAnalysisResponseV2.MarketOverviewMetrics moMetrics = new MarketingAnalysisResponseV2.MarketOverviewMetrics(); moMetrics.setOnlineDemandIndex("Высокий (8/10)"); - moMetrics.setOnlinePotential("Растущий тренд (+15-20% YoY)"); + moMetrics.setOnlinePotential("Растущий тренд )"); moMetrics.setDemandGrowthRateYoy("+18.5%"); moMetrics.setSeasonalityCoefficientSpring("1.25 (Высокий сезон)"); moMetrics.setSeasonalityRange("Сентябрь - Май");