This commit is contained in:
arys
2026-01-19 15:32:10 +05:00
parent 6222e5dcb6
commit 19925ca5b4
@@ -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 """