From fff887df8918544527b5ca0e585014f7faca17c4 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 14 Dec 2025 20:06:53 +0500 Subject: [PATCH] . --- TT for improvement.md | 84 +++ .../service/MarketingAnalysisService.java | 495 +++++++++++------- 2 files changed, 403 insertions(+), 176 deletions(-) create mode 100644 TT for improvement.md diff --git a/TT for improvement.md b/TT for improvement.md new file mode 100644 index 0000000..b745123 --- /dev/null +++ b/TT for improvement.md @@ -0,0 +1,84 @@ +Привет! Я хочу провести глубокий рефакторинг метода generateJsonAnalysis в классе MarketingAnalysisService.java. + +У нас нет ограничений по токенам, но есть жесткое требование к качеству и детализации данных. Текущий метод пытается получить всё за один запрос, из-за чего страдает глубина. + +ЗАДАЧА: +Перепиши метод generateJsonAnalysis так, чтобы он выполнял агрегацию данных из нескольких отдельных запросов к OpenAI. +Метод должен вызывать приватные helper-методы (создай их), каждый из которых отвечает за свою часть и возвращает Map. + +СТРУКТУРА НОВОГО МЕТОДА: + +1. Вызвать getAudienceAnalysis(request) -> возвращает секцию "audienceAnalysis" +2. Вызвать getCompetitorAnalysis(request) -> возвращает секцию "competitorAnalysis" +3. Вызвать getSeasonalityAndMarket(request) -> возвращает секцию "seasonality" и "market" +4. Объединить результаты в один Map и вернуть его. + +ДЕТАЛИЗАЦИЯ HELPER-МЕТОДОВ (Промпты внутри них): + +--- Метод 1: getAudienceAnalysis --- +Промпт должен требовать JSON строго такой структуры (для отрисовки виджетов на фронте): +{ +"audienceAnalysis": { +"ageGroups": { "18-24": 15, "25-34": 40, "35-44": 30, "45+": 15 }, // Pie chart data +"ageComment": "Текст вывода под графиком возраста (1-2 предложения)", +"genderDistribution": { "Женщины": 60, "Мужчины": 40 }, // Donut chart +"genderComment": "Текст вывода под графиком пола", +"segments": [ // Список из 4 детальных карточек сегментов +{ +"name": "Название (например 'Молодые профессионалы')", +"sharePercent": 35, +"ageRange": "25-34", +"incomeLevel": "Средний+", +"motivation": "Ключевая мотивация покупки", +"triggers": "Триггеры (на что реагируют)", +"primaryChannels": ["Instagram", "TikTok"], // Топ каналы для этого сегмента +"secondaryChannels": ["Telegram"] +} +], +"segmentsChannelMatrix": [ // Для таблицы heatmap: Сегмент vs Канал +{ "segmentName": "Молодые профи", "instagram": "high", "telegram": "medium", "youtube": "low" } +], +"keyTakeaways": ["Вывод 1", "Вывод 2", "Вывод 3"] +} +} + +--- Метод 2: getCompetitorAnalysis --- +Промпт должен требовать JSON: +{ +"competitorAnalysis": { +"marketShareChart": [ // Оценочные доли рынка для графика +{ "name": "Наш бренд", "value": 25 }, +{ "name": "Конкурент А", "value": 30 }, +{ "name": "Конкурент B", "value": 20 }, +{ "name": "Другие", "value": 25 } +], +"marketShareComment": "Вывод по доле рынка", +"competitorsDetails": [ // Детали по 3-4 конкурентам +{ +"name": "Конкурент А", +"priceStrategy": "Высокая/Средняя/Низкая", +"digitalMetrics": { "reach": 50000, "followers": 12000, "activityIndex": 85 }, +"channelsHeatmap": { // Оценка (0-100) качества ведения канала +"Instagram": 90, "TikTok": 20, "Telegram": 70, "YouTube": 40 +} +} +], +"comparisonTable": [ // Сравнительная таблица (5-6 характеристик) +{ "feature": "Ценовая политика", "us": "Средняя", "compA": "Высокая", "compB": "Низкая" }, +{ "feature": "УТП", "us": "Сервис", "compA": "Бренд", "compB": "Дешевизна" } +], +"swotCompetitors": { // Краткий SWOT по рынку в целом +"strengths": ["..."], "weaknesses": ["..."] +}, +"keyInsights": ["Вывод 1", "Вывод 2"] +} +} + +ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ: + +1. Используй существующий `openAIAnalyticsService.generateWithInstruction` для вызовов. +2. В каждом helper-методе добавь отдельный `try-catch` и логирование, чтобы падение одной части не ломало весь отчет (возвращай пустую Map или stub-данные при ошибке). +3. Используй `objectMapper` для парсинга ответов. +4. Убедись, что JSON очищается от markdown-тегов (```json) перед парсингом. + +Пожалуйста, полностью реализуй этот модульный подход. diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index db56814..dfb1818 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -140,198 +140,341 @@ public class MarketingAnalysisService { } /** - * Промпт №1: Генерация структурированного JSON с данными для графиков + * Генерация структурированного JSON с данными для графиков + * Использует модульный подход: три отдельных запроса к OpenAI для повышения + * детализации */ private Map generateJsonAnalysis(MarketingAnalysisRequest request) { - // Форматирование целевой аудитории - StringBuilder targetAudienceBuilder = new StringBuilder(); - Map targetAudienceData = request.getTargetAudience(); - if (targetAudienceData != null) { - if (targetAudienceData.containsKey("genders")) { - @SuppressWarnings("unchecked") - List genders = (List) targetAudienceData.get("genders"); - if (genders != null && !genders.isEmpty()) { - targetAudienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". "); - } - } - if (targetAudienceData.containsKey("ageRanges")) { - @SuppressWarnings("unchecked") - List ageRanges = (List) targetAudienceData.get("ageRanges"); - if (ageRanges != null && !ageRanges.isEmpty()) { - targetAudienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". "); - } - } - if (targetAudienceData.containsKey("types")) { - @SuppressWarnings("unchecked") - List types = (List) targetAudienceData.get("types"); - if (types != null && !types.isEmpty()) { - targetAudienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". "); - } - } - } - String targetAudienceStr = targetAudienceBuilder.toString().trim(); - if (targetAudienceStr.isEmpty()) { - targetAudienceStr = "Не указана"; + Map result = new HashMap<>(); + + // 1. Получаем анализ целевой аудитории + logger.info("Starting audience analysis generation"); + Map audienceAnalysis = getAudienceAnalysis(request); + if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) { + result.putAll(audienceAnalysis); + logger.info("Audience analysis generated successfully"); + } else { + logger.warn("Audience analysis returned empty result"); } - // Форматирование региона - List regions = request.getRegion(); - String regionStr = (regions != null && !regions.isEmpty()) - ? String.join(", ", regions) - : "Не указан"; + // 2. Получаем анализ конкурентов + logger.info("Starting competitor analysis generation"); + Map competitorAnalysis = getCompetitorAnalysis(request); + if (competitorAnalysis != null && !competitorAnalysis.isEmpty()) { + result.putAll(competitorAnalysis); + logger.info("Competitor analysis generated successfully"); + } else { + logger.warn("Competitor analysis returned empty result"); + } - // Построение промпта №1 - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append( - "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); - promptBuilder.append( - "Твоя задача — на основе входных данных о бизнесе пользователя выполнить маркетинговый анализ и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); - promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); - promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); - promptBuilder.append("---\n\n"); - promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n"); - promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n"); - promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (") - .append(request.getBusinessNiche()).append(")\n"); - promptBuilder.append("- Целевая аудитория (если указана): ").append(targetAudienceStr).append("\n"); - promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche()) - .append(" (уточни основных конкурентов)\n"); - promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n"); - promptBuilder.append("- Регион/город: ").append(regionStr).append("\n\n"); - promptBuilder.append( - "## Если каких-то данных не хватает, ты можешь логично дополнить предположениями, НО в таких полях добавь флаг \"estimated\": true.\n\n"); - promptBuilder.append("СФОРМИРУЙ ОДИН JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); - promptBuilder.append("{\n"); - promptBuilder.append("\"meta\": {\n"); - promptBuilder.append("\"companyName\": string,\n"); - promptBuilder.append("\"region\": string,\n"); - promptBuilder.append("\"primaryGoal\": string,\n"); - promptBuilder.append("\"notes\": string\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"seasonality\": {\n"); - promptBuilder.append("\"description\": string,\n"); - promptBuilder.append("\"months\": [ \"Январь\", \"Февраль\", ... 12 месяцев ],\n"); - promptBuilder.append("\"demandIndex\": [12 чисел от 0 до 100],\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"competitors\": [\n"); - promptBuilder.append("{\n"); - promptBuilder.append("\"name\": string,\n"); - promptBuilder.append("\"reach\": integer, // 1000–50000\n"); - promptBuilder.append("\"socialActivityIndex\": integer, // 0–100\n"); - promptBuilder.append("\"reviewsCount\": integer, // 0–5000\n"); - promptBuilder.append("\"strengths\": [string],\n"); - promptBuilder.append("\"weaknesses\": [string],\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("}\n"); - promptBuilder.append("],\n\n"); - promptBuilder.append("\"audienceSegments\": {\n"); - promptBuilder.append("\"description\": string,\n"); - promptBuilder.append("\"segments\": [\n"); - promptBuilder.append("{\n"); - promptBuilder.append("\"name\": string,\n"); - promptBuilder.append("\"sharePercent\": integer, // сумма всех sharePercent = 100\n"); - promptBuilder.append("\"ageRange\": string,\n"); - promptBuilder.append("\"gender\": string, // \"мужчины\", \"женщины\", \"смешанная\"\n"); - promptBuilder.append("\"incomeLevel\": string, // например: \"низкий\", \"средний\", \"выше среднего\"\n"); - promptBuilder.append("\"keyNeeds\": [string],\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("}\n"); - promptBuilder.append("]\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"channels\": [\n"); - promptBuilder.append("{\n"); - promptBuilder.append( - "\"name\": string, // \"Instagram\", \"TikTok\", \"Telegram\", \"2GIS\", \"Google Ads\" и т.п.\n"); - promptBuilder.append("\"potentialIndex\": integer, // 0–100\n"); - promptBuilder.append("\"priorityLevel\": string, // \"высокий\", \"средний\", \"низкий\"\n"); - promptBuilder.append("\"roleInStrategy\": string, // коротко: для чего этот канал\n"); - promptBuilder.append("\"justification\": string, // почему этот канал важен\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("}\n"); - promptBuilder.append("],\n\n"); - promptBuilder.append("\"funnel\": {\n"); - promptBuilder.append("\"description\": string,\n"); - promptBuilder.append("\"stages\": [\n"); - promptBuilder.append("{\n"); - promptBuilder.append( - "\"stageName\": string, // \"Охват\", \"Вовлечённость\", \"Клики\", \"Переходы\", \"Заявки\", \"Покупки\"\n"); - promptBuilder.append("\"value\": integer, // количество (должно логично уменьшаться вниз по воронке)\n"); - promptBuilder.append("\"conversionPercent\": number // от предыдущего этапа, с одним знаком после запятой\n"); - promptBuilder.append("}\n"); - promptBuilder.append("],\n"); - promptBuilder.append("\"estimated\": boolean\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"swot\": {\n"); - promptBuilder.append("\"strengths\": [string],\n"); - promptBuilder.append("\"weaknesses\": [string],\n"); - promptBuilder.append("\"opportunities\": [string],\n"); - promptBuilder.append("\"threats\": [string]\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"positioning\": {\n"); - promptBuilder.append("\"brandImage\": string,\n"); - promptBuilder.append("\"keyMessage\": string,\n"); - promptBuilder.append("\"toneOfVoice\": string,\n"); - promptBuilder.append("\"trustFactors\": [string]\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"valueProposition\": {\n"); - promptBuilder.append("\"utpVariants\": [string], // 1–3 варианта УТП\n"); - promptBuilder.append("\"mainBenefits\": [string]\n"); - promptBuilder.append("},\n\n"); - promptBuilder.append("\"contentRecommendations\": {\n"); - promptBuilder.append("\"salesContent\": [string],\n"); - promptBuilder.append("\"expertContent\": [string],\n"); - promptBuilder.append("\"trustContent\": [string],\n"); - promptBuilder.append("\"entertainmentContent\": [string]\n"); - promptBuilder.append("}\n"); - promptBuilder.append("}\n\n"); - promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); - promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); - promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); - promptBuilder.append("- Сумма \"sharePercent\" по audienceSegments.segments должна быть = 100.\n"); - promptBuilder.append("- Значения \"demandIndex\" должны быть от 0 до 100.\n"); - promptBuilder.append("- Значения \"potentialIndex\" должны быть от 0 до 100.\n"); - promptBuilder.append("- В \"funnel.stages\" значения \"value\" должны уменьшаться от этапа к этапу.\n"); - promptBuilder.append("- Не используй null, если можно задать разумное значение.\n"); - promptBuilder.append("- Все строки — на русском языке.\n"); + // 3. Получаем анализ сезонности и рынка + logger.info("Starting seasonality and market analysis generation"); + Map seasonalityAndMarket = getSeasonalityAndMarket(request); + if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) { + result.putAll(seasonalityAndMarket); + logger.info("Seasonality and market analysis generated successfully"); + } else { + logger.warn("Seasonality and market analysis returned empty result"); + } - String prompt = promptBuilder.toString(); - String context = buildContextString(request); + // Проверяем, что хотя бы одна секция была успешно сгенерирована + if (result.isEmpty()) { + logger.error("All analysis sections failed to generate"); + return null; + } + logger.info("JSON analysis generation completed. Generated sections: {}", result.keySet()); + return result; + } + + /** + * Очистка JSON-ответа от markdown-тегов + */ + private String cleanJsonFromMarkdown(String jsonResponse) { + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { + return jsonResponse; + } + String cleanedJson = jsonResponse.trim(); + if (cleanedJson.startsWith("```json")) { + cleanedJson = cleanedJson.substring(7); + } + if (cleanedJson.startsWith("```")) { + cleanedJson = cleanedJson.substring(3); + } + if (cleanedJson.endsWith("```")) { + cleanedJson = cleanedJson.substring(0, cleanedJson.length() - 3); + } + return cleanedJson.trim(); + } + + /** + * Генерация анализа целевой аудитории + */ + private Map getAudienceAnalysis(MarketingAnalysisRequest request) { try { + String context = buildContextString(request); + + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append( + "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); + promptBuilder.append( + "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ целевой аудитории и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); + promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); + promptBuilder.append("---\n\n"); + promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n"); + promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n"); + promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (") + .append(request.getBusinessNiche()).append(")\n"); + + Map targetAudienceData = request.getTargetAudience(); + StringBuilder targetAudienceBuilder = new StringBuilder(); + if (targetAudienceData != null) { + if (targetAudienceData.containsKey("genders")) { + @SuppressWarnings("unchecked") + List genders = (List) targetAudienceData.get("genders"); + if (genders != null && !genders.isEmpty()) { + targetAudienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". "); + } + } + if (targetAudienceData.containsKey("ageRanges")) { + @SuppressWarnings("unchecked") + List ageRanges = (List) targetAudienceData.get("ageRanges"); + if (ageRanges != null && !ageRanges.isEmpty()) { + targetAudienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". "); + } + } + if (targetAudienceData.containsKey("types")) { + @SuppressWarnings("unchecked") + List types = (List) targetAudienceData.get("types"); + if (types != null && !types.isEmpty()) { + targetAudienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". "); + } + } + } + String targetAudienceStr = targetAudienceBuilder.toString().trim(); + if (targetAudienceStr.isEmpty()) { + targetAudienceStr = "Не указана"; + } + promptBuilder.append("- Целевая аудитория (если указана): ").append(targetAudienceStr).append("\n"); + + List regions = request.getRegion(); + String regionStr = (regions != null && !regions.isEmpty()) + ? String.join(", ", regions) + : "Не указан"; + promptBuilder.append("- Регион/город: ").append(regionStr).append("\n"); + promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n"); + + promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); + promptBuilder.append("{\n"); + promptBuilder.append("\"audienceAnalysis\": {\n"); + promptBuilder.append( + "\"ageGroups\": { \"18-24\": число, \"25-34\": число, \"35-44\": число, \"45+\": число }, // Pie chart data, сумма = 100\n"); + promptBuilder.append("\"ageComment\": \"Текст вывода под графиком возраста (1-2 предложения)\",\n"); + promptBuilder.append( + "\"genderDistribution\": { \"Женщины\": число, \"Мужчины\": число }, // Donut chart, сумма = 100\n"); + promptBuilder.append("\"genderComment\": \"Текст вывода под графиком пола\",\n"); + promptBuilder.append("\"segments\": [ // Список из 4 детальных карточек сегментов\n"); + promptBuilder.append("{\n"); + promptBuilder.append("\"name\": \"Название (например 'Молодые профессионалы')\",\n"); + promptBuilder.append("\"sharePercent\": число, // сумма всех sharePercent = 100\n"); + promptBuilder.append("\"ageRange\": \"25-34\",\n"); + promptBuilder.append("\"incomeLevel\": \"Средний+\",\n"); + promptBuilder.append("\"motivation\": \"Ключевая мотивация покупки\",\n"); + promptBuilder.append("\"triggers\": \"Триггеры (на что реагируют)\",\n"); + promptBuilder + .append("\"primaryChannels\": [\"Instagram\", \"TikTok\"], // Топ каналы для этого сегмента\n"); + promptBuilder.append("\"secondaryChannels\": [\"Telegram\"]\n"); + promptBuilder.append("}\n"); + promptBuilder.append("],\n"); + promptBuilder.append("\"segmentsChannelMatrix\": [ // Для таблицы heatmap: Сегмент vs Канал\n"); + promptBuilder.append( + "{ \"segmentName\": \"Молодые профи\", \"instagram\": \"high\", \"telegram\": \"medium\", \"youtube\": \"low\" }\n"); + promptBuilder.append("],\n"); + promptBuilder.append("\"keyTakeaways\": [\"Вывод 1\", \"Вывод 2\", \"Вывод 3\"]\n"); + promptBuilder.append("}\n"); + promptBuilder.append("}\n\n"); + promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); + promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); + promptBuilder.append("- Сумма значений в ageGroups должна быть = 100.\n"); + promptBuilder.append("- Сумма значений в genderDistribution должна быть = 100.\n"); + promptBuilder.append("- Сумма sharePercent по всем segments должна быть = 100.\n"); + promptBuilder.append("- Все строки — на русском языке.\n"); + + String prompt = promptBuilder.toString(); String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { - logger.warn("Empty JSON response from AI"); - return null; + logger.warn("Empty JSON response from AI for audience analysis"); + return new HashMap<>(); } - // Очистка ответа от возможных markdown блоков - String cleanedJson = jsonResponse.trim(); - if (cleanedJson.startsWith("```json")) { - cleanedJson = cleanedJson.substring(7); - } - if (cleanedJson.startsWith("```")) { - cleanedJson = cleanedJson.substring(3); - } - if (cleanedJson.endsWith("```")) { - cleanedJson = cleanedJson.substring(0, cleanedJson.length() - 3); - } - cleanedJson = cleanedJson.trim(); - - // Парсинг JSON + String cleanedJson = cleanJsonFromMarkdown(jsonResponse); Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { }); - // Валидация JSON - if (validateJsonAnalysis(jsonMap)) { - return jsonMap; - } else { - logger.warn("JSON validation failed, but returning parsed JSON anyway"); - return jsonMap; - } + return jsonMap; } catch (Exception e) { - logger.error("Error generating JSON analysis: {}", e.getMessage(), e); - return null; + logger.error("Error generating audience analysis: {}", e.getMessage(), e); + return new HashMap<>(); + } + } + + /** + * Генерация анализа конкурентов + */ + private Map getCompetitorAnalysis(MarketingAnalysisRequest request) { + try { + String context = buildContextString(request); + + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append( + "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); + promptBuilder.append( + "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ конкурентов и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); + promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); + promptBuilder.append("---\n\n"); + promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n"); + promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n"); + promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (") + .append(request.getBusinessNiche()).append(")\n"); + promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche()) + .append(" (уточни основных конкурентов)\n"); + + List regions = request.getRegion(); + String regionStr = (regions != null && !regions.isEmpty()) + ? String.join(", ", regions) + : "Не указан"; + promptBuilder.append("- Регион/город: ").append(regionStr).append("\n"); + promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n"); + + promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); + promptBuilder.append("{\n"); + promptBuilder.append("\"competitorAnalysis\": {\n"); + promptBuilder.append("\"marketShareChart\": [ // Оценочные доли рынка для графика\n"); + promptBuilder.append("{ \"name\": \"Наш бренд\", \"value\": число },\n"); + promptBuilder.append("{ \"name\": \"Конкурент А\", \"value\": число },\n"); + promptBuilder.append("{ \"name\": \"Конкурент B\", \"value\": число },\n"); + promptBuilder.append("{ \"name\": \"Другие\", \"value\": число }\n"); + promptBuilder.append("], // сумма всех value должна быть = 100\n"); + promptBuilder.append("\"marketShareComment\": \"Вывод по доле рынка\",\n"); + promptBuilder.append("\"competitorsDetails\": [ // Детали по 3-4 конкурентам\n"); + promptBuilder.append("{\n"); + promptBuilder.append("\"name\": \"Конкурент А\",\n"); + promptBuilder.append("\"priceStrategy\": \"Высокая/Средняя/Низкая\",\n"); + promptBuilder.append( + "\"digitalMetrics\": { \"reach\": число, \"followers\": число, \"activityIndex\": число }, // activityIndex от 0 до 100\n"); + promptBuilder.append("\"channelsHeatmap\": { // Оценка (0-100) качества ведения канала\n"); + promptBuilder.append("\"Instagram\": число, \"TikTok\": число, \"Telegram\": число, \"YouTube\": число\n"); + promptBuilder.append("}\n"); + promptBuilder.append("}\n"); + promptBuilder.append("],\n"); + promptBuilder.append("\"comparisonTable\": [ // Сравнительная таблица (5-6 характеристик)\n"); + promptBuilder.append( + "{ \"feature\": \"Ценовая политика\", \"us\": \"Средняя\", \"compA\": \"Высокая\", \"compB\": \"Низкая\" },\n"); + promptBuilder.append( + "{ \"feature\": \"УТП\", \"us\": \"Сервис\", \"compA\": \"Бренд\", \"compB\": \"Дешевизна\" }\n"); + promptBuilder.append("],\n"); + promptBuilder.append("\"swotCompetitors\": { // Краткий SWOT по рынку в целом\n"); + promptBuilder.append("\"strengths\": [\"...\"], \"weaknesses\": [\"...\"]\n"); + promptBuilder.append("},\n"); + promptBuilder.append("\"keyInsights\": [\"Вывод 1\", \"Вывод 2\"]\n"); + promptBuilder.append("}\n"); + promptBuilder.append("}\n\n"); + promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); + promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); + promptBuilder.append("- Сумма всех value в marketShareChart должна быть = 100.\n"); + promptBuilder.append("- Все строки — на русском языке.\n"); + + String prompt = promptBuilder.toString(); + String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { + logger.warn("Empty JSON response from AI for competitor analysis"); + return new HashMap<>(); + } + + String cleanedJson = cleanJsonFromMarkdown(jsonResponse); + Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { + }); + + return jsonMap; + } catch (Exception e) { + logger.error("Error generating competitor analysis: {}", e.getMessage(), e); + return new HashMap<>(); + } + } + + /** + * Генерация анализа сезонности и рынка + */ + private Map getSeasonalityAndMarket(MarketingAnalysisRequest request) { + try { + String context = buildContextString(request); + + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append( + "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); + promptBuilder.append( + "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ сезонности и рынка и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n"); + promptBuilder.append("Ответ должен быть валидным JSON.\n\n"); + promptBuilder.append("---\n\n"); + promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n"); + promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n"); + promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (") + .append(request.getBusinessNiche()).append(")\n"); + + List regions = request.getRegion(); + String regionStr = (regions != null && !regions.isEmpty()) + ? String.join(", ", regions) + : "Не указан"; + promptBuilder.append("- Регион/город: ").append(regionStr).append("\n"); + promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n"); + + promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); + promptBuilder.append("{\n"); + promptBuilder.append("\"seasonality\": {\n"); + promptBuilder.append("\"description\": \"Описание сезонных колебаний спроса\",\n"); + promptBuilder.append( + "\"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n"); + promptBuilder.append("\"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n"); + promptBuilder.append("\"estimated\": boolean\n"); + promptBuilder.append("},\n"); + promptBuilder.append("\"market\": {\n"); + promptBuilder.append("\"size\": \"Размер рынка (например, 'средний', 'крупный')\",\n"); + promptBuilder.append("\"growthRate\": \"Темп роста рынка\",\n"); + promptBuilder.append("\"trends\": [\"Тренд 1\", \"Тренд 2\", \"Тренд 3\"],\n"); + promptBuilder.append("\"opportunities\": [\"Возможность 1\", \"Возможность 2\"],\n"); + promptBuilder.append("\"threats\": [\"Угроза 1\", \"Угроза 2\"],\n"); + promptBuilder.append("\"estimated\": boolean\n"); + promptBuilder.append("}\n"); + promptBuilder.append("}\n\n"); + promptBuilder.append("ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n"); + promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n"); + promptBuilder.append("- Значения \"demandIndex\" должны быть от 0 до 100.\n"); + promptBuilder.append("- Все строки — на русском языке.\n"); + + String prompt = promptBuilder.toString(); + String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { + logger.warn("Empty JSON response from AI for seasonality and market analysis"); + return new HashMap<>(); + } + + String cleanedJson = cleanJsonFromMarkdown(jsonResponse); + Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { + }); + + return jsonMap; + } catch (Exception e) { + logger.error("Error generating seasonality and market analysis: {}", e.getMessage(), e); + return new HashMap<>(); } }