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);
}
/**
* 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 """