From d344fecc94a952b6d2a4c0ccac1aa7149290ebbc Mon Sep 17 00:00:00 2001 From: root Date: Sun, 14 Dec 2025 21:59:19 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 219 ++++++++++-------- 1 file changed, 126 insertions(+), 93 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 3c1cffe..954d31d 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -13,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,6 +27,8 @@ import java.util.concurrent.TimeUnit; public class MarketingAnalysisService { private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class); + private static final int MAX_RETRY_ATTEMPTS = 3; + private static final long RETRY_DELAY_MS = 1000; private final MarketingAnalysisRepository repository; private final OpenAIAnalyticsService openAIAnalyticsService; @@ -635,107 +638,137 @@ public class MarketingAnalysisService { * Генерация анализа сезонности и рынка */ private Map getSeasonalityAndMarket(MarketingAnalysisRequest request) { - try { - String context = buildContextString(request); + String context = buildContextString(request); - StringBuilder promptBuilder = new StringBuilder(); - promptBuilder.append( - "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); - promptBuilder.append( - "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ сезонности и рынка и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); - promptBuilder.append( - "КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ сезонных колебаний спроса с конкретными объяснениями. " - + "Опиши рыночные тренды, возможности и угрозы с конкретными инсайтами, а не общими фразами.\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"); + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append( + "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n"); + promptBuilder.append( + "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ сезонности и рынка и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n"); + promptBuilder.append( + "КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ сезонных колебаний спроса с конкретными объяснениями. " + + "Опиши рыночные тренды, возможности и угрозы с конкретными инсайтами, а не общими фразами.\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"); + 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( - " \"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n"); - promptBuilder.append(" \"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n"); - promptBuilder.append( - " \"description\": \"Детальное описание сезонности (3-4 предложения). Опиши конкретные причины сезонных колебаний: " - + "праздники, климатические условия, культурные особенности, жизненные циклы целевой аудитории. " - + "Укажи, какие месяцы являются пиковыми и почему, какие - низкими и что это означает для бизнеса.\"\n"); - promptBuilder.append(" },\n"); - promptBuilder.append(" \"market\": {\n"); - promptBuilder.append( - " \"marketSize\": \"Оценка объема рынка (например, 'Оценка объема рынка составляет X млн тенге в год' или 'Средний рынок с потенциалом роста'). " - + "Дай конкретную оценку или описание масштаба рынка в Казахстане для данной ниши.\",\n"); - promptBuilder.append( - " \"growthRate\": \"Темп роста рынка (например, '+15% в год' или 'Стабильный рост 10-12% ежегодно')\",\n"); - promptBuilder.append( - " \"trends\": [\"Конкретный тренд 1 (1-2 предложения с деталями)\", \"Конкретный тренд 2 (1-2 предложения с деталями)\", \"Конкретный тренд 3 (1-2 предложения с деталями)\"],\n"); - promptBuilder.append( - " \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения с деталями)\", \"Конкретная возможность 2 (1-2 предложения с деталями)\"],\n"); - promptBuilder.append( - " \"threats\": [\"Конкретная угроза 1 (1-2 предложения с деталями)\", \"Конкретная угроза 2 (1-2 предложения с деталями)\"]\n"); - promptBuilder.append(" }\n"); - promptBuilder.append("}\n\n"); - promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n"); - promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n"); - promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n"); - promptBuilder - .append("3. Значения \"demandIndex\" должны быть от 0 до 100 (12 чисел для каждого месяца).\n"); - promptBuilder.append( - "4. Поле \"description\" в seasonality должно содержать ДЕТАЛЬНОЕ описание (3-4 предложения) с конкретными причинами сезонных колебаний.\n"); - promptBuilder.append( - "5. Поле \"marketSize\" должно содержать конкретную оценку объема рынка, а не просто слова 'средний' или 'крупный'.\n"); - promptBuilder.append( - "6. Массивы trends, opportunities, threats должны содержать КОНКРЕТНЫЕ инсайты (по 1-2 предложения каждый), а не общие фразы.\n"); - promptBuilder.append( - "7. Каждый элемент в trends должен описывать реальный тренд с конкретными деталями (например, 'Рост популярности онлайн-заказов на 30% за последний год').\n"); - promptBuilder.append( - "8. Каждый элемент в opportunities должен описывать конкретную возможность с практическими деталями.\n"); - promptBuilder.append( - "9. Каждый элемент в threats должен описывать конкретную угрозу с практическими деталями.\n"); - promptBuilder.append("10. Все строки — на русском языке.\n"); - promptBuilder.append( - "11. НЕ включай поле \"estimated\" ни в seasonality, ни в market.\n"); + promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n"); + promptBuilder.append("{\n"); + promptBuilder.append(" \"seasonality\": {\n"); + promptBuilder.append( + " \"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n"); + promptBuilder.append(" \"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n"); + promptBuilder.append( + " \"description\": \"Детальное описание сезонности (3-4 предложения). Опиши конкретные причины сезонных колебаний: " + + "праздники, климатические условия, культурные особенности, жизненные циклы целевой аудитории. " + + "Укажи, какие месяцы являются пиковыми и почему, какие - низкими и что это означает для бизнеса.\"\n"); + promptBuilder.append(" },\n"); + promptBuilder.append(" \"market\": {\n"); + promptBuilder.append( + " \"marketSize\": \"Оценка объема рынка (например, 'Оценка объема рынка составляет X млн тенге в год' или 'Средний рынок с потенциалом роста'). " + + "Дай конкретную оценку или описание масштаба рынка в Казахстане для данной ниши.\",\n"); + promptBuilder.append( + " \"growthRate\": \"Темп роста рынка (например, '+15% в год' или 'Стабильный рост 10-12% ежегодно')\",\n"); + promptBuilder.append( + " \"trends\": [\"Конкретный тренд 1 (1-2 предложения с деталями)\", \"Конкретный тренд 2 (1-2 предложения с деталями)\", \"Конкретный тренд 3 (1-2 предложения с деталями)\"],\n"); + promptBuilder.append( + " \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения с деталями)\", \"Конкретная возможность 2 (1-2 предложения с деталями)\"],\n"); + promptBuilder.append( + " \"threats\": [\"Конкретная угроза 1 (1-2 предложения с деталями)\", \"Конкретная угроза 2 (1-2 предложения с деталями)\"]\n"); + promptBuilder.append(" }\n"); + promptBuilder.append("}\n\n"); + promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n"); + promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n"); + promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n"); + promptBuilder + .append("3. Значения \"demandIndex\" должны быть от 0 до 100 (12 чисел для каждого месяца).\n"); + promptBuilder.append( + "4. Поле \"description\" в seasonality должно содержать ДЕТАЛЬНОЕ описание (3-4 предложения) с конкретными причинами сезонных колебаний.\n"); + promptBuilder.append( + "5. Поле \"marketSize\" должно содержать конкретную оценку объема рынка, а не просто слова 'средний' или 'крупный'.\n"); + promptBuilder.append( + "6. Массивы trends, opportunities, threats должны содержать КОНКРЕТНЫЕ инсайты (по 1-2 предложения каждый), а не общие фразы.\n"); + promptBuilder.append( + "7. Каждый элемент в trends должен описывать реальный тренд с конкретными деталями (например, 'Рост популярности онлайн-заказов на 30% за последний год').\n"); + promptBuilder.append( + "8. Каждый элемент в opportunities должен описывать конкретную возможность с практическими деталями.\n"); + promptBuilder.append( + "9. Каждый элемент в threats должен описывать конкретную угрозу с практическими деталями.\n"); + promptBuilder.append("10. Все строки — на русском языке.\n"); + promptBuilder.append( + "11. НЕ включай поле \"estimated\" ни в seasonality, ни в market.\n"); - String prompt = promptBuilder.toString(); - logger.debug( - "getSeasonalityAndMarket: Generating seasonality and market analysis with detailed prompt (length: {} chars)", - prompt.length()); - logger.info("getSeasonalityAndMarket: Calling OpenAI API for seasonality and market analysis"); - String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); - logger.debug("getSeasonalityAndMarket: OpenAI response received. Length: {} chars", - jsonResponse != null ? jsonResponse.length() : 0); + String prompt = promptBuilder.toString(); + logger.debug( + "getSeasonalityAndMarket: Generating seasonality and market analysis with detailed prompt (length: {} chars)", + prompt.length()); - if (jsonResponse == null || jsonResponse.trim().isEmpty()) { - logger.warn("getSeasonalityAndMarket: Empty JSON response from AI for seasonality and market analysis"); + // Retry logic for JsonParseException + int attempt = 0; + while (attempt < MAX_RETRY_ATTEMPTS) { + try { + attempt++; + logger.info( + "getSeasonalityAndMarket: Calling OpenAI API for seasonality and market analysis (attempt {}/{})", + attempt, MAX_RETRY_ATTEMPTS); + String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru"); + logger.debug("getSeasonalityAndMarket: OpenAI response received. Length: {} chars", + jsonResponse != null ? jsonResponse.length() : 0); + + if (jsonResponse == null || jsonResponse.trim().isEmpty()) { + logger.warn( + "getSeasonalityAndMarket: Empty JSON response from AI for seasonality and market analysis"); + return new HashMap<>(); + } + + logger.debug("getSeasonalityAndMarket: Cleaning JSON from markdown"); + String cleanedJson = cleanJsonFromMarkdown(jsonResponse); + logger.debug("getSeasonalityAndMarket: Parsing JSON response"); + Map jsonMap = objectMapper.readValue(cleanedJson, + new TypeReference>() { + }); + logger.debug("getSeasonalityAndMarket: JSON parsed successfully. Keys: {}", jsonMap.keySet()); + + logger.info("getSeasonalityAndMarket: Seasonality and market analysis generated successfully"); + return jsonMap; + } catch (JsonParseException e) { + logger.warn("getSeasonalityAndMarket: JsonParseException on attempt {}/{}: {}. Will retry...", + attempt, MAX_RETRY_ATTEMPTS, e.getMessage()); + if (attempt < MAX_RETRY_ATTEMPTS) { + try { + Thread.sleep(RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + logger.error("getSeasonalityAndMarket: Retry delay interrupted", ie); + return new HashMap<>(); + } + } else { + logger.error("getSeasonalityAndMarket: JsonParseException after {} attempts: {}", + MAX_RETRY_ATTEMPTS, e.getMessage(), e); + logger.error("getSeasonalityAndMarket: Stack trace: ", e); + return new HashMap<>(); + } + } catch (Exception e) { + logger.error("getSeasonalityAndMarket: Error generating seasonality and market analysis: {}", + e.getMessage(), e); + logger.error("getSeasonalityAndMarket: Stack trace: ", e); return new HashMap<>(); } - - logger.debug("getSeasonalityAndMarket: Cleaning JSON from markdown"); - String cleanedJson = cleanJsonFromMarkdown(jsonResponse); - logger.debug("getSeasonalityAndMarket: Parsing JSON response"); - Map jsonMap = objectMapper.readValue(cleanedJson, new TypeReference>() { - }); - logger.debug("getSeasonalityAndMarket: JSON parsed successfully. Keys: {}", jsonMap.keySet()); - - logger.info("getSeasonalityAndMarket: Seasonality and market analysis generated successfully"); - return jsonMap; - } catch (Exception e) { - logger.error("getSeasonalityAndMarket: Error generating seasonality and market analysis: {}", - e.getMessage(), e); - logger.error("getSeasonalityAndMarket: Stack trace: ", e); - return new HashMap<>(); } + + logger.error("getSeasonalityAndMarket: Failed after {} attempts", MAX_RETRY_ATTEMPTS); + return new HashMap<>(); } /**