fix
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user