.
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.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -37,6 +41,18 @@ public class MarketingAnalysisService {
|
|||||||
private final ResearchPdfService researchPdfService;
|
private final ResearchPdfService researchPdfService;
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
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}")
|
@Value("${openai.model.name.text:gpt-4o}")
|
||||||
private String textModelName;
|
private String textModelName;
|
||||||
|
|
||||||
@@ -54,6 +70,15 @@ public class MarketingAnalysisService {
|
|||||||
this.researchPdfService = researchPdfService;
|
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) {
|
public List<MarketingAnalysis> startAnalysis(MarketingAnalysisRequest request, String userId) {
|
||||||
List<String> analysisTypes = request.getAnalysisType();
|
List<String> analysisTypes = request.getAnalysisType();
|
||||||
if (analysisTypes == null || analysisTypes.isEmpty()) {
|
if (analysisTypes == null || analysisTypes.isEmpty()) {
|
||||||
@@ -1708,6 +1733,21 @@ public class MarketingAnalysisService {
|
|||||||
if (fullJson.containsKey("valueProposition")) {
|
if (fullJson.containsKey("valueProposition")) {
|
||||||
context.put("valueProposition", fullJson.get("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;
|
break;
|
||||||
case "content":
|
case "content":
|
||||||
// Если contentRecommendations отсутствует, всё равно можно построить
|
// Если contentRecommendations отсутствует, всё равно можно построить
|
||||||
@@ -2005,6 +2045,16 @@ public class MarketingAnalysisService {
|
|||||||
*/
|
*/
|
||||||
private String genSectionCompetitors(Map<String, Object> contextJson) {
|
private String genSectionCompetitors(Map<String, Object> contextJson) {
|
||||||
try {
|
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);
|
String jsonString = objectMapper.writeValueAsString(contextJson);
|
||||||
StringBuilder promptBuilder = new StringBuilder();
|
StringBuilder promptBuilder = new StringBuilder();
|
||||||
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
||||||
@@ -2059,8 +2109,8 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("JSON ДАННЫЕ:\n");
|
promptBuilder.append("JSON ДАННЫЕ:\n");
|
||||||
promptBuilder.append(jsonString);
|
promptBuilder.append(jsonString);
|
||||||
|
|
||||||
String result = openAIAnalyticsService.generateWithInstructionWithModel(
|
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName,
|
||||||
jsonString, promptBuilder.toString(), "ru", textModelName);
|
2);
|
||||||
|
|
||||||
if (!isValidAiResponse(result)) {
|
if (!isValidAiResponse(result)) {
|
||||||
logger.warn("genSectionCompetitors: AI returned invalid or refusal response");
|
logger.warn("genSectionCompetitors: AI returned invalid or refusal response");
|
||||||
@@ -2142,6 +2192,14 @@ public class MarketingAnalysisService {
|
|||||||
*/
|
*/
|
||||||
private String genSectionChannels(Map<String, Object> contextJson) {
|
private String genSectionChannels(Map<String, Object> contextJson) {
|
||||||
try {
|
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);
|
String jsonString = objectMapper.writeValueAsString(contextJson);
|
||||||
StringBuilder promptBuilder = new StringBuilder();
|
StringBuilder promptBuilder = new StringBuilder();
|
||||||
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
||||||
@@ -2177,8 +2235,8 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("JSON ДАННЫЕ:\n");
|
promptBuilder.append("JSON ДАННЫЕ:\n");
|
||||||
promptBuilder.append(jsonString);
|
promptBuilder.append(jsonString);
|
||||||
|
|
||||||
String result = openAIAnalyticsService.generateWithInstructionWithModel(
|
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName,
|
||||||
jsonString, promptBuilder.toString(), "ru", textModelName);
|
2);
|
||||||
|
|
||||||
if (!isValidAiResponse(result)) {
|
if (!isValidAiResponse(result)) {
|
||||||
logger.warn("genSectionChannels: AI returned invalid or refusal response");
|
logger.warn("genSectionChannels: AI returned invalid or refusal response");
|
||||||
@@ -2261,6 +2319,20 @@ public class MarketingAnalysisService {
|
|||||||
*/
|
*/
|
||||||
private String genSectionPositioning(Map<String, Object> contextJson) {
|
private String genSectionPositioning(Map<String, Object> contextJson) {
|
||||||
try {
|
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);
|
String jsonString = objectMapper.writeValueAsString(contextJson);
|
||||||
StringBuilder promptBuilder = new StringBuilder();
|
StringBuilder promptBuilder = new StringBuilder();
|
||||||
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
||||||
@@ -2271,6 +2343,11 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
|
promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
|
||||||
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
|
||||||
promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\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("СТРУКТУРА РАЗДЕЛА:\n");
|
||||||
promptBuilder.append("IX. Рекомендации по позиционированию и УТП\n\n");
|
promptBuilder.append("IX. Рекомендации по позиционированию и УТП\n\n");
|
||||||
promptBuilder.append("Детально проанализируй:\n");
|
promptBuilder.append("Детально проанализируй:\n");
|
||||||
@@ -2289,8 +2366,8 @@ public class MarketingAnalysisService {
|
|||||||
promptBuilder.append("JSON ДАННЫЕ:\n");
|
promptBuilder.append("JSON ДАННЫЕ:\n");
|
||||||
promptBuilder.append(jsonString);
|
promptBuilder.append(jsonString);
|
||||||
|
|
||||||
String result = openAIAnalyticsService.generateWithInstructionWithModel(
|
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", miniModelName, textModelName,
|
||||||
jsonString, promptBuilder.toString(), "ru", miniModelName);
|
2);
|
||||||
|
|
||||||
if (!isValidAiResponse(result)) {
|
if (!isValidAiResponse(result)) {
|
||||||
logger.warn("genSectionPositioning: AI returned invalid or refusal response");
|
logger.warn("genSectionPositioning: AI returned invalid or refusal response");
|
||||||
@@ -3385,27 +3462,38 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
// Создаем CompletableFuture для каждого раздела
|
// Создаем CompletableFuture для каждого раздела
|
||||||
CompletableFuture<String> section1 = CompletableFuture
|
CompletableFuture<String> section1 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionSummary(prepareSectionContext(analysisJson, "summary")));
|
.supplyAsync(() -> genSectionSummary(prepareSectionContext(analysisJson, "summary")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section2 = CompletableFuture
|
CompletableFuture<String> section2 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionProduct(prepareSectionContext(analysisJson, "product")));
|
.supplyAsync(() -> genSectionProduct(prepareSectionContext(analysisJson, "product")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section3 = CompletableFuture
|
CompletableFuture<String> section3 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionMarket(prepareSectionContext(analysisJson, "market")));
|
.supplyAsync(() -> genSectionMarket(prepareSectionContext(analysisJson, "market")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section4 = CompletableFuture
|
CompletableFuture<String> section4 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionAudience(prepareSectionContext(analysisJson, "audience")));
|
.supplyAsync(() -> genSectionAudience(prepareSectionContext(analysisJson, "audience")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section5 = CompletableFuture
|
CompletableFuture<String> section5 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionCompetitors(prepareSectionContext(analysisJson, "competitors")));
|
.supplyAsync(() -> genSectionCompetitors(prepareSectionContext(analysisJson, "competitors")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section6 = CompletableFuture
|
CompletableFuture<String> section6 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionSWOT(prepareSectionContext(analysisJson, "swot")));
|
.supplyAsync(() -> genSectionSWOT(prepareSectionContext(analysisJson, "swot")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section7 = CompletableFuture
|
CompletableFuture<String> section7 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionChannels(prepareSectionContext(analysisJson, "channels")));
|
.supplyAsync(() -> genSectionChannels(prepareSectionContext(analysisJson, "channels")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section8 = CompletableFuture
|
CompletableFuture<String> section8 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionFunnel(prepareSectionContext(analysisJson, "funnel")));
|
.supplyAsync(() -> genSectionFunnel(prepareSectionContext(analysisJson, "funnel")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section9 = CompletableFuture
|
CompletableFuture<String> section9 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionPositioning(prepareSectionContext(analysisJson, "positioning")));
|
.supplyAsync(() -> genSectionPositioning(prepareSectionContext(analysisJson, "positioning")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section10 = CompletableFuture
|
CompletableFuture<String> section10 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionContent(prepareSectionContext(analysisJson, "content")));
|
.supplyAsync(() -> genSectionContent(prepareSectionContext(analysisJson, "content")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
CompletableFuture<String> section11 = CompletableFuture
|
CompletableFuture<String> section11 = CompletableFuture
|
||||||
.supplyAsync(() -> genSectionStrategy(prepareSectionContext(analysisJson, "strategy")));
|
.supplyAsync(() -> genSectionStrategy(prepareSectionContext(analysisJson, "strategy")),
|
||||||
|
sectionGenerationExecutor);
|
||||||
|
|
||||||
// Ждем завершения всех разделов
|
// Ждем завершения всех разделов
|
||||||
CompletableFuture.allOf(section1, section2, section3, section4, section5, section6,
|
CompletableFuture.allOf(section1, section2, section3, section4, section5, section6,
|
||||||
|
|||||||
Reference in New Issue
Block a user