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