From 0807280d5d23d6c24888b007eafd014deadb20d9 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Dec 2025 19:02:28 +0500 Subject: [PATCH] . --- g.md | 213 +++ .../service/MarketingAnalysisService.java | 1366 +++++++++-------- 2 files changed, 924 insertions(+), 655 deletions(-) create mode 100644 g.md diff --git a/g.md b/g.md new file mode 100644 index 0000000..5ab20a9 --- /dev/null +++ b/g.md @@ -0,0 +1,213 @@ +Промт №1 — для JSON-аналитики (цифры + структура для графиков). +Промт №2 — для текстового отчёта на основе JSON. +Краткое ТЗ для backend: как из этого собирать графики и отчёт. + +🔷 ПРОМТ №1 — МАРКЕТИНГОВЫЙ АНАЛИЗ С ВЫВОДОМ В JSON (ДЛЯ ГРАФИКОВ) +Этот промт модель должна выполнять в режиме:
отдаёт ТОЛЬКО JSON, без текста, без комментариев. +Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане. + +Твоя задача — на основе входных данных о бизнесе пользователя выполнить маркетинговый анализ и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре. + +Не добавляй пояснений, комментариев, текста до или после JSON. +Ответ должен быть валидным JSON. + +--- + +ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ: + +- Название компании/продукта: {{company_name}} +- Описание продукта/услуг: {{product_description}} +- Целевая аудитория (если указана): {{target_audience}} +- Конкуренты (если указаны): {{competitors}} +- Цель продвижения: {{goals}} +- Регион/город: {{region}} + +## Если каких-то данных не хватает, ты можешь логично дополнить предположениями, НО в таких полях добавь флаг "estimated": true. + +СФОРМИРУЙ ОДИН JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ: + +{ +"meta": { +"companyName": string, +"region": string, +"primaryGoal": string, +"notes": string +}, + +"seasonality": { +"description": string, +"months": [ "Январь", "Февраль", ... 12 месяцев ], +"demandIndex": [12 чисел от 0 до 100], +"estimated": boolean +}, + +"competitors": [ +{ +"name": string, +"reach": integer, // 1000–50000 +"socialActivityIndex": integer, // 0–100 +"reviewsCount": integer, // 0–5000 +"strengths": [string], +"weaknesses": [string], +"estimated": boolean +} +], + +"audienceSegments": { +"description": string, +"segments": [ +{ +"name": string, +"sharePercent": integer, // сумма всех sharePercent = 100 +"ageRange": string, +"gender": string, // "мужчины", "женщины", "смешанная" +"incomeLevel": string, // например: "низкий", "средний", "выше среднего" +"keyNeeds": [string], +"estimated": boolean +} +] +}, + +"channels": [ +{ +"name": string, // "Instagram", "TikTok", "Telegram", "2GIS", "Google Ads" и т.п. +"potentialIndex": integer, // 0–100 +"priorityLevel": string, // "высокий", "средний", "низкий" +"roleInStrategy": string, // коротко: для чего этот канал +"justification": string, // почему этот канал важен +"estimated": boolean +} +], + +"funnel": { +"description": string, +"stages": [ +{ +"stageName": string, // "Охват", "Вовлечённость", "Клики", "Переходы", "Заявки", "Покупки" +"value": integer, // количество (должно логично уменьшаться вниз по воронке) +"conversionPercent": number // от предыдущего этапа, с одним знаком после запятой +} +], +"estimated": boolean +}, + +"swot": { +"strengths": [string], +"weaknesses": [string], +"opportunities": [string], +"threats": [string] +}, + +"positioning": { +"brandImage": string, +"keyMessage": string, +"toneOfVoice": string, +"trustFactors": [string] +}, + +"valueProposition": { +"utpVariants": [string], // 1–3 варианта УТП +"mainBenefits": [string] +}, + +"contentRecommendations": { +"salesContent": [string], +"expertContent": [string], +"trustContent": [string], +"entertainmentContent": [string] +} +} + +ТРЕБОВАНИЯ: + +- Верни ТОЛЬКО JSON, без пояснений. +- Следи, чтобы JSON был синтаксически корректным. +- Сумма "sharePercent" по audienceSegments.segments должна быть = 100. +- Значения "demandIndex" должны быть от 0 до 100. +- Значения "potentialIndex" должны быть от 0 до 100. +- В "funnel.stages" значения "value" должны уменьшаться от этапа к этапу. +- Не используй null, если можно задать разумное значение. +- Все строки — на русском языке. + Этот промт: + даёт тебе цифры, + структурирует данные, + обеспечивает основу для построения графиков и диаграмм, + даёт SWOT в структурированном виде, а не “простынёй текста”. + +ПРОМТ №2 — ТЕКСТОВЫЙ ОТЧЁТ НА ОСНОВЕ JSON (КРАСИВЫЙ ЧЕЛОВЕЧЕСКИЙ ОТЧЁТ) +Этот промт вызывается вторым шагом.
Ему на вход даётся JSON, который вернул промт №1. + +Ты — профессиональный маркетолог и аналитик. + +Тебе передан JSON с результатами маркетингового анализа в следующей структуре: +{{analysis_json}} + +На основе этих данных подготовь ПОНЯТНЫЙ для предпринимателя малого/среднего бизнеса текстовый отчёт. + +ТРЕБОВАНИЯ К ОТЧЁТУ: + +1. Структура разделов: + I. Краткое резюме (что за бизнес, какая цель, общий вывод) + II. Анализ продукта + III. Анализ рынка и сезонности + IV. Анализ целевой аудитории + V. Анализ конкурентов + VI. SWOT-анализ (в виде 4 списков) + VII. Каналы продвижения и их потенциал + VIII. Воронка спроса (с пояснением, где “узкое место”) + IX. Рекомендации по позиционированию и УТП + X. Рекомендации по контенту + XI. Итоговая стратегия: что делать в первую очередь + +2. Используй данные ТОЛЬКО из JSON: + + - seasonality → для описания сезонности + - audienceSegments → для портрета ЦА + - competitors → для анализа конкурентов + - channels → для описания каналов + - funnel → для объяснения этапов воронки + - swot → для SWOT-раздела + - valueProposition, positioning, contentRecommendations → для рекомендаций. + +3. Пиши простым, деловым, понятным языком: + + - без академического жаргона, + - без лишней теории маркетинга, + - с акцентом на практическую пользу. + +4. Не повторяй сам JSON. + Отчёт — это человеческое объяснение на основе уже рассчитанных данных. + +5. Не выдумывай новые цифры — используй только те, что есть в JSON. + +ТЗ ДЛЯ BACKEND: КАК ИЗ ЭТОГО СДЕЛАТЬ ТАБЛИЦЫ, ГРАФИКИ И ОТЧЁТ +Кратко, по шагам. + +1. Вызов промта №1 (JSON-анализ) + Backend берёт входные данные пользователя. + Отправляет их в LLM с промтом №1. + Получает строку JSON. + Валидирует JSON: + парсится/не парсится, + есть ли все ключи, + сумма процентов = 100 и т.д. + Сохраняет JSON в БД (например, в колонке analysis_json). +2. Построение графиков и диаграмм + Используем JSON-поля: + seasonality.months + seasonality.demandIndex
→ линейный график (Line chart). + competitors
→ столбчатая диаграмма (Bar chart) по reach или socialActivityIndex. + audienceSegments.segments
→ круговая диаграмма (Pie chart) по sharePercent. + channels
→ горизонтальный bar chart по potentialIndex. + funnel.stages
→ воронка (Funnel chart) и/или bar chart по value. + Технически: + Python (matplotlib / plotly) или JS (ECharts / Chart.js) — на твой выбор. + На выходе: PNG/SVG для отчёта или интерактив для веб-панели. +3. Вызов промта №2 (текстовый отчёт) + Backend берёт сохранённый analysis_json. + Подставляет его в промт №2 (как {{analysis_json}}). + LLM возвращает текстовый отчёт. + Отчёт сохраняется в БД и/или показывается пользователю. +4. SWOT и таблицы + SWOT уже структурирован в swot. + На фронтенде его легко отобразить как таблицу 2×2. + Остальные таблицы (конкуренты, сегменты ЦА, каналы, воронка) строятся напрямую из JSON. diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index f1f08ff..b0287d2 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -13,6 +13,9 @@ import org.springframework.http.MediaType; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + import java.time.LocalDateTime; import java.util.*; @@ -25,6 +28,7 @@ public class MarketingAnalysisService { private final OpenAIAnalyticsService openAIAnalyticsService; private final MinIOService minIOService; private final ResearchPdfService researchPdfService; + private final ObjectMapper objectMapper = new ObjectMapper(); public MarketingAnalysisService( MarketingAnalysisRepository repository, @@ -132,6 +136,548 @@ public class MarketingAnalysisService { } } + /** + * Промпт №1: Генерация структурированного JSON с данными для графиков + */ + 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 = "Не указана"; + } + + // Форматирование региона + List regions = request.getRegion(); + String regionStr = (regions != null && !regions.isEmpty()) + ? String.join(", ", regions) + : "Не указан"; + + // Построение промпта №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"); + + String prompt = promptBuilder.toString(); + String context = buildContextString(request); + + try { + String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { + logger.warn("Empty JSON response from AI"); + return null; + } + + // Очистка ответа от возможных 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 + 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; + } + } catch (Exception e) { + logger.error("Error generating JSON analysis: {}", e.getMessage(), e); + return null; + } + } + + /** + * Промпт №2: Генерация текстового отчёта на основе JSON + */ + private String generateTextReportFromJson(Map analysisJson) { + if (analysisJson == null || analysisJson.isEmpty()) { + logger.warn("Empty analysisJson provided for text report generation"); + return "Не удалось сгенерировать текстовый отчёт: отсутствуют данные JSON-анализа."; + } + + try { + // Преобразование JSON в строку для промпта + String jsonString = objectMapper.writeValueAsString(analysisJson); + + // Построение промпта №2 + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append("Ты — профессиональный маркетолог и аналитик.\n\n"); + promptBuilder.append("Тебе передан JSON с результатами маркетингового анализа в следующей структуре:\n"); + promptBuilder.append(jsonString).append("\n\n"); + promptBuilder.append("На основе этих данных подготовь ПОНЯТНЫЙ для предпринимателя малого/среднего бизнеса текстовый отчёт.\n\n"); + promptBuilder.append("ТРЕБОВАНИЯ К ОТЧЁТУ:\n\n"); + promptBuilder.append("1. Структура разделов:\n"); + promptBuilder.append(" I. Краткое резюме (что за бизнес, какая цель, общий вывод)\n"); + promptBuilder.append(" II. Анализ продукта\n"); + promptBuilder.append(" III. Анализ рынка и сезонности\n"); + promptBuilder.append(" IV. Анализ целевой аудитории\n"); + promptBuilder.append(" V. Анализ конкурентов\n"); + promptBuilder.append(" VI. SWOT-анализ (в виде 4 списков)\n"); + promptBuilder.append(" VII. Каналы продвижения и их потенциал\n"); + promptBuilder.append(" VIII. Воронка спроса (с пояснением, где \"узкое место\")\n"); + promptBuilder.append(" IX. Рекомендации по позиционированию и УТП\n"); + promptBuilder.append(" X. Рекомендации по контенту\n"); + promptBuilder.append(" XI. Итоговая стратегия: что делать в первую очередь\n\n"); + promptBuilder.append("2. Используй данные ТОЛЬКО из JSON:\n\n"); + promptBuilder.append(" - seasonality → для описания сезонности\n"); + promptBuilder.append(" - audienceSegments → для портрета ЦА\n"); + promptBuilder.append(" - competitors → для анализа конкурентов\n"); + promptBuilder.append(" - channels → для описания каналов\n"); + promptBuilder.append(" - funnel → для объяснения этапов воронки\n"); + promptBuilder.append(" - swot → для SWOT-раздела\n"); + promptBuilder.append(" - valueProposition, positioning, contentRecommendations → для рекомендаций.\n\n"); + promptBuilder.append("3. Пиши простым, деловым, понятным языком:\n\n"); + promptBuilder.append(" - без академического жаргона,\n"); + promptBuilder.append(" - без лишней теории маркетинга,\n"); + promptBuilder.append(" - с акцентом на практическую пользу.\n\n"); + promptBuilder.append("4. Не повторяй сам JSON.\n"); + promptBuilder.append(" Отчёт — это человеческое объяснение на основе уже рассчитанных данных.\n\n"); + promptBuilder.append("5. Не выдумывай новые цифры — используй только те, что есть в JSON.\n"); + + String prompt = promptBuilder.toString(); + String context = jsonString; // Используем JSON как контекст + + String textReport = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + return textReport != null ? textReport : "Не удалось сгенерировать текстовый отчёт."; + } catch (Exception e) { + logger.error("Error generating text report from JSON: {}", e.getMessage(), e); + return "Ошибка при генерации текстового отчёта: " + e.getMessage(); + } + } + + /** + * Валидация JSON-анализа + */ + private boolean validateJsonAnalysis(Map jsonMap) { + if (jsonMap == null || jsonMap.isEmpty()) { + return false; + } + + try { + // Проверка обязательных полей + if (!jsonMap.containsKey("meta") || !jsonMap.containsKey("seasonality") || + !jsonMap.containsKey("competitors") || !jsonMap.containsKey("audienceSegments") || + !jsonMap.containsKey("channels") || !jsonMap.containsKey("funnel") || + !jsonMap.containsKey("swot")) { + logger.warn("Missing required fields in JSON analysis"); + return false; + } + + // Проверка суммы sharePercent = 100 + @SuppressWarnings("unchecked") + Map audienceSegments = (Map) jsonMap.get("audienceSegments"); + if (audienceSegments != null && audienceSegments.containsKey("segments")) { + @SuppressWarnings("unchecked") + List> segments = (List>) audienceSegments.get("segments"); + if (segments != null) { + int totalPercent = 0; + for (Map segment : segments) { + if (segment.containsKey("sharePercent")) { + Object percentObj = segment.get("sharePercent"); + if (percentObj instanceof Number) { + totalPercent += ((Number) percentObj).intValue(); + } + } + } + if (totalPercent != 100) { + logger.warn("Sum of sharePercent is {} instead of 100", totalPercent); + // Не считаем это критической ошибкой, продолжаем работу + } + } + } + + // Проверка диапазонов значений + @SuppressWarnings("unchecked") + Map seasonality = (Map) jsonMap.get("seasonality"); + if (seasonality != null && seasonality.containsKey("demandIndex")) { + @SuppressWarnings("unchecked") + List demandIndex = (List) seasonality.get("demandIndex"); + if (demandIndex != null) { + for (Object value : demandIndex) { + if (value instanceof Number) { + int intValue = ((Number) value).intValue(); + if (intValue < 0 || intValue > 100) { + logger.warn("demandIndex value {} is out of range [0-100]", intValue); + } + } + } + } + } + + return true; + } catch (Exception e) { + logger.error("Error validating JSON analysis: {}", e.getMessage(), e); + return false; + } + } + + /** + * Построение строки контекста из запроса + */ + private String buildContextString(MarketingAnalysisRequest request) { + StringBuilder contextBuilder = new StringBuilder(); + contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n"); + contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n"); + + Map targetAudienceData = request.getTargetAudience(); + if (targetAudienceData != null) { + StringBuilder audienceBuilder = new StringBuilder(); + if (targetAudienceData.containsKey("genders")) { + @SuppressWarnings("unchecked") + List genders = (List) targetAudienceData.get("genders"); + if (genders != null && !genders.isEmpty()) { + audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". "); + } + } + if (targetAudienceData.containsKey("ageRanges")) { + @SuppressWarnings("unchecked") + List ageRanges = (List) targetAudienceData.get("ageRanges"); + if (ageRanges != null && !ageRanges.isEmpty()) { + audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". "); + } + } + if (targetAudienceData.containsKey("types")) { + @SuppressWarnings("unchecked") + List types = (List) targetAudienceData.get("types"); + if (types != null && !types.isEmpty()) { + audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". "); + } + } + contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n"); + } + + List regions = request.getRegion(); + if (regions != null && !regions.isEmpty()) { + contextBuilder.append("Регион: ").append(String.join(", ", regions)).append("\n"); + } + contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n"); + if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) { + contextBuilder.append("Сильная сторона: ").append(request.getStrongSide()).append("\n"); + } + if (request.getWeakSide() != null && !request.getWeakSide().trim().isEmpty()) { + contextBuilder.append("Слабая сторона: ").append(request.getWeakSide()).append("\n"); + } + + return contextBuilder.toString(); + } + + /** + * Парсинг данных для графиков из JSON-анализа + */ + private Map parseChartDataFromJson(Map analysisJson) { + Map chartsData = new HashMap<>(); + + if (analysisJson == null || analysisJson.isEmpty()) { + return chartsData; + } + + try { + // 1. Сезонность + @SuppressWarnings("unchecked") + Map seasonality = (Map) analysisJson.get("seasonality"); + if (seasonality != null) { + @SuppressWarnings("unchecked") + List months = (List) seasonality.get("months"); + @SuppressWarnings("unchecked") + List demandIndex = (List) seasonality.get("demandIndex"); + + if (months != null && demandIndex != null && months.size() == demandIndex.size()) { + Map seasonalityMap = new LinkedHashMap<>(); + for (int i = 0; i < months.size() && i < demandIndex.size(); i++) { + String month = months.get(i); + Object valueObj = demandIndex.get(i); + if (valueObj instanceof Number) { + seasonalityMap.put(month, ((Number) valueObj).intValue()); + } + } + if (!seasonalityMap.isEmpty()) { + chartsData.put("seasonality", seasonalityMap); + } + } + } + + // 2. Конкуренты + @SuppressWarnings("unchecked") + List> competitors = (List>) analysisJson.get("competitors"); + if (competitors != null && !competitors.isEmpty()) { + List> competitorsList = new ArrayList<>(); + for (Map comp : competitors) { + Map competitorData = new HashMap<>(); + competitorData.put("name", comp.getOrDefault("name", "Неизвестный конкурент")); + competitorData.put("reach", comp.getOrDefault("reach", 10000)); + competitorData.put("activity", comp.getOrDefault("socialActivityIndex", 50)); + competitorData.put("reviews", comp.getOrDefault("reviewsCount", 100)); + competitorData.put("strengths", comp.getOrDefault("strengths", "Не указано")); + competitorData.put("weaknesses", comp.getOrDefault("weaknesses", "Не указано")); + competitorsList.add(competitorData); + } + chartsData.put("competitors", competitorsList); + } + + // 3. Сегментация аудитории + @SuppressWarnings("unchecked") + Map audienceSegments = (Map) analysisJson.get("audienceSegments"); + if (audienceSegments != null) { + @SuppressWarnings("unchecked") + List> segments = (List>) audienceSegments.get("segments"); + if (segments != null && !segments.isEmpty()) { + Map segmentationData = new HashMap<>(); + + // Группировка по возрасту + Map ageGroups = new HashMap<>(); + Map genders = new HashMap<>(); + List> segmentsList = new ArrayList<>(); + + for (Map segment : segments) { + String name = (String) segment.getOrDefault("name", "Сегмент"); + Object percentObj = segment.get("sharePercent"); + int percent = percentObj instanceof Number ? ((Number) percentObj).intValue() : 0; + + String ageRange = (String) segment.getOrDefault("ageRange", ""); + String gender = (String) segment.getOrDefault("gender", ""); + + // Добавляем в общий список сегментов + Map segmentData = new HashMap<>(); + segmentData.put("label", name); + segmentData.put("value", String.valueOf(percent)); + segmentsList.add(segmentData); + + // Группируем по возрасту + if (!ageRange.isEmpty()) { + ageGroups.put(ageRange, ageGroups.getOrDefault(ageRange, 0) + percent); + } + + // Группируем по полу + if (!gender.isEmpty()) { + genders.put(gender, genders.getOrDefault(gender, 0) + percent); + } + } + + // Преобразуем в нужный формат + List> ageGroupsList = new ArrayList<>(); + for (Map.Entry entry : ageGroups.entrySet()) { + Map ageGroup = new HashMap<>(); + ageGroup.put("label", entry.getKey()); + ageGroup.put("value", String.valueOf(entry.getValue())); + ageGroupsList.add(ageGroup); + } + + List> gendersList = new ArrayList<>(); + for (Map.Entry entry : genders.entrySet()) { + Map genderData = new HashMap<>(); + genderData.put("label", entry.getKey()); + genderData.put("value", String.valueOf(entry.getValue())); + gendersList.add(genderData); + } + + if (!ageGroupsList.isEmpty()) { + segmentationData.put("ageGroups", ageGroupsList); + } + if (!gendersList.isEmpty()) { + segmentationData.put("genders", gendersList); + } + if (!segmentsList.isEmpty()) { + segmentationData.put("segments", segmentsList); + } + + if (!segmentationData.isEmpty()) { + chartsData.put("audienceSegmentation", segmentationData); + } + } + } + + // 4. Каналы продвижения + @SuppressWarnings("unchecked") + List> channels = (List>) analysisJson.get("channels"); + if (channels != null && !channels.isEmpty()) { + List> channelsList = new ArrayList<>(); + for (Map channel : channels) { + Map channelData = new HashMap<>(); + channelData.put("name", channel.getOrDefault("name", "Неизвестный канал")); + channelData.put("potential", channel.getOrDefault("potentialIndex", 50)); + channelData.put("justification", channel.getOrDefault("justification", "Подходит для данной ниши")); + channelsList.add(channelData); + } + chartsData.put("channelsPotential", channelsList); + } + + // 5. Воронка конверсии + @SuppressWarnings("unchecked") + Map funnel = (Map) analysisJson.get("funnel"); + if (funnel != null) { + @SuppressWarnings("unchecked") + List> stages = (List>) funnel.get("stages"); + if (stages != null && !stages.isEmpty()) { + List> funnelList = new ArrayList<>(); + for (Map stage : stages) { + Map stageData = new HashMap<>(); + stageData.put("stage", stage.getOrDefault("stageName", "Этап")); + stageData.put("value", stage.getOrDefault("value", 0)); + + Object conversionObj = stage.get("conversionPercent"); + if (conversionObj instanceof Number) { + stageData.put("conversion", ((Number) conversionObj).intValue()); + } else { + stageData.put("conversion", 0); + } + funnelList.add(stageData); + } + chartsData.put("conversionFunnel", funnelList); + } + } + + // 6. SWOT (для таблицы) + @SuppressWarnings("unchecked") + Map swot = (Map) analysisJson.get("swot"); + if (swot != null) { + chartsData.put("swot", swot); + } + + } catch (Exception e) { + logger.error("Error parsing chart data from JSON: {}", e.getMessage(), e); + } + + return chartsData; + } + private Map generateMarketingReport(MarketingAnalysisRequest request) { Map report = new HashMap<>(); @@ -180,7 +726,7 @@ public class MarketingAnalysisService { } contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n"); } - + // Format regions List regions = request.getRegion(); if (regions != null && !regions.isEmpty()) { @@ -197,18 +743,34 @@ public class MarketingAnalysisService { contextBuilder.append("Типы анализа: ").append(String.join(", ", analysisTypeNames)).append("\n"); String context = contextBuilder.toString(); - // Generate chart data first (numerical data for graphs) - Map chartsData = generateChartData(context, request); - report.put("chartsData", chartsData); + // Шаг 1: Генерация JSON-анализа (Промпт №1) + Map analysisJson = generateJsonAnalysis(request); - // Generate unified analysis with all types using new prompt structure - String unifiedAnalysisContent = generateUnifiedAnalysis(context, request, detailLevel); - - // Store analysis data - report.put("analysisTypes", analysisTypeNames); - report.put("detailLevel", detailLevelName); - report.put("fullAnalysis", unifiedAnalysisContent != null ? unifiedAnalysisContent : "Анализ не удалось сгенерировать."); - report.put("summary", unifiedAnalysisContent != null ? unifiedAnalysisContent : "Анализ не удалось сгенерировать."); // Keep for backward compatibility + if (analysisJson == null || analysisJson.isEmpty()) { + logger.error("Failed to generate JSON analysis"); + // При ошибке возвращаем базовую структуру с сообщением об ошибке + report.put("analysisTypes", analysisTypeNames); + report.put("detailLevel", detailLevelName); + report.put("fullAnalysis", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже."); + report.put("summary", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже."); + report.put("chartsData", new HashMap<>()); + } else { + // Сохраняем JSON в отчёт + report.put("analysisJson", analysisJson); + + // Шаг 2: Генерация текстового отчёта на основе JSON (Промпт №2) + String textReport = generateTextReportFromJson(analysisJson); + + // Извлекаем данные для графиков из JSON + Map chartsData = parseChartDataFromJson(analysisJson); + report.put("chartsData", chartsData); + + // Сохраняем данные анализа + report.put("analysisTypes", analysisTypeNames); + report.put("detailLevel", detailLevelName); + report.put("fullAnalysis", textReport != null ? textReport : "Анализ не удалось сгенерировать."); + report.put("summary", textReport != null ? textReport : "Анализ не удалось сгенерировать."); // Keep for backward compatibility + } // Generate target audience description (adjusted by detail level) String audiencePrompt; @@ -295,649 +857,6 @@ public class MarketingAnalysisService { return report; } - private Map generateChartData(String context, MarketingAnalysisRequest request) { - Map chartsData = new HashMap<>(); - - // Build prompt for chart data generation - StringBuilder chartPromptBuilder = new StringBuilder(); - chartPromptBuilder.append("Ты — профессиональный маркетолог и аналитик, работающий с малым и средним бизнесом Казахстана. "); - chartPromptBuilder.append("На основе следующей информации о бизнесе сгенерируй ТОЛЬКО числовые данные в виде таблиц для построения графиков. "); - chartPromptBuilder.append("Ответ должен содержать ТОЛЬКО таблицы в формате Markdown, без дополнительного текста.\n\n"); - - chartPromptBuilder.append("📊 Сгенерируй следующие таблицы:\n\n"); - - chartPromptBuilder.append("1️⃣ Сезонность спроса (линейный график, 12 месяцев)\n"); - chartPromptBuilder.append("Выведи таблицу в формате:\n"); - chartPromptBuilder.append("| Месяц | Уровень спроса (0–100) |\n"); - chartPromptBuilder.append("|-------|------------------------|\n"); - chartPromptBuilder.append("| Январь | [число 0-100] |\n"); - chartPromptBuilder.append("| Февраль | [число 0-100] |\n"); - chartPromptBuilder.append("... (для всех 12 месяцев)\n"); - chartPromptBuilder.append("Значения должны соответствовать нише, иметь сезонную динамику, быть реалистичными.\n\n"); - - chartPromptBuilder.append("2️⃣ Сравнительный анализ конкурентов (столбчатая диаграмма)\n"); - chartPromptBuilder.append("Выведи таблицу в формате:\n"); - chartPromptBuilder.append("| Конкурент | Охват аудитории | Активность в соцсетях (0–100) | Количество отзывов | Сильные стороны | Слабые стороны |\n"); - chartPromptBuilder.append("|-----------|-----------------|-------------------------------|-------------------|----------------|----------------|\n"); - chartPromptBuilder.append("| [Название] | [1000-50000] | [0-100] | [число] | [текст] | [текст] |\n"); - chartPromptBuilder.append("... (для 3-5 конкурентов)\n"); - chartPromptBuilder.append("Охват — в диапазоне 1 000–50 000, Активность — 0–100.\n\n"); - - chartPromptBuilder.append("3️⃣ Сегментация целевой аудитории (круговая диаграмма)\n"); - chartPromptBuilder.append("Выведи три таблицы:\n"); - chartPromptBuilder.append("Таблица 1 - Возрастные группы:\n"); - chartPromptBuilder.append("| Возрастная группа | Процент (%) |\n"); - chartPromptBuilder.append("|-------------------|-------------|\n"); - chartPromptBuilder.append("| [группа] | [число] |\n\n"); - chartPromptBuilder.append("Таблица 2 - Пол:\n"); - chartPromptBuilder.append("| Пол | Процент (%) |\n"); - chartPromptBuilder.append("|-----|-------------|\n"); - chartPromptBuilder.append("| [пол] | [число] |\n\n"); - chartPromptBuilder.append("Таблица 3 - Сегменты ЦА:\n"); - chartPromptBuilder.append("| Сегмент | Процент (%) |\n"); - chartPromptBuilder.append("|---------|-------------|\n"); - chartPromptBuilder.append("| [сегмент] | [число] |\n"); - chartPromptBuilder.append("Сумма процентов в каждой таблице = 100%.\n\n"); - - chartPromptBuilder.append("4️⃣ Потенциал каналов продвижения (бар-чарт)\n"); - chartPromptBuilder.append("Выведи таблицу в формате:\n"); - chartPromptBuilder.append("| Канал | Потенциал (0–100) | Обоснование |\n"); - chartPromptBuilder.append("|-------|-------------------|-------------|\n"); - chartPromptBuilder.append("| Instagram | [0-100] | [текст] |\n"); - chartPromptBuilder.append("| TikTok | [0-100] | [текст] |\n"); - chartPromptBuilder.append("| Telegram | [0-100] | [текст] |\n"); - chartPromptBuilder.append("| 2GIS | [0-100] | [текст] |\n"); - chartPromptBuilder.append("| Google Ads | [0-100] | [текст] |\n\n"); - - chartPromptBuilder.append("5️⃣ Конверсионная воронка (воронка спроса)\n"); - chartPromptBuilder.append("Выведи таблицу в формате:\n"); - chartPromptBuilder.append("| Этап | Значение | Конверсия (%) |\n"); - chartPromptBuilder.append("|------|----------|---------------|\n"); - chartPromptBuilder.append("| Охват | [число] | 100 |\n"); - chartPromptBuilder.append("| Вовлечённость | [число] | [%] |\n"); - chartPromptBuilder.append("| Клики | [число] | [%] |\n"); - chartPromptBuilder.append("| Переходы | [число] | [%] |\n"); - chartPromptBuilder.append("| Заявки | [число] | [%] |\n"); - chartPromptBuilder.append("| Покупки | [число] | [%] |\n"); - chartPromptBuilder.append("Числа должны уменьшаться логично от этапа к этапу.\n\n"); - - chartPromptBuilder.append("Информация о бизнесе:\n"); - chartPromptBuilder.append(context); - - String chartPrompt = chartPromptBuilder.toString(); - String chartDataResponse = openAIAnalyticsService.generateWithInstruction(context, chartPrompt, "ru"); - - // Parse the response to extract chart data - if (chartDataResponse != null && !chartDataResponse.trim().isEmpty()) { - Map parsedData = parseChartDataFromResponse(chartDataResponse); - chartsData.putAll(parsedData); - } - - return chartsData; - } - - private Map parseChartDataFromResponse(String response) { - Map chartData = new HashMap<>(); - - // Parse seasonality data - Map seasonality = parseSeasonalityData(response); - if (!seasonality.isEmpty()) { - chartData.put("seasonality", seasonality); - } - - // Parse competitors data - List> competitors = parseCompetitorsData(response); - if (!competitors.isEmpty()) { - chartData.put("competitors", competitors); - } - - // Parse audience segmentation - Map audienceSegmentation = parseAudienceSegmentation(response); - if (!audienceSegmentation.isEmpty()) { - chartData.put("audienceSegmentation", audienceSegmentation); - } - - // Parse channels potential - List> channelsPotential = parseChannelsPotential(response); - if (!channelsPotential.isEmpty()) { - chartData.put("channelsPotential", channelsPotential); - } - - // Parse conversion funnel - List> conversionFunnel = parseConversionFunnel(response); - if (!conversionFunnel.isEmpty()) { - chartData.put("conversionFunnel", conversionFunnel); - } - - return chartData; - } - - private Map parseSeasonalityData(String response) { - Map seasonality = new LinkedHashMap<>(); - String[] months = {"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", - "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"}; - - // Try to find table with seasonality data - String[] lines = response.split("\n"); - boolean inTable = false; - for (String line : lines) { - line = line.trim(); - if (line.contains("Месяц") && line.contains("Уровень спроса")) { - inTable = true; - continue; - } - if (inTable && line.startsWith("|") && !line.startsWith("|---")) { - String[] parts = line.split("\\|"); - if (parts.length >= 3) { - String month = parts[1].trim(); - String valueStr = parts[2].trim(); - try { - // Extract number from value string - valueStr = valueStr.replaceAll("[^0-9]", ""); - if (!valueStr.isEmpty()) { - int value = Integer.parseInt(valueStr); - if (value >= 0 && value <= 100) { - seasonality.put(month, value); - } - } - } catch (NumberFormatException e) { - // Skip invalid values - } - } - } - if (inTable && line.isEmpty()) { - break; - } - } - - // If parsing failed, generate default values - if (seasonality.isEmpty()) { - for (String month : months) { - seasonality.put(month, 50 + (int)(Math.random() * 30)); // Random between 50-80 - } - } - - return seasonality; - } - - private List> parseCompetitorsData(String response) { - List> competitors = new ArrayList<>(); - - String[] lines = response.split("\n"); - boolean inTable = false; - for (String line : lines) { - line = line.trim(); - if (line.contains("Конкурент") && line.contains("Охват аудитории")) { - inTable = true; - continue; - } - if (inTable && line.startsWith("|") && !line.startsWith("|---")) { - String[] parts = line.split("\\|"); - if (parts.length >= 7) { - Map competitor = new HashMap<>(); - competitor.put("name", parts[1].trim()); - - try { - String reachStr = parts[2].trim().replaceAll("[^0-9]", ""); - if (!reachStr.isEmpty()) { - competitor.put("reach", Integer.parseInt(reachStr)); - } else { - competitor.put("reach", 10000); - } - } catch (NumberFormatException e) { - competitor.put("reach", 10000); - } - - try { - String activityStr = parts[3].trim().replaceAll("[^0-9]", ""); - if (!activityStr.isEmpty()) { - int activity = Integer.parseInt(activityStr); - competitor.put("activity", Math.min(100, Math.max(0, activity))); - } else { - competitor.put("activity", 50); - } - } catch (NumberFormatException e) { - competitor.put("activity", 50); - } - - try { - String reviewsStr = parts[4].trim().replaceAll("[^0-9]", ""); - if (!reviewsStr.isEmpty()) { - competitor.put("reviews", Integer.parseInt(reviewsStr)); - } else { - competitor.put("reviews", 100); - } - } catch (NumberFormatException e) { - competitor.put("reviews", 100); - } - - competitor.put("strengths", parts[5].trim()); - competitor.put("weaknesses", parts[6].trim()); - - if (!competitor.get("name").toString().isEmpty()) { - competitors.add(competitor); - } - } - } - if (inTable && line.isEmpty() && !competitors.isEmpty()) { - break; - } - } - - // If parsing failed, add default competitor - if (competitors.isEmpty()) { - Map defaultCompetitor = new HashMap<>(); - defaultCompetitor.put("name", "Основной конкурент"); - defaultCompetitor.put("reach", 15000); - defaultCompetitor.put("activity", 65); - defaultCompetitor.put("reviews", 150); - defaultCompetitor.put("strengths", "Сильное присутствие в соцсетях"); - defaultCompetitor.put("weaknesses", "Высокая цена"); - competitors.add(defaultCompetitor); - } - - return competitors; - } - - private Map parseAudienceSegmentation(String response) { - Map segmentation = new HashMap<>(); - List> ageGroups = new ArrayList<>(); - List> genders = new ArrayList<>(); - List> segments = new ArrayList<>(); - - String[] lines = response.split("\n"); - boolean inAgeTable = false; - boolean inGenderTable = false; - boolean inSegmentsTable = false; - - for (String line : lines) { - line = line.trim(); - - if (line.contains("Возрастная группа") || line.contains("Возраст")) { - inAgeTable = true; - inGenderTable = false; - inSegmentsTable = false; - continue; - } - if (line.contains("Пол") && !line.contains("Возраст")) { - inAgeTable = false; - inGenderTable = true; - inSegmentsTable = false; - continue; - } - if (line.contains("Сегмент") || (line.contains("Сегменты") && line.contains("ЦА"))) { - inAgeTable = false; - inGenderTable = false; - inSegmentsTable = true; - continue; - } - - if (line.startsWith("|") && !line.startsWith("|---")) { - String[] parts = line.split("\\|"); - if (parts.length >= 3) { - String name = parts[1].trim(); - String percentStr = parts[2].trim().replaceAll("[^0-9]", ""); - - if (!name.isEmpty() && !percentStr.isEmpty()) { - try { - int percent = Integer.parseInt(percentStr); - Map item = new HashMap<>(); - item.put("label", name); - item.put("value", String.valueOf(percent)); - - if (inAgeTable) { - ageGroups.add(item); - } else if (inGenderTable) { - genders.add(item); - } else if (inSegmentsTable) { - segments.add(item); - } - } catch (NumberFormatException e) { - // Skip invalid values - } - } - } - } - } - - if (!ageGroups.isEmpty()) { - segmentation.put("ageGroups", ageGroups); - } - if (!genders.isEmpty()) { - segmentation.put("genders", genders); - } - if (!segments.isEmpty()) { - segmentation.put("segments", segments); - } - - // Add defaults if parsing failed - if (segmentation.isEmpty()) { - List> defaultAgeGroups = new ArrayList<>(); - defaultAgeGroups.add(Map.of("label", "18-25", "value", "25")); - defaultAgeGroups.add(Map.of("label", "26-35", "value", "35")); - defaultAgeGroups.add(Map.of("label", "36-45", "value", "30")); - defaultAgeGroups.add(Map.of("label", "46+", "value", "10")); - segmentation.put("ageGroups", defaultAgeGroups); - - List> defaultGenders = new ArrayList<>(); - defaultGenders.add(Map.of("label", "Мужской", "value", "55")); - defaultGenders.add(Map.of("label", "Женский", "value", "45")); - segmentation.put("genders", defaultGenders); - } - - return segmentation; - } - - private List> parseChannelsPotential(String response) { - List> channels = new ArrayList<>(); - String[] channelNames = {"Instagram", "TikTok", "Telegram", "2GIS", "Google Ads"}; - - String[] lines = response.split("\n"); - boolean inTable = false; - for (String line : lines) { - line = line.trim(); - if (line.contains("Канал") && line.contains("Потенциал")) { - inTable = true; - continue; - } - if (inTable && line.startsWith("|") && !line.startsWith("|---")) { - String[] parts = line.split("\\|"); - if (parts.length >= 4) { - String channelName = parts[1].trim(); - String potentialStr = parts[2].trim().replaceAll("[^0-9]", ""); - String justification = parts[3].trim(); - - if (!channelName.isEmpty()) { - Map channel = new HashMap<>(); - channel.put("name", channelName); - - try { - if (!potentialStr.isEmpty()) { - int potential = Integer.parseInt(potentialStr); - channel.put("potential", Math.min(100, Math.max(0, potential))); - } else { - channel.put("potential", 50); - } - } catch (NumberFormatException e) { - channel.put("potential", 50); - } - - channel.put("justification", justification.isEmpty() ? "Подходит для данной ниши" : justification); - channels.add(channel); - } - } - } - if (inTable && line.isEmpty() && !channels.isEmpty()) { - break; - } - } - - // If parsing failed, add default channels - if (channels.isEmpty()) { - for (String channelName : channelNames) { - Map channel = new HashMap<>(); - channel.put("name", channelName); - channel.put("potential", 50 + (int)(Math.random() * 40)); // Random between 50-90 - channel.put("justification", "Подходит для данной ниши"); - channels.add(channel); - } - } - - return channels; - } - - private List> parseConversionFunnel(String response) { - List> funnel = new ArrayList<>(); - String[] stages = {"Охват", "Вовлечённость", "Клики", "Переходы", "Заявки", "Покупки"}; - - String[] lines = response.split("\n"); - boolean inTable = false; - for (String line : lines) { - line = line.trim(); - if (line.contains("Этап") && line.contains("Значение")) { - inTable = true; - continue; - } - if (inTable && line.startsWith("|") && !line.startsWith("|---")) { - String[] parts = line.split("\\|"); - if (parts.length >= 4) { - String stage = parts[1].trim(); - String valueStr = parts[2].trim().replaceAll("[^0-9]", ""); - String conversionStr = parts[3].trim().replaceAll("[^0-9]", ""); - - if (!stage.isEmpty()) { - Map funnelStage = new HashMap<>(); - funnelStage.put("stage", stage); - - try { - if (!valueStr.isEmpty()) { - funnelStage.put("value", Integer.parseInt(valueStr)); - } else { - // Generate default decreasing values - int baseValue = 10000; - int index = funnel.size(); - funnelStage.put("value", (int)(baseValue * Math.pow(0.6, index))); - } - } catch (NumberFormatException e) { - int baseValue = 10000; - int index = funnel.size(); - funnelStage.put("value", (int)(baseValue * Math.pow(0.6, index))); - } - - try { - if (!conversionStr.isEmpty()) { - funnelStage.put("conversion", Integer.parseInt(conversionStr)); - } else { - // Default conversion rates - int[] defaultConversions = {100, 40, 25, 15, 8, 3}; - funnelStage.put("conversion", defaultConversions[Math.min(funnel.size(), defaultConversions.length - 1)]); - } - } catch (NumberFormatException e) { - int[] defaultConversions = {100, 40, 25, 15, 8, 3}; - funnelStage.put("conversion", defaultConversions[Math.min(funnel.size(), defaultConversions.length - 1)]); - } - - funnel.add(funnelStage); - } - } - } - if (inTable && line.isEmpty() && !funnel.isEmpty()) { - break; - } - } - - // If parsing failed, add default funnel - if (funnel.isEmpty()) { - int baseValue = 10000; - int[] conversions = {100, 40, 25, 15, 8, 3}; - for (int i = 0; i < stages.length; i++) { - Map funnelStage = new HashMap<>(); - funnelStage.put("stage", stages[i]); - funnelStage.put("value", (int)(baseValue * Math.pow(0.6, i))); - funnelStage.put("conversion", conversions[i]); - funnel.add(funnelStage); - } - } - - return funnel; - } - - private String generateUnifiedAnalysis(String context, MarketingAnalysisRequest request, DetailLevel detailLevel) { - if (detailLevel == null) { - detailLevel = DetailLevel.СТАНДАРТНО; // Default - } - - String lengthInstruction; - // Adjust length based on detail level - if (detailLevel == DetailLevel.КРАТКО) { - lengthInstruction = "Каждый раздел должен быть кратким (1-2 абзаца)."; - } else if (detailLevel == DetailLevel.ПОДРОБНО) { - lengthInstruction = "Каждый раздел должен быть подробным (5-8 абзацев) с глубоким анализом."; - } else { - lengthInstruction = "Каждый раздел должен быть структурированным (3-5 абзацев)."; - } - - // Build input data section - StringBuilder inputDataBuilder = new StringBuilder(); - inputDataBuilder.append("Название компании/продукта: ").append(request.getProduct()).append("\n"); - inputDataBuilder.append("Описание продукта: ").append(request.getProduct()).append("\n"); - - Map targetAudienceData = request.getTargetAudience(); - if (targetAudienceData != null) { - StringBuilder audienceBuilder = new StringBuilder(); - if (targetAudienceData.containsKey("genders")) { - @SuppressWarnings("unchecked") - List genders = (List) targetAudienceData.get("genders"); - if (genders != null && !genders.isEmpty()) { - audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". "); - } - } - if (targetAudienceData.containsKey("ageRanges")) { - @SuppressWarnings("unchecked") - List ageRanges = (List) targetAudienceData.get("ageRanges"); - if (ageRanges != null && !ageRanges.isEmpty()) { - audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". "); - } - } - if (targetAudienceData.containsKey("types")) { - @SuppressWarnings("unchecked") - List types = (List) targetAudienceData.get("types"); - if (types != null && !types.isEmpty()) { - audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". "); - } - } - inputDataBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n"); - } - - inputDataBuilder.append("Конкуренты: ").append(request.getBusinessNiche()).append(" (уточни основных конкурентов)\n"); - inputDataBuilder.append("Цель продвижения: ").append(request.getGoal()).append("\n"); - List regions = request.getRegion(); - if (regions != null && !regions.isEmpty()) { - inputDataBuilder.append("Регион/город: ").append(String.join(", ", regions)).append("\n"); - } - - // Build the new prompt according to documentation - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append("Ты — профессиональный маркетолог и аналитик, работающий с малым и средним бизнесом Казахстана. "); - promptBuilder.append("Твоя задача — выполнить глубокий маркетинговый анализ компании пользователя с генерацией всех необходимых количественных данных для построения графиков и диаграмм. "); - promptBuilder.append("Пиши структурированно, лаконично, без воды, понятным языком, ориентируясь на предпринимателя МСБ.\n\n"); - - promptBuilder.append("📥 Входные данные пользователя:\n"); - promptBuilder.append(inputDataBuilder.toString()); - promptBuilder.append("Если данных недостаточно — логично дополни, отметив: «Данные уточнены системой автоматически».\n\n"); - - promptBuilder.append("📊 Требования к числовым данным\n"); - promptBuilder.append("Ты обязан сгенерировать реалистичные числовые значения, подходящие для автоматической визуализации.\n\n"); - - promptBuilder.append("1️⃣ Сезонность спроса (линейный график, 12 месяцев)\n"); - promptBuilder.append("Выведи таблицу:\n"); - promptBuilder.append("| Месяц | Уровень спроса (0–100) |\n"); - promptBuilder.append("Значения должны:\n"); - promptBuilder.append("- соответствовать нише,\n"); - promptBuilder.append("- иметь сезонную динамику,\n"); - promptBuilder.append("- быть реалистичными.\n\n"); - - promptBuilder.append("2️⃣ Сравнительный анализ конкурентов (столбчатая диаграмма)\n"); - promptBuilder.append("Выведи таблицу:\n"); - promptBuilder.append("| Конкурент | Охват аудитории | Активность в соцсетях (0–100) | Количество отзывов | Сильные стороны | Слабые стороны |\n"); - promptBuilder.append("Охват — в диапазоне 1 000–50 000, Активность — 0–100.\n\n"); - - promptBuilder.append("3️⃣ Сегментация целевой аудитории (круговая диаграмма)\n"); - promptBuilder.append("Определи:\n"); - promptBuilder.append("- возрастные группы (%)\n"); - promptBuilder.append("- пол (%)\n"); - promptBuilder.append("- 3–5 ключевых сегментов ЦА (%)\n"); - promptBuilder.append("Сумма процентов = 100%.\n\n"); - - promptBuilder.append("4️⃣ Потенциал каналов продвижения (бар-чарт)\n"); - promptBuilder.append("Каналы: Instagram, TikTok, Telegram, 2GIS, Google Ads\n"); - promptBuilder.append("Выведи таблицу:\n"); - promptBuilder.append("| Канал | Потенциал (0–100) | Обоснование |\n\n"); - - promptBuilder.append("5️⃣ Конверсионная воронка (воронка спроса)\n"); - promptBuilder.append("Выведи таблицу:\n"); - promptBuilder.append("| Этап | Значение | Конверсия (%) |\n"); - promptBuilder.append("Этапы:\n"); - promptBuilder.append("- Охват\n"); - promptBuilder.append("- Вовлечённость\n"); - promptBuilder.append("- Клики\n"); - promptBuilder.append("- Переходы\n"); - promptBuilder.append("- Заявки\n"); - promptBuilder.append("- Покупки\n"); - promptBuilder.append("Числа должны уменьшаться логично.\n\n"); - - promptBuilder.append("📘 Теперь сформируй полный маркетинговый анализ:\n\n"); - - promptBuilder.append("I. Анализ продукта\n"); - promptBuilder.append("- ключевые свойства\n"); - promptBuilder.append("- преимущества\n"); - promptBuilder.append("- возможные слабости\n"); - promptBuilder.append("- ценность для рынка Казахстана\n\n"); - - promptBuilder.append("II. Анализ рынка Казахстана\n"); - promptBuilder.append("- динамика\n"); - promptBuilder.append("- тренды\n"); - promptBuilder.append("- сезонность (используй данные из таблицы)\n"); - promptBuilder.append("- возможности и ограничения\n\n"); - - promptBuilder.append("III. Анализ целевой аудитории\n"); - promptBuilder.append("- портрет покупателя\n"); - promptBuilder.append("- боли, потребности, мотивации\n"); - promptBuilder.append("- триггеры принятия решения\n"); - promptBuilder.append("- сегментация (вставить таблицу)\n\n"); - - promptBuilder.append("IV. Анализ конкурентов\n"); - promptBuilder.append("Вставить таблицу конкурентов. Сформировать вывод о конкурентной среде. Указать рыночные ниши, которые свободны.\n\n"); - - promptBuilder.append("V. SWOT-анализ\n"); - promptBuilder.append("Сформируй матрицу 2×2:\n"); - promptBuilder.append("- Strengths\n"); - promptBuilder.append("- Weaknesses\n"); - promptBuilder.append("- Opportunities\n"); - promptBuilder.append("- Threats\n\n"); - - promptBuilder.append("VI. Уникальное торговое предложение (УТП)\n"); - promptBuilder.append("Создай 1–2 варианта УТП. Требования:\n"); - promptBuilder.append("- ясно,\n"); - promptBuilder.append("- коротко,\n"); - promptBuilder.append("- конкретно,\n"); - promptBuilder.append("- опираясь на анализ продукта и конкурентов.\n\n"); - - promptBuilder.append("VII. Рекомендации по позиционированию\n"); - promptBuilder.append("Определи:\n"); - promptBuilder.append("- образ бренда\n"); - promptBuilder.append("- ключевое сообщение\n"); - promptBuilder.append("- стиль коммуникации\n"); - promptBuilder.append("- что важно подчеркнуть\n\n"); - - promptBuilder.append("VIII. Рекомендации по каналам продвижения\n"); - promptBuilder.append("Вставить таблицу потенциала каналов. Дать объяснение, почему каждый канал подходит для этой ниши.\n\n"); - - promptBuilder.append("IX. Рекомендации по контенту\n"); - promptBuilder.append("Приведи набор контент-направлений:\n"); - promptBuilder.append("- продающий\n"); - promptBuilder.append("- экспертный\n"); - promptBuilder.append("- визуальный\n"); - promptBuilder.append("- доверительный\n"); - promptBuilder.append("- развлекательный\n"); - promptBuilder.append("Дай примеры публикаций.\n\n"); - - promptBuilder.append("X. Итоговая стратегия продвижения\n"); - promptBuilder.append("Сформулируй чётко:\n"); - promptBuilder.append("- цель\n"); - promptBuilder.append("- ключевые шаги\n"); - promptBuilder.append("- что делать в первую очередь\n"); - promptBuilder.append("- что даст максимальный эффект\n"); - promptBuilder.append("- ожидаемый результат\n\n"); - - promptBuilder.append(lengthInstruction).append(" "); - promptBuilder.append("Ответ должен быть на русском языке.\n\n"); - promptBuilder.append(context); - - String prompt = promptBuilder.toString(); - return openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); - } - private String generateAnalysisByType(String context, AnalysisType analysisType, DetailLevel detailLevel) { if (analysisType == null) { analysisType = AnalysisType.РЫНОК; // Default @@ -1257,6 +1176,143 @@ public class MarketingAnalysisService { } markdown.append("\n"); } + + // SWOT table (if available from JSON) + @SuppressWarnings("unchecked") + Map swot = (Map) chartsData.get("swot"); + if (swot != null) { + markdown.append("### 6️⃣ SWOT-анализ\n\n"); + @SuppressWarnings("unchecked") + List strengths = (List) swot.get("strengths"); + @SuppressWarnings("unchecked") + List weaknesses = (List) swot.get("weaknesses"); + @SuppressWarnings("unchecked") + List opportunities = (List) swot.get("opportunities"); + @SuppressWarnings("unchecked") + List threats = (List) swot.get("threats"); + + markdown.append("| Сильные стороны | Слабые стороны |\n"); + markdown.append("|----------------|----------------|\n"); + markdown.append("| "); + if (strengths != null && !strengths.isEmpty()) { + markdown.append(String.join("; ", strengths)); + } + markdown.append(" | "); + if (weaknesses != null && !weaknesses.isEmpty()) { + markdown.append(String.join("; ", weaknesses)); + } + markdown.append(" |\n\n"); + + markdown.append("| Возможности | Угрозы |\n"); + markdown.append("|-------------|-------|\n"); + markdown.append("| "); + if (opportunities != null && !opportunities.isEmpty()) { + markdown.append(String.join("; ", opportunities)); + } + markdown.append(" | "); + if (threats != null && !threats.isEmpty()) { + markdown.append(String.join("; ", threats)); + } + markdown.append(" |\n\n"); + } + } + + // Add additional sections from JSON analysis if available + @SuppressWarnings("unchecked") + Map analysisJson = (Map) reportData.get("analysisJson"); + if (analysisJson != null && !analysisJson.isEmpty()) { + // Positioning section + @SuppressWarnings("unchecked") + Map positioning = (Map) analysisJson.get("positioning"); + if (positioning != null) { + markdown.append("## Позиционирование\n\n"); + if (positioning.containsKey("brandImage")) { + markdown.append("**Образ бренда:** ").append(positioning.get("brandImage")).append("\n\n"); + } + if (positioning.containsKey("keyMessage")) { + markdown.append("**Ключевое сообщение:** ").append(positioning.get("keyMessage")).append("\n\n"); + } + if (positioning.containsKey("toneOfVoice")) { + markdown.append("**Стиль коммуникации:** ").append(positioning.get("toneOfVoice")).append("\n\n"); + } + @SuppressWarnings("unchecked") + List trustFactors = (List) positioning.get("trustFactors"); + if (trustFactors != null && !trustFactors.isEmpty()) { + markdown.append("**Факторы доверия:**\n"); + for (String factor : trustFactors) { + markdown.append("- ").append(factor).append("\n"); + } + markdown.append("\n"); + } + } + + // Value Proposition section + @SuppressWarnings("unchecked") + Map valueProposition = (Map) analysisJson.get("valueProposition"); + if (valueProposition != null) { + markdown.append("## Уникальное торговое предложение (УТП)\n\n"); + @SuppressWarnings("unchecked") + List utpVariants = (List) valueProposition.get("utpVariants"); + if (utpVariants != null && !utpVariants.isEmpty()) { + markdown.append("**Варианты УТП:**\n"); + for (String utp : utpVariants) { + markdown.append("- ").append(utp).append("\n"); + } + markdown.append("\n"); + } + @SuppressWarnings("unchecked") + List mainBenefits = (List) valueProposition.get("mainBenefits"); + if (mainBenefits != null && !mainBenefits.isEmpty()) { + markdown.append("**Основные преимущества:**\n"); + for (String benefit : mainBenefits) { + markdown.append("- ").append(benefit).append("\n"); + } + markdown.append("\n"); + } + } + + // Content Recommendations section + @SuppressWarnings("unchecked") + Map contentRecommendations = (Map) analysisJson.get("contentRecommendations"); + if (contentRecommendations != null) { + markdown.append("## Рекомендации по контенту\n\n"); + @SuppressWarnings("unchecked") + List salesContent = (List) contentRecommendations.get("salesContent"); + if (salesContent != null && !salesContent.isEmpty()) { + markdown.append("### Продающий контент:\n"); + for (String content : salesContent) { + markdown.append("- ").append(content).append("\n"); + } + markdown.append("\n"); + } + @SuppressWarnings("unchecked") + List expertContent = (List) contentRecommendations.get("expertContent"); + if (expertContent != null && !expertContent.isEmpty()) { + markdown.append("### Экспертный контент:\n"); + for (String content : expertContent) { + markdown.append("- ").append(content).append("\n"); + } + markdown.append("\n"); + } + @SuppressWarnings("unchecked") + List trustContent = (List) contentRecommendations.get("trustContent"); + if (trustContent != null && !trustContent.isEmpty()) { + markdown.append("### Доверительный контент:\n"); + for (String content : trustContent) { + markdown.append("- ").append(content).append("\n"); + } + markdown.append("\n"); + } + @SuppressWarnings("unchecked") + List entertainmentContent = (List) contentRecommendations.get("entertainmentContent"); + if (entertainmentContent != null && !entertainmentContent.isEmpty()) { + markdown.append("### Развлекательный контент:\n"); + for (String content : entertainmentContent) { + markdown.append("- ").append(content).append("\n"); + } + markdown.append("\n"); + } + } } // Display full analysis with 10 sections