This commit is contained in:
arys
2026-01-17 17:33:40 +05:00
parent ace178274d
commit 99571b74a7
@@ -657,82 +657,48 @@ public class MarketingAnalysisService {
logger.info("Starting Marketing Analysis V2 (Big 4 Standard) for request: {}", request);
try {
// 1) Gather web evidence (Deep Search Strategy)
// Мы расширяем набор данных, чтобы найти реальные казахстанские инсайты
Map<String, Object> researchPack = buildResearchPack(request);
// 2) Resolve Competitors with strict filtering
// Фильтруем мусор, оставляем только реальные бренды для контекста
List<String> competitorCompanies = resolveCompetitorCompaniesForBenchmarks(request, researchPack, 5);
researchPack.put("competitor_companies", competitorCompanies);
// 3) Construct the "Big 4 Partner" System Prompt
// Здесь вся магия: задаем контекст, валюту, каналы и запрещаем галлюцинации.
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 systemPrompt = buildSystemPromptForKazakhstan();
String instruction = buildDetailedInstruction(request, competitorCompanies, structureExampleJson);
// 4) Build specific user instructions & constraints
String instruction = buildDetailedInstruction(request, competitorCompanies);
// 5) Prepare Context JSON
String contextJson = objectMapper.writeValueAsString(Map.of(
"request", request,
"evidence", researchPack
));
// 6) Call LLM (with retry logic and placeholder cleaning)
// Используем gpt-4o для максимального качества аналитики
String lastCleanedResponse = null;
for (int attempt = 1; attempt <= 2; attempt++) {
if (attempt > 1) {
// Если первый раз модель схалтурила, даем пинка
instruction += "\n\n!!! CRITICAL CORRECTION NEEDED !!!\n"
+ "В предыдущем ответе были замечены плейсхолдеры (EXAMPLE__) или невалидные данные.\n"
+ "Ты обязан использовать ТОЛЬКО реальные данные из evidence. Перегенерируй JSON.";
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel(
contextJson,
instruction,
"ru",
textModelName, // gpt-4o strictly
textModelName,
systemPrompt,
16000,
180000L
);
if (aiResponse == null || aiResponse.isBlank()) {
throw new IllegalStateException("OpenAI returned empty response for analysis v2");
}
String cleaned = cleanJsonFromMarkdown(aiResponse);
if (cleaned != null) {
cleaned = cleaned.replace("\uFEFF", "").trim();
}
lastCleanedResponse = cleaned;
// ... (очистка \uFEFF и валидация плейсхолдеров)
// Validate against placeholders
if (containsV2ExamplePlaceholders(cleaned)) {
logger.warn("generateAnalysisV2: Response contained placeholders (Attempt {}). Retrying...", attempt);
continue;
}
// 7) Parse and Return
try {
MarketingAnalysisResponseV2 parsed = objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class);
// Финальная полировка данных, если вдруг модель оставила ключи-заглушки
replacePlaceholderCompanyKeysIfNeeded(parsed, competitorCompanies);
return parsed;
} catch (Exception e) {
logger.error("JSON Parsing failed on attempt {}: {}", attempt, e.getMessage());
// Если это была последняя попытка - падаем, иначе пробуем снова
if (attempt == 2) throw e;
}
}
throw new IllegalStateException("Failed to generate valid analysis after retries.");
} catch (Exception e) {
@@ -740,7 +706,6 @@ public class MarketingAnalysisService {
throw new RuntimeException("Не удалось сгенерировать маркетинговый анализ V2: " + e.getMessage(), e);
}
}
/**
* Строит системный промпт, задающий ролевую модель и контекст Казахстана.
*/
@@ -771,69 +736,46 @@ public class MarketingAnalysisService {
/**
* Строит детальную инструкцию по заполнению полей JSON.
*/
private String buildDetailedInstruction(MarketingAnalysisRequest request, List<String> competitorCompanies) {
// Формируем строгую инструкцию по возрастным группам, если они заданы
private String buildDetailedInstruction(MarketingAnalysisRequest request, List<String> competitorCompanies, String structureExampleJson) {
String ageRangesInstruction = "";
Map<String, Object> targetAudienceData = request.getTargetAudience();
if (targetAudienceData != null && targetAudienceData.containsKey("ageRanges")) {
@SuppressWarnings("unchecked")
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
if (ageRanges != null && !ageRanges.isEmpty()) {
ageRangesInstruction = " - В поле `target_audience.age_structure` и `income_by_age` используй СТРОГО эти группы: "
+ String.join(", ", ageRanges) + ".\n";
}
}
// Пример JSON (структура), чтобы модель не ошиблась в типах данных
// Важно: мы не передаем values, только keys
String structureExample = buildJsonStructureExample();
// ... (логика формирования ageRangesInstruction остается прежней)
return String.format("""
Сгенерируй JSON объект `MarketingAnalysisResponseV2` для запроса:
Ниша: %s
Продукт: %s
Регион: %s
ИНСТРУКЦИИ ПО ЗАПОЛНЕНИЮ СЕКЦИЙ:
1. **ИСТОЧНИКИ (sources)**:
- В поле `sources` (массив строк) в корне JSON собери ВСЕ URL-ссылки, которые ты использовал для аналитики из предоставленного `evidence`.
- В текстах (conclusion, descriptions) ССЫЛКИ НЕ СТАВИТЬ. Текст должен быть чистым.
2. **КАНАЛЫ (acquisition_channels)**:
- Приоритеты для Казахстана:
1) **Instagram / TikTok** (Таргет, блогеры).
2) **Kaspi Marketing** (Для товарки - обязательно).
3) **Google Ads / SEO** (Поиск).
4) **2GIS** (Для офлайн точек).
- Забудь про Яндекс.Директ и ВКонтакте, если это не узкая B2B специфика.
3. **КОНКУРЕНТЫ**:
- Используй только эти названия (если они релевантны): %s.
- Если список пуст, найди лучших в `evidence.followups.competitors`.
- Не используй выдуманные названия типа "Магазин у дома".
4. **ДЕНЬГИ (marketing_economics)**:
- Все суммы в KZT (Тенге).
- Учитывай реальную стоимость клика (CPC) и лида (CPL) в Казахстане. Не занижай цены.
5. **ГРАФИКИ**:
- `monthly_online_index`: Учитывай сезонность (Наурыз, Back to School, Новый год, Каспи Жума).
- `income_by_age`: Доход в Тенге. Реальные зарплаты по данным Бюро Нацстатистики (evidence).
%s
ВАЖНО: Верни ТОЛЬКО ЧИСТЫЙ JSON. Без `Markdown`. Без комментариев.
Пример структуры (заполни его реальными данными из evidence):
%s
""",
Твоя задача: Провести глубокий маркетинговый анализ и вернуть его СТРОГО в формате JSON.
Ниша: %s
Продукт: %s
Регион: %s
ПРАВИЛА ЗАПОЛНЕНИЯ:
1. **СТРУКТУРА**: Ты обязан использовать структуру JSON, приведенную ниже.
- НЕ переименовывай ключи (например, используй `competitive_environment`, а не `competitors`).
- НЕ пропускай секции.
- Заполни все поля `EXAMPLE__` и `-9999` реальными данными и аналитическими оценками на основе `evidence`.
2. **ИСТОЧНИКИ (sources)**:
- В корневом поле `sources` перечисли ВСЕ реальные URL-адреса из `evidence`, которые ты использовал.
- Это обязательное требование для фронтенда.
3. **ЛОКАЛИЗАЦИЯ (Казахстан)**:
- Валюта: Тенге (KZT).
- Каналы: Instagram, Kaspi, 2GIS, TikTok.
- Данные: Опирайся на Бюро национальной статистики РК и локальные отчеты.
4. **ФОРМАТ**:
- Числа вместо -9999.
- Строки вместо EXAMPLE__.
- Только валидный JSON без Markdown.
%s
ЭТАЛОННАЯ СТРУКТУРА ДАННЫХ ДЛЯ ЗАПОЛНЕНИЯ:
%s
""",
request.getBusinessNiche(),
request.getProduct(),
request.getRegion() != null ? request.getRegion() : "Казахстан",
String.join(", ", competitorCompanies),
ageRangesInstruction,
structureExample
structureExampleJson
);
}