This commit is contained in:
root
2025-12-20 00:34:22 +05:00
parent e5d7e5fb77
commit 17ba04552c
@@ -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<MarketingAnalysis> startAnalysis(MarketingAnalysisRequest request, String userId) {
List<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> section1 = CompletableFuture
.supplyAsync(() -> genSectionSummary(prepareSectionContext(analysisJson, "summary")));
.supplyAsync(() -> genSectionSummary(prepareSectionContext(analysisJson, "summary")),
sectionGenerationExecutor);
CompletableFuture<String> section2 = CompletableFuture
.supplyAsync(() -> genSectionProduct(prepareSectionContext(analysisJson, "product")));
.supplyAsync(() -> genSectionProduct(prepareSectionContext(analysisJson, "product")),
sectionGenerationExecutor);
CompletableFuture<String> section3 = CompletableFuture
.supplyAsync(() -> genSectionMarket(prepareSectionContext(analysisJson, "market")));
.supplyAsync(() -> genSectionMarket(prepareSectionContext(analysisJson, "market")),
sectionGenerationExecutor);
CompletableFuture<String> section4 = CompletableFuture
.supplyAsync(() -> genSectionAudience(prepareSectionContext(analysisJson, "audience")));
.supplyAsync(() -> genSectionAudience(prepareSectionContext(analysisJson, "audience")),
sectionGenerationExecutor);
CompletableFuture<String> section5 = CompletableFuture
.supplyAsync(() -> genSectionCompetitors(prepareSectionContext(analysisJson, "competitors")));
.supplyAsync(() -> genSectionCompetitors(prepareSectionContext(analysisJson, "competitors")),
sectionGenerationExecutor);
CompletableFuture<String> section6 = CompletableFuture
.supplyAsync(() -> genSectionSWOT(prepareSectionContext(analysisJson, "swot")));
.supplyAsync(() -> genSectionSWOT(prepareSectionContext(analysisJson, "swot")),
sectionGenerationExecutor);
CompletableFuture<String> section7 = CompletableFuture
.supplyAsync(() -> genSectionChannels(prepareSectionContext(analysisJson, "channels")));
.supplyAsync(() -> genSectionChannels(prepareSectionContext(analysisJson, "channels")),
sectionGenerationExecutor);
CompletableFuture<String> section8 = CompletableFuture
.supplyAsync(() -> genSectionFunnel(prepareSectionContext(analysisJson, "funnel")));
.supplyAsync(() -> genSectionFunnel(prepareSectionContext(analysisJson, "funnel")),
sectionGenerationExecutor);
CompletableFuture<String> section9 = CompletableFuture
.supplyAsync(() -> genSectionPositioning(prepareSectionContext(analysisJson, "positioning")));
.supplyAsync(() -> genSectionPositioning(prepareSectionContext(analysisJson, "positioning")),
sectionGenerationExecutor);
CompletableFuture<String> section10 = CompletableFuture
.supplyAsync(() -> genSectionContent(prepareSectionContext(analysisJson, "content")));
.supplyAsync(() -> genSectionContent(prepareSectionContext(analysisJson, "content")),
sectionGenerationExecutor);
CompletableFuture<String> section11 = CompletableFuture
.supplyAsync(() -> genSectionStrategy(prepareSectionContext(analysisJson, "strategy")));
.supplyAsync(() -> genSectionStrategy(prepareSectionContext(analysisJson, "strategy")),
sectionGenerationExecutor);
// Ждем завершения всех разделов
CompletableFuture.allOf(section1, section2, section3, section4, section5, section6,