fix
This commit is contained in:
@@ -444,11 +444,6 @@ public class MarketingAnalysisService {
|
|||||||
|| "strategy".equalsIgnoreCase(section);
|
|| "strategy".equalsIgnoreCase(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hybrid web search strategy:
|
|
||||||
* - one general "research pack" per report
|
|
||||||
* - plus section-specific followups (III/V/XI are later enriched via prompts)
|
|
||||||
*/
|
|
||||||
private Map<String, Object> buildResearchPack(MarketingAnalysisRequest request) {
|
private Map<String, Object> buildResearchPack(MarketingAnalysisRequest request) {
|
||||||
Map<String, Object> pack = new LinkedHashMap<>();
|
Map<String, Object> pack = new LinkedHashMap<>();
|
||||||
pack.put("generatedAt", LocalDateTime.now().toString());
|
pack.put("generatedAt", LocalDateTime.now().toString());
|
||||||
@@ -643,14 +638,16 @@ public class MarketingAnalysisService {
|
|||||||
logger.info("Starting Marketing Analysis V2 (Big 4 Standard) for request: {}", request);
|
logger.info("Starting Marketing Analysis V2 (Big 4 Standard) for request: {}", request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. Сбор данных
|
||||||
Map<String, Object> researchPack = buildResearchPack(request);
|
Map<String, Object> researchPack = buildResearchPack(request);
|
||||||
List<String> competitorCompanies = resolveCompetitorCompaniesForBenchmarks(request, researchPack, 5);
|
List<String> competitorCompanies = resolveCompetitorCompaniesForBenchmarks(request, researchPack, 5);
|
||||||
researchPack.put("competitor_companies", competitorCompanies);
|
researchPack.put("competitor_companies", competitorCompanies);
|
||||||
|
|
||||||
|
// 2. Пример JSON (Обновлен под КЗ)
|
||||||
MarketingAnalysisResponseV2 exampleDto = buildAnalysisV2ExampleForPrompt(request, competitorCompanies);
|
MarketingAnalysisResponseV2 exampleDto = buildAnalysisV2ExampleForPrompt(request, competitorCompanies);
|
||||||
exampleDto.setSources(List.of("https://stat.gov.kz/example", "https://forbes.kz/example"));
|
|
||||||
String structureExampleJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(exampleDto);
|
String structureExampleJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(exampleDto);
|
||||||
|
|
||||||
|
// 3. Формирование промпта (Безопасный метод!)
|
||||||
String systemPrompt = buildSystemPromptForKazakhstan();
|
String systemPrompt = buildSystemPromptForKazakhstan();
|
||||||
String instruction = buildDetailedInstruction(request, competitorCompanies, structureExampleJson);
|
String instruction = buildDetailedInstruction(request, competitorCompanies, structureExampleJson);
|
||||||
|
|
||||||
@@ -661,23 +658,32 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
// Вызов AI
|
||||||
String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||||
contextJson,
|
contextJson,
|
||||||
instruction,
|
instruction,
|
||||||
"ru",
|
"ru",
|
||||||
textModelName,
|
textModelName, // GPT-4o
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
16000,
|
16000,
|
||||||
180000L
|
180000L
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (aiResponse == null || aiResponse.isBlank()) {
|
||||||
|
throw new IllegalStateException("OpenAI returned empty response");
|
||||||
|
}
|
||||||
|
|
||||||
String cleaned = cleanJsonFromMarkdown(aiResponse);
|
String cleaned = cleanJsonFromMarkdown(aiResponse);
|
||||||
|
if (cleaned != null) cleaned = cleaned.replace("\uFEFF", "").trim();
|
||||||
|
|
||||||
|
|
||||||
MarketingAnalysisResponseV2 parsed = objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class);
|
MarketingAnalysisResponseV2 parsed = objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class);
|
||||||
replacePlaceholderCompanyKeysIfNeeded(parsed, competitorCompanies);
|
replacePlaceholderCompanyKeysIfNeeded(parsed, competitorCompanies);
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("JSON Parsing/Generation failed on attempt {}: {}", attempt, e.getMessage());
|
logger.error("JSON Parsing/Generation failed on attempt {}: {}", attempt, e.getMessage());
|
||||||
if (attempt == 2) throw e;
|
if (attempt == 2) throw e;
|
||||||
|
Thread.sleep(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Failed to generate valid analysis after retries.");
|
throw new IllegalStateException("Failed to generate valid analysis after retries.");
|
||||||
@@ -699,54 +705,68 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
private String buildDetailedInstruction(MarketingAnalysisRequest request, List<String> competitorCompanies, String structureExampleJson) {
|
private String buildDetailedInstruction(MarketingAnalysisRequest request, List<String> competitorCompanies, String structureExampleJson) {
|
||||||
|
|
||||||
List<String> regions = request.getRegion();
|
String regionsList = (request.getRegion() != null && !request.getRegion().isEmpty())
|
||||||
String regionsInstruction;
|
? String.join(", ", request.getRegion())
|
||||||
if (regions != null && !regions.isEmpty()) {
|
: "Алматы, Астана, Шымкент"; String ageRangesInstruction = "";
|
||||||
String regionsList = String.join(", ", regions);
|
if (request.getTargetAudience() != null && request.getTargetAudience().containsKey("ageRanges")) {
|
||||||
regionsInstruction = "ГЕОГРАФИЯ (СТРОГО ОБЯЗАТЕЛЬНО):\n" +
|
@SuppressWarnings("unchecked")
|
||||||
"В секции 'market_overview.geography' ты ОБЯЗАН вернуть данные ДЛЯ КАЖДОГО города из этого списка: [" + regionsList + "].\n" +
|
List<String> ranges = (List<String>) request.getTargetAudience().get("ageRanges");
|
||||||
"НЕ ПРОПУСКАЙ ни одного города из списка. Значение (0-100) должно отражать относительный спрос.";
|
if (ranges != null && !ranges.isEmpty()) {
|
||||||
} else {
|
ageRangesInstruction = "Используй ТОЛЬКО эти возрастные группы: " + String.join(", ", ranges);
|
||||||
regionsInstruction = "ГЕОГРАФИЯ: Алматы, Астана, Шымкент (по умолчанию).";
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String channelsInstruction = "КАНАЛЫ (РК SPECS):\n" +
|
String template = """
|
||||||
"- В 'acquisition_channels' обязательно включи: Instagram, TikTok, Kaspi Marketing, 2GIS, Google Ads.\n" +
|
Твоя задача: Провести глубокий маркетинговый анализ и вернуть его СТРОГО в формате JSON.
|
||||||
"- ЗАПРЕЩЕНО включать или 'ВКонтакте' как платные каналы (они не работают в РК). Если они есть в примере - удали их.\n" +
|
|
||||||
"- Оценивай эффективность реально: 2GIS работает локально, Kaspi дает горячих лидов.";
|
|
||||||
|
|
||||||
String audienceInstruction = "АУДИТОРИЯ:\n" +
|
ВХОДНЫЕ ДАННЫЕ:
|
||||||
"- Следи, чтобы возрастные группы в 'age_structure' НЕ пересекались по логике, но если входные данные пересекаются - нормализуй их так, чтобы сумма % была равна 100%.\n" +
|
Ниша: {{NICHE}}
|
||||||
"- Поле 'income_by_age' ОБЯЗАТЕЛЬНО для заполнения (средний доход в KZT).";
|
Продукт: {{PRODUCT}}
|
||||||
|
|
||||||
return String.format("""
|
-----------------------------------------------------------------------
|
||||||
Твоя задача: Провести глубокий маркетинговый анализ для рынка Казахстана и вернуть JSON.
|
КРИТИЧЕСКИЕ ИНСТРУКЦИИ ПО КАЗАХСТАНУ (KZ):
|
||||||
Ниша: %s
|
|
||||||
Продукт: %s
|
|
||||||
|
|
||||||
%s
|
1. ГЕОГРАФИЯ (market_overview.geography):
|
||||||
|
- Ты ОБЯЗАН включить в анализ ВСЕ следующие города: [{{REGIONS}}].
|
||||||
|
- Не ограничивайся тремя городами, если в списке их больше.
|
||||||
|
- Значения (0-100) должны быть разными и неровными (например, 87, 64, 43).
|
||||||
|
|
||||||
%s
|
2. КОНКУРЕНТЫ (competitive_environment):
|
||||||
|
- Найди в `evidence` РЕАЛЬНЫХ лидеров (сети, крупные игроки из 2GIS/Kaspi).
|
||||||
|
- Игнорируй мелкие ИП или выдуманные названия.
|
||||||
|
- Заполни `digital_visibility_benchmarks` реальными именами.
|
||||||
|
|
||||||
%s
|
3. КАНАЛЫ (acquisition_channels):
|
||||||
|
- Приоритет в РК: Instagram (Reels), TikTok, Kaspi Marketing, 2GIS.
|
||||||
|
- Яндекс.Директ и VK ставь ниже (Low/Medium), если это B2C.
|
||||||
|
- Указывай стоимость лида (CPL) неровными числами (напр. 2350 KZT).
|
||||||
|
|
||||||
ПРАВИЛА ЦИФР И ЭКОНОМИКИ:
|
4. ЦИФРЫ И ГРАФИКИ:
|
||||||
1. Все цены в KZT.
|
- ЗАПРЕЩЕНО использовать круглые числа (20%, 50%, 1000). Пиши: 18.7%, 42.3%, 1240.
|
||||||
2. Округляй до красивых чисел (например, 15 000, а не 14 321).
|
- income_by_age: Заполни реальными доходами в KZT по возрастам (из evidence).
|
||||||
3. Прибыльность: Будь реалистом. Маржа в офлайн-бизнесе редко превышает 20-30%. Не рисуй сверхприбыли.
|
- monthly_online_index: График должен быть "живым" (не прямая линия).
|
||||||
4. В полях cost/value пиши строку с валютой (например "5 000 KZT").
|
|
||||||
5. В полях status/efficiency пиши понятные оценки: "High Impact", "Low Efficiency", "Moderate".
|
|
||||||
|
|
||||||
ЭТАЛОННАЯ СТРУКТУРА JSON (Snake Case):
|
5. ИСТОЧНИКИ:
|
||||||
%s
|
- Собери ВСЕ ссылки из `evidence` в ОДИН СПИСОК в самом конце (поле `recommendations.conclusion`).
|
||||||
""",
|
- Не вставляй ссылки внутрь текста разделов.
|
||||||
request.getBusinessNiche(),
|
|
||||||
request.getProduct(),
|
-----------------------------------------------------------------------
|
||||||
regionsInstruction,
|
{{AGE_INSTRUCTION}}
|
||||||
channelsInstruction,
|
|
||||||
audienceInstruction,
|
ФОРМАТ ОТВЕТА:
|
||||||
structureExampleJson
|
- Только валидный JSON.
|
||||||
);
|
- Заполни все поля `EXAMPLE__` и `-9999` реальными данными.
|
||||||
|
|
||||||
|
ЭТАЛОННАЯ СТРУКТУРА:
|
||||||
|
{{JSON_STRUCTURE}}
|
||||||
|
""";
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace("{{NICHE}}", request.getBusinessNiche())
|
||||||
|
.replace("{{PRODUCT}}", request.getProduct())
|
||||||
|
.replace("{{REGIONS}}", regionsList)
|
||||||
|
.replace("{{AGE_INSTRUCTION}}", ageRangesInstruction)
|
||||||
|
.replace("{{JSON_STRUCTURE}}", structureExampleJson);
|
||||||
}
|
}
|
||||||
private String buildJsonStructureExample() {
|
private String buildJsonStructureExample() {
|
||||||
return """
|
return """
|
||||||
|
|||||||
Reference in New Issue
Block a user