From 17ba04552c12e6be409b691eed9949cd8ff59e73 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Dec 2025 00:34:22 +0500 Subject: [PATCH] . --- .../service/MarketingAnalysisService.java | 122 +++++++++++++++--- 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index f171d91..36494a7 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -19,9 +19,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PreDestroy; + import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @Service @@ -37,6 +41,18 @@ public class MarketingAnalysisService { private final ResearchPdfService researchPdfService; private final ObjectMapper objectMapper = new ObjectMapper(); + /** + * Ограничиваем параллелизм генерации секций, чтобы не "забивать" семафор + * OpenAIAnalyticsService + * и не получать: "Timeout waiting for OpenAI request slot. Skipping request." + */ + private final ExecutorService sectionGenerationExecutor = Executors.newFixedThreadPool(2, r -> { + Thread t = new Thread(r); + t.setName("report-sections-" + t.getId()); + t.setDaemon(true); + return t; + }); + @Value("${openai.model.name.text:gpt-4o}") private String textModelName; @@ -54,6 +70,15 @@ public class MarketingAnalysisService { this.researchPdfService = researchPdfService; } + @PreDestroy + public void shutdownExecutors() { + try { + sectionGenerationExecutor.shutdown(); + } catch (Exception e) { + logger.warn("Failed to shutdown sectionGenerationExecutor: {}", e.getMessage()); + } + } + public List startAnalysis(MarketingAnalysisRequest request, String userId) { List analysisTypes = request.getAnalysisType(); if (analysisTypes == null || analysisTypes.isEmpty()) { @@ -1708,6 +1733,21 @@ public class MarketingAnalysisService { if (fullJson.containsKey("valueProposition")) { context.put("valueProposition", fullJson.get("valueProposition")); } + // Если явных полей позиционирования нет — всё равно даём опорный контекст для + // гипотез, + // но модель должна помечать это как гипотезы (см. промпт секции IX). + if (fullJson.containsKey("audienceAnalysis")) { + context.put("audienceAnalysis", fullJson.get("audienceAnalysis")); + } + if (fullJson.containsKey("competitorAnalysis")) { + context.put("competitorAnalysis", fullJson.get("competitorAnalysis")); + } + if (fullJson.containsKey("market")) { + context.put("market", fullJson.get("market")); + } + if (fullJson.containsKey("seasonality")) { + context.put("seasonality", fullJson.get("seasonality")); + } break; case "content": // Если contentRecommendations отсутствует, всё равно можно построить @@ -2005,6 +2045,16 @@ public class MarketingAnalysisService { */ private String genSectionCompetitors(Map contextJson) { try { + boolean hasCompetitorAnalysis = hasNonEmptyValue(contextJson, "competitorAnalysis"); + boolean hasLegacyCompetitors = hasNonEmptyValue(contextJson, "competitors") + || hasNonEmptyValue(contextJson, "comparisonTable") + || hasNonEmptyValue(contextJson, "marketShareChart"); + if (!hasCompetitorAnalysis && !hasLegacyCompetitors) { + return "V. Анализ конкурентов\n\n" + + "Раздел пропущен: отсутствуют данные конкурентов в JSON (competitorAnalysis/competitors). " + + "Нужно сгенерировать competitorAnalysis.marketShareChart и competitorAnalysis.comparisonTable."; + } + String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); @@ -2059,8 +2109,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = openAIAnalyticsService.generateWithInstructionWithModel( - jsonString, promptBuilder.toString(), "ru", textModelName); + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName, + 2); if (!isValidAiResponse(result)) { logger.warn("genSectionCompetitors: AI returned invalid or refusal response"); @@ -2142,6 +2192,14 @@ public class MarketingAnalysisService { */ private String genSectionChannels(Map contextJson) { try { + boolean hasChannels = hasNonEmptyValue(contextJson, "channels") + || hasNonEmptyValue(contextJson, "channelsPotential"); + if (!hasChannels) { + return "VII. Каналы продвижения и их потенциал\n\n" + + "Раздел пропущен: отсутствуют данные по каналам в JSON (channels/channelsPotential/strategyAnalysis.channelsPotential). " + + "Нужно сгенерировать strategyAnalysis.channelsPotential."; + } + String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); @@ -2177,8 +2235,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = openAIAnalyticsService.generateWithInstructionWithModel( - jsonString, promptBuilder.toString(), "ru", textModelName); + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName, + 2); if (!isValidAiResponse(result)) { logger.warn("genSectionChannels: AI returned invalid or refusal response"); @@ -2261,6 +2319,20 @@ public class MarketingAnalysisService { */ private String genSectionPositioning(Map contextJson) { try { + // Этот раздел может быть качественным. Если полей positioning/valueProposition + // нет, + // разрешаем только гипотезы + план тестирования, не выдавая их за факты. + boolean hasPositioning = hasNonEmptyValue(contextJson, "positioning"); + boolean hasValueProp = hasNonEmptyValue(contextJson, "valueProposition"); + boolean hasAnyBase = hasNonEmptyValue(contextJson, "product") + || hasNonEmptyValue(contextJson, "businessNiche") + || hasNonEmptyValue(contextJson, "audienceAnalysis") + || hasNonEmptyValue(contextJson, "competitorAnalysis"); + if (!hasPositioning && !hasValueProp && !hasAnyBase) { + return "IX. Рекомендации по позиционированию и УТП\n\n" + + "Раздел пропущен: в JSON отсутствуют данные для позиционирования (positioning/valueProposition) и нет базового контекста для гипотез."; + } + String jsonString = objectMapper.writeValueAsString(contextJson); StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n"); @@ -2271,6 +2343,11 @@ public class MarketingAnalysisService { promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n"); promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n"); promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n"); + promptBuilder.append("ВАЖНО ПРО НЕДОСТАЮЩИЕ ДАННЫЕ:\n"); + promptBuilder.append( + "- Если в JSON нет positioning и/или valueProposition: сформулируй 2-3 ГИПОТЕЗЫ позиционирования и УТП.\n"); + promptBuilder.append( + "- Каждую гипотезу помечай как \"Гипотеза\" и добавляй план проверки (A/B тест, оффер-тест, опрос), не выдавая её за факт.\n\n"); promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n"); promptBuilder.append("IX. Рекомендации по позиционированию и УТП\n\n"); promptBuilder.append("Детально проанализируй:\n"); @@ -2289,8 +2366,8 @@ public class MarketingAnalysisService { promptBuilder.append("JSON ДАННЫЕ:\n"); promptBuilder.append(jsonString); - String result = openAIAnalyticsService.generateWithInstructionWithModel( - jsonString, promptBuilder.toString(), "ru", miniModelName); + String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", miniModelName, textModelName, + 2); if (!isValidAiResponse(result)) { logger.warn("genSectionPositioning: AI returned invalid or refusal response"); @@ -3385,27 +3462,38 @@ public class MarketingAnalysisService { // Создаем CompletableFuture для каждого раздела CompletableFuture section1 = CompletableFuture - .supplyAsync(() -> genSectionSummary(prepareSectionContext(analysisJson, "summary"))); + .supplyAsync(() -> genSectionSummary(prepareSectionContext(analysisJson, "summary")), + sectionGenerationExecutor); CompletableFuture section2 = CompletableFuture - .supplyAsync(() -> genSectionProduct(prepareSectionContext(analysisJson, "product"))); + .supplyAsync(() -> genSectionProduct(prepareSectionContext(analysisJson, "product")), + sectionGenerationExecutor); CompletableFuture section3 = CompletableFuture - .supplyAsync(() -> genSectionMarket(prepareSectionContext(analysisJson, "market"))); + .supplyAsync(() -> genSectionMarket(prepareSectionContext(analysisJson, "market")), + sectionGenerationExecutor); CompletableFuture section4 = CompletableFuture - .supplyAsync(() -> genSectionAudience(prepareSectionContext(analysisJson, "audience"))); + .supplyAsync(() -> genSectionAudience(prepareSectionContext(analysisJson, "audience")), + sectionGenerationExecutor); CompletableFuture section5 = CompletableFuture - .supplyAsync(() -> genSectionCompetitors(prepareSectionContext(analysisJson, "competitors"))); + .supplyAsync(() -> genSectionCompetitors(prepareSectionContext(analysisJson, "competitors")), + sectionGenerationExecutor); CompletableFuture section6 = CompletableFuture - .supplyAsync(() -> genSectionSWOT(prepareSectionContext(analysisJson, "swot"))); + .supplyAsync(() -> genSectionSWOT(prepareSectionContext(analysisJson, "swot")), + sectionGenerationExecutor); CompletableFuture section7 = CompletableFuture - .supplyAsync(() -> genSectionChannels(prepareSectionContext(analysisJson, "channels"))); + .supplyAsync(() -> genSectionChannels(prepareSectionContext(analysisJson, "channels")), + sectionGenerationExecutor); CompletableFuture section8 = CompletableFuture - .supplyAsync(() -> genSectionFunnel(prepareSectionContext(analysisJson, "funnel"))); + .supplyAsync(() -> genSectionFunnel(prepareSectionContext(analysisJson, "funnel")), + sectionGenerationExecutor); CompletableFuture section9 = CompletableFuture - .supplyAsync(() -> genSectionPositioning(prepareSectionContext(analysisJson, "positioning"))); + .supplyAsync(() -> genSectionPositioning(prepareSectionContext(analysisJson, "positioning")), + sectionGenerationExecutor); CompletableFuture section10 = CompletableFuture - .supplyAsync(() -> genSectionContent(prepareSectionContext(analysisJson, "content"))); + .supplyAsync(() -> genSectionContent(prepareSectionContext(analysisJson, "content")), + sectionGenerationExecutor); CompletableFuture section11 = CompletableFuture - .supplyAsync(() -> genSectionStrategy(prepareSectionContext(analysisJson, "strategy"))); + .supplyAsync(() -> genSectionStrategy(prepareSectionContext(analysisJson, "strategy")), + sectionGenerationExecutor); // Ждем завершения всех разделов CompletableFuture.allOf(section1, section2, section3, section4, section5, section6,