.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user