.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
Привет! Я хочу провести глубокий рефакторинг метода generateJsonAnalysis в классе MarketingAnalysisService.java.
|
||||
|
||||
У нас нет ограничений по токенам, но есть жесткое требование к качеству и детализации данных. Текущий метод пытается получить всё за один запрос, из-за чего страдает глубина.
|
||||
|
||||
ЗАДАЧА:
|
||||
Перепиши метод generateJsonAnalysis так, чтобы он выполнял агрегацию данных из нескольких отдельных запросов к OpenAI.
|
||||
Метод должен вызывать приватные helper-методы (создай их), каждый из которых отвечает за свою часть и возвращает Map<String, Object>.
|
||||
|
||||
СТРУКТУРА НОВОГО МЕТОДА:
|
||||
|
||||
1. Вызвать getAudienceAnalysis(request) -> возвращает секцию "audienceAnalysis"
|
||||
2. Вызвать getCompetitorAnalysis(request) -> возвращает секцию "competitorAnalysis"
|
||||
3. Вызвать getSeasonalityAndMarket(request) -> возвращает секцию "seasonality" и "market"
|
||||
4. Объединить результаты в один Map<String, Object> и вернуть его.
|
||||
|
||||
ДЕТАЛИЗАЦИЯ HELPER-МЕТОДОВ (Промпты внутри них):
|
||||
|
||||
--- Метод 1: getAudienceAnalysis ---
|
||||
Промпт должен требовать JSON строго такой структуры (для отрисовки виджетов на фронте):
|
||||
{
|
||||
"audienceAnalysis": {
|
||||
"ageGroups": { "18-24": 15, "25-34": 40, "35-44": 30, "45+": 15 }, // Pie chart data
|
||||
"ageComment": "Текст вывода под графиком возраста (1-2 предложения)",
|
||||
"genderDistribution": { "Женщины": 60, "Мужчины": 40 }, // Donut chart
|
||||
"genderComment": "Текст вывода под графиком пола",
|
||||
"segments": [ // Список из 4 детальных карточек сегментов
|
||||
{
|
||||
"name": "Название (например 'Молодые профессионалы')",
|
||||
"sharePercent": 35,
|
||||
"ageRange": "25-34",
|
||||
"incomeLevel": "Средний+",
|
||||
"motivation": "Ключевая мотивация покупки",
|
||||
"triggers": "Триггеры (на что реагируют)",
|
||||
"primaryChannels": ["Instagram", "TikTok"], // Топ каналы для этого сегмента
|
||||
"secondaryChannels": ["Telegram"]
|
||||
}
|
||||
],
|
||||
"segmentsChannelMatrix": [ // Для таблицы heatmap: Сегмент vs Канал
|
||||
{ "segmentName": "Молодые профи", "instagram": "high", "telegram": "medium", "youtube": "low" }
|
||||
],
|
||||
"keyTakeaways": ["Вывод 1", "Вывод 2", "Вывод 3"]
|
||||
}
|
||||
}
|
||||
|
||||
--- Метод 2: getCompetitorAnalysis ---
|
||||
Промпт должен требовать JSON:
|
||||
{
|
||||
"competitorAnalysis": {
|
||||
"marketShareChart": [ // Оценочные доли рынка для графика
|
||||
{ "name": "Наш бренд", "value": 25 },
|
||||
{ "name": "Конкурент А", "value": 30 },
|
||||
{ "name": "Конкурент B", "value": 20 },
|
||||
{ "name": "Другие", "value": 25 }
|
||||
],
|
||||
"marketShareComment": "Вывод по доле рынка",
|
||||
"competitorsDetails": [ // Детали по 3-4 конкурентам
|
||||
{
|
||||
"name": "Конкурент А",
|
||||
"priceStrategy": "Высокая/Средняя/Низкая",
|
||||
"digitalMetrics": { "reach": 50000, "followers": 12000, "activityIndex": 85 },
|
||||
"channelsHeatmap": { // Оценка (0-100) качества ведения канала
|
||||
"Instagram": 90, "TikTok": 20, "Telegram": 70, "YouTube": 40
|
||||
}
|
||||
}
|
||||
],
|
||||
"comparisonTable": [ // Сравнительная таблица (5-6 характеристик)
|
||||
{ "feature": "Ценовая политика", "us": "Средняя", "compA": "Высокая", "compB": "Низкая" },
|
||||
{ "feature": "УТП", "us": "Сервис", "compA": "Бренд", "compB": "Дешевизна" }
|
||||
],
|
||||
"swotCompetitors": { // Краткий SWOT по рынку в целом
|
||||
"strengths": ["..."], "weaknesses": ["..."]
|
||||
},
|
||||
"keyInsights": ["Вывод 1", "Вывод 2"]
|
||||
}
|
||||
}
|
||||
|
||||
ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ:
|
||||
|
||||
1. Используй существующий `openAIAnalyticsService.generateWithInstruction` для вызовов.
|
||||
2. В каждом helper-методе добавь отдельный `try-catch` и логирование, чтобы падение одной части не ломало весь отчет (возвращай пустую Map или stub-данные при ошибке).
|
||||
3. Используй `objectMapper` для парсинга ответов.
|
||||
4. Убедись, что JSON очищается от markdown-тегов (```json) перед парсингом.
|
||||
|
||||
Пожалуйста, полностью реализуй этот модульный подход.
|
||||
@@ -140,198 +140,341 @@ public class MarketingAnalysisService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Промпт №1: Генерация структурированного JSON с данными для графиков
|
||||
* Генерация структурированного JSON с данными для графиков
|
||||
* Использует модульный подход: три отдельных запроса к OpenAI для повышения
|
||||
* детализации
|
||||
*/
|
||||
private Map<String, Object> generateJsonAnalysis(MarketingAnalysisRequest request) {
|
||||
// Форматирование целевой аудитории
|
||||
StringBuilder targetAudienceBuilder = new StringBuilder();
|
||||
Map<String, Object> targetAudienceData = request.getTargetAudience();
|
||||
if (targetAudienceData != null) {
|
||||
if (targetAudienceData.containsKey("genders")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> genders = (List<String>) targetAudienceData.get("genders");
|
||||
if (genders != null && !genders.isEmpty()) {
|
||||
targetAudienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
|
||||
}
|
||||
}
|
||||
if (targetAudienceData.containsKey("ageRanges")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
|
||||
if (ageRanges != null && !ageRanges.isEmpty()) {
|
||||
targetAudienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
|
||||
}
|
||||
}
|
||||
if (targetAudienceData.containsKey("types")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> types = (List<String>) targetAudienceData.get("types");
|
||||
if (types != null && !types.isEmpty()) {
|
||||
targetAudienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
|
||||
}
|
||||
}
|
||||
}
|
||||
String targetAudienceStr = targetAudienceBuilder.toString().trim();
|
||||
if (targetAudienceStr.isEmpty()) {
|
||||
targetAudienceStr = "Не указана";
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 1. Получаем анализ целевой аудитории
|
||||
logger.info("Starting audience analysis generation");
|
||||
Map<String, Object> audienceAnalysis = getAudienceAnalysis(request);
|
||||
if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) {
|
||||
result.putAll(audienceAnalysis);
|
||||
logger.info("Audience analysis generated successfully");
|
||||
} else {
|
||||
logger.warn("Audience analysis returned empty result");
|
||||
}
|
||||
|
||||
// Форматирование региона
|
||||
List<String> regions = request.getRegion();
|
||||
String regionStr = (regions != null && !regions.isEmpty())
|
||||
? String.join(", ", regions)
|
||||
: "Не указан";
|
||||
// 2. Получаем анализ конкурентов
|
||||
logger.info("Starting competitor analysis generation");
|
||||
Map<String, Object> competitorAnalysis = getCompetitorAnalysis(request);
|
||||
if (competitorAnalysis != null && !competitorAnalysis.isEmpty()) {
|
||||
result.putAll(competitorAnalysis);
|
||||
logger.info("Competitor analysis generated successfully");
|
||||
} else {
|
||||
logger.warn("Competitor analysis returned empty result");
|
||||
}
|
||||
|
||||
// Построение промпта №1
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append(
|
||||
"Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
|
||||
promptBuilder.append(
|
||||
"Твоя задача — на основе входных данных о бизнесе пользователя выполнить маркетинговый анализ и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
|
||||
promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
|
||||
promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
|
||||
promptBuilder.append("---\n\n");
|
||||
promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
|
||||
promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
|
||||
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
|
||||
.append(request.getBusinessNiche()).append(")\n");
|
||||
promptBuilder.append("- Целевая аудитория (если указана): ").append(targetAudienceStr).append("\n");
|
||||
promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche())
|
||||
.append(" (уточни основных конкурентов)\n");
|
||||
promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n");
|
||||
promptBuilder.append("- Регион/город: ").append(regionStr).append("\n\n");
|
||||
promptBuilder.append(
|
||||
"## Если каких-то данных не хватает, ты можешь логично дополнить предположениями, НО в таких полях добавь флаг \"estimated\": true.\n\n");
|
||||
promptBuilder.append("СФОРМИРУЙ ОДИН JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"meta\": {\n");
|
||||
promptBuilder.append("\"companyName\": string,\n");
|
||||
promptBuilder.append("\"region\": string,\n");
|
||||
promptBuilder.append("\"primaryGoal\": string,\n");
|
||||
promptBuilder.append("\"notes\": string\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"seasonality\": {\n");
|
||||
promptBuilder.append("\"description\": string,\n");
|
||||
promptBuilder.append("\"months\": [ \"Январь\", \"Февраль\", ... 12 месяцев ],\n");
|
||||
promptBuilder.append("\"demandIndex\": [12 чисел от 0 до 100],\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"competitors\": [\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"name\": string,\n");
|
||||
promptBuilder.append("\"reach\": integer, // 1000–50000\n");
|
||||
promptBuilder.append("\"socialActivityIndex\": integer, // 0–100\n");
|
||||
promptBuilder.append("\"reviewsCount\": integer, // 0–5000\n");
|
||||
promptBuilder.append("\"strengths\": [string],\n");
|
||||
promptBuilder.append("\"weaknesses\": [string],\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("],\n\n");
|
||||
promptBuilder.append("\"audienceSegments\": {\n");
|
||||
promptBuilder.append("\"description\": string,\n");
|
||||
promptBuilder.append("\"segments\": [\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"name\": string,\n");
|
||||
promptBuilder.append("\"sharePercent\": integer, // сумма всех sharePercent = 100\n");
|
||||
promptBuilder.append("\"ageRange\": string,\n");
|
||||
promptBuilder.append("\"gender\": string, // \"мужчины\", \"женщины\", \"смешанная\"\n");
|
||||
promptBuilder.append("\"incomeLevel\": string, // например: \"низкий\", \"средний\", \"выше среднего\"\n");
|
||||
promptBuilder.append("\"keyNeeds\": [string],\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("]\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"channels\": [\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append(
|
||||
"\"name\": string, // \"Instagram\", \"TikTok\", \"Telegram\", \"2GIS\", \"Google Ads\" и т.п.\n");
|
||||
promptBuilder.append("\"potentialIndex\": integer, // 0–100\n");
|
||||
promptBuilder.append("\"priorityLevel\": string, // \"высокий\", \"средний\", \"низкий\"\n");
|
||||
promptBuilder.append("\"roleInStrategy\": string, // коротко: для чего этот канал\n");
|
||||
promptBuilder.append("\"justification\": string, // почему этот канал важен\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("],\n\n");
|
||||
promptBuilder.append("\"funnel\": {\n");
|
||||
promptBuilder.append("\"description\": string,\n");
|
||||
promptBuilder.append("\"stages\": [\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append(
|
||||
"\"stageName\": string, // \"Охват\", \"Вовлечённость\", \"Клики\", \"Переходы\", \"Заявки\", \"Покупки\"\n");
|
||||
promptBuilder.append("\"value\": integer, // количество (должно логично уменьшаться вниз по воронке)\n");
|
||||
promptBuilder.append("\"conversionPercent\": number // от предыдущего этапа, с одним знаком после запятой\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("],\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"swot\": {\n");
|
||||
promptBuilder.append("\"strengths\": [string],\n");
|
||||
promptBuilder.append("\"weaknesses\": [string],\n");
|
||||
promptBuilder.append("\"opportunities\": [string],\n");
|
||||
promptBuilder.append("\"threats\": [string]\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"positioning\": {\n");
|
||||
promptBuilder.append("\"brandImage\": string,\n");
|
||||
promptBuilder.append("\"keyMessage\": string,\n");
|
||||
promptBuilder.append("\"toneOfVoice\": string,\n");
|
||||
promptBuilder.append("\"trustFactors\": [string]\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"valueProposition\": {\n");
|
||||
promptBuilder.append("\"utpVariants\": [string], // 1–3 варианта УТП\n");
|
||||
promptBuilder.append("\"mainBenefits\": [string]\n");
|
||||
promptBuilder.append("},\n\n");
|
||||
promptBuilder.append("\"contentRecommendations\": {\n");
|
||||
promptBuilder.append("\"salesContent\": [string],\n");
|
||||
promptBuilder.append("\"expertContent\": [string],\n");
|
||||
promptBuilder.append("\"trustContent\": [string],\n");
|
||||
promptBuilder.append("\"entertainmentContent\": [string]\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("}\n\n");
|
||||
promptBuilder.append("ТРЕБОВАНИЯ:\n\n");
|
||||
promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n");
|
||||
promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n");
|
||||
promptBuilder.append("- Сумма \"sharePercent\" по audienceSegments.segments должна быть = 100.\n");
|
||||
promptBuilder.append("- Значения \"demandIndex\" должны быть от 0 до 100.\n");
|
||||
promptBuilder.append("- Значения \"potentialIndex\" должны быть от 0 до 100.\n");
|
||||
promptBuilder.append("- В \"funnel.stages\" значения \"value\" должны уменьшаться от этапа к этапу.\n");
|
||||
promptBuilder.append("- Не используй null, если можно задать разумное значение.\n");
|
||||
promptBuilder.append("- Все строки — на русском языке.\n");
|
||||
// 3. Получаем анализ сезонности и рынка
|
||||
logger.info("Starting seasonality and market analysis generation");
|
||||
Map<String, Object> seasonalityAndMarket = getSeasonalityAndMarket(request);
|
||||
if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) {
|
||||
result.putAll(seasonalityAndMarket);
|
||||
logger.info("Seasonality and market analysis generated successfully");
|
||||
} else {
|
||||
logger.warn("Seasonality and market analysis returned empty result");
|
||||
}
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
String context = buildContextString(request);
|
||||
// Проверяем, что хотя бы одна секция была успешно сгенерирована
|
||||
if (result.isEmpty()) {
|
||||
logger.error("All analysis sections failed to generate");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("JSON analysis generation completed. Generated sections: {}", result.keySet());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка JSON-ответа от markdown-тегов
|
||||
*/
|
||||
private String cleanJsonFromMarkdown(String jsonResponse) {
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
return jsonResponse;
|
||||
}
|
||||
String cleanedJson = jsonResponse.trim();
|
||||
if (cleanedJson.startsWith("```json")) {
|
||||
cleanedJson = cleanedJson.substring(7);
|
||||
}
|
||||
if (cleanedJson.startsWith("```")) {
|
||||
cleanedJson = cleanedJson.substring(3);
|
||||
}
|
||||
if (cleanedJson.endsWith("```")) {
|
||||
cleanedJson = cleanedJson.substring(0, cleanedJson.length() - 3);
|
||||
}
|
||||
return cleanedJson.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация анализа целевой аудитории
|
||||
*/
|
||||
private Map<String, Object> getAudienceAnalysis(MarketingAnalysisRequest request) {
|
||||
try {
|
||||
String context = buildContextString(request);
|
||||
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append(
|
||||
"Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
|
||||
promptBuilder.append(
|
||||
"Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ целевой аудитории и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
|
||||
promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
|
||||
promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
|
||||
promptBuilder.append("---\n\n");
|
||||
promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
|
||||
promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
|
||||
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
|
||||
.append(request.getBusinessNiche()).append(")\n");
|
||||
|
||||
Map<String, Object> targetAudienceData = request.getTargetAudience();
|
||||
StringBuilder targetAudienceBuilder = new StringBuilder();
|
||||
if (targetAudienceData != null) {
|
||||
if (targetAudienceData.containsKey("genders")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> genders = (List<String>) targetAudienceData.get("genders");
|
||||
if (genders != null && !genders.isEmpty()) {
|
||||
targetAudienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
|
||||
}
|
||||
}
|
||||
if (targetAudienceData.containsKey("ageRanges")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
|
||||
if (ageRanges != null && !ageRanges.isEmpty()) {
|
||||
targetAudienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
|
||||
}
|
||||
}
|
||||
if (targetAudienceData.containsKey("types")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> types = (List<String>) targetAudienceData.get("types");
|
||||
if (types != null && !types.isEmpty()) {
|
||||
targetAudienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
|
||||
}
|
||||
}
|
||||
}
|
||||
String targetAudienceStr = targetAudienceBuilder.toString().trim();
|
||||
if (targetAudienceStr.isEmpty()) {
|
||||
targetAudienceStr = "Не указана";
|
||||
}
|
||||
promptBuilder.append("- Целевая аудитория (если указана): ").append(targetAudienceStr).append("\n");
|
||||
|
||||
List<String> regions = request.getRegion();
|
||||
String regionStr = (regions != null && !regions.isEmpty())
|
||||
? String.join(", ", regions)
|
||||
: "Не указан";
|
||||
promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
|
||||
promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");
|
||||
|
||||
promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"audienceAnalysis\": {\n");
|
||||
promptBuilder.append(
|
||||
"\"ageGroups\": { \"18-24\": число, \"25-34\": число, \"35-44\": число, \"45+\": число }, // Pie chart data, сумма = 100\n");
|
||||
promptBuilder.append("\"ageComment\": \"Текст вывода под графиком возраста (1-2 предложения)\",\n");
|
||||
promptBuilder.append(
|
||||
"\"genderDistribution\": { \"Женщины\": число, \"Мужчины\": число }, // Donut chart, сумма = 100\n");
|
||||
promptBuilder.append("\"genderComment\": \"Текст вывода под графиком пола\",\n");
|
||||
promptBuilder.append("\"segments\": [ // Список из 4 детальных карточек сегментов\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"name\": \"Название (например 'Молодые профессионалы')\",\n");
|
||||
promptBuilder.append("\"sharePercent\": число, // сумма всех sharePercent = 100\n");
|
||||
promptBuilder.append("\"ageRange\": \"25-34\",\n");
|
||||
promptBuilder.append("\"incomeLevel\": \"Средний+\",\n");
|
||||
promptBuilder.append("\"motivation\": \"Ключевая мотивация покупки\",\n");
|
||||
promptBuilder.append("\"triggers\": \"Триггеры (на что реагируют)\",\n");
|
||||
promptBuilder
|
||||
.append("\"primaryChannels\": [\"Instagram\", \"TikTok\"], // Топ каналы для этого сегмента\n");
|
||||
promptBuilder.append("\"secondaryChannels\": [\"Telegram\"]\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("],\n");
|
||||
promptBuilder.append("\"segmentsChannelMatrix\": [ // Для таблицы heatmap: Сегмент vs Канал\n");
|
||||
promptBuilder.append(
|
||||
"{ \"segmentName\": \"Молодые профи\", \"instagram\": \"high\", \"telegram\": \"medium\", \"youtube\": \"low\" }\n");
|
||||
promptBuilder.append("],\n");
|
||||
promptBuilder.append("\"keyTakeaways\": [\"Вывод 1\", \"Вывод 2\", \"Вывод 3\"]\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("}\n\n");
|
||||
promptBuilder.append("ТРЕБОВАНИЯ:\n\n");
|
||||
promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n");
|
||||
promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n");
|
||||
promptBuilder.append("- Сумма значений в ageGroups должна быть = 100.\n");
|
||||
promptBuilder.append("- Сумма значений в genderDistribution должна быть = 100.\n");
|
||||
promptBuilder.append("- Сумма sharePercent по всем segments должна быть = 100.\n");
|
||||
promptBuilder.append("- Все строки — на русском языке.\n");
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
logger.warn("Empty JSON response from AI");
|
||||
return null;
|
||||
logger.warn("Empty JSON response from AI for audience analysis");
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
// Очистка ответа от возможных markdown блоков
|
||||
String cleanedJson = jsonResponse.trim();
|
||||
if (cleanedJson.startsWith("```json")) {
|
||||
cleanedJson = cleanedJson.substring(7);
|
||||
}
|
||||
if (cleanedJson.startsWith("```")) {
|
||||
cleanedJson = cleanedJson.substring(3);
|
||||
}
|
||||
if (cleanedJson.endsWith("```")) {
|
||||
cleanedJson = cleanedJson.substring(0, cleanedJson.length() - 3);
|
||||
}
|
||||
cleanedJson = cleanedJson.trim();
|
||||
|
||||
// Парсинг JSON
|
||||
String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
|
||||
Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// Валидация JSON
|
||||
if (validateJsonAnalysis(jsonMap)) {
|
||||
return jsonMap;
|
||||
} else {
|
||||
logger.warn("JSON validation failed, but returning parsed JSON anyway");
|
||||
return jsonMap;
|
||||
}
|
||||
return jsonMap;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error generating JSON analysis: {}", e.getMessage(), e);
|
||||
return null;
|
||||
logger.error("Error generating audience analysis: {}", e.getMessage(), e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация анализа конкурентов
|
||||
*/
|
||||
private Map<String, Object> getCompetitorAnalysis(MarketingAnalysisRequest request) {
|
||||
try {
|
||||
String context = buildContextString(request);
|
||||
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append(
|
||||
"Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
|
||||
promptBuilder.append(
|
||||
"Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ конкурентов и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
|
||||
promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
|
||||
promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
|
||||
promptBuilder.append("---\n\n");
|
||||
promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
|
||||
promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
|
||||
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
|
||||
.append(request.getBusinessNiche()).append(")\n");
|
||||
promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche())
|
||||
.append(" (уточни основных конкурентов)\n");
|
||||
|
||||
List<String> regions = request.getRegion();
|
||||
String regionStr = (regions != null && !regions.isEmpty())
|
||||
? String.join(", ", regions)
|
||||
: "Не указан";
|
||||
promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
|
||||
promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");
|
||||
|
||||
promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"competitorAnalysis\": {\n");
|
||||
promptBuilder.append("\"marketShareChart\": [ // Оценочные доли рынка для графика\n");
|
||||
promptBuilder.append("{ \"name\": \"Наш бренд\", \"value\": число },\n");
|
||||
promptBuilder.append("{ \"name\": \"Конкурент А\", \"value\": число },\n");
|
||||
promptBuilder.append("{ \"name\": \"Конкурент B\", \"value\": число },\n");
|
||||
promptBuilder.append("{ \"name\": \"Другие\", \"value\": число }\n");
|
||||
promptBuilder.append("], // сумма всех value должна быть = 100\n");
|
||||
promptBuilder.append("\"marketShareComment\": \"Вывод по доле рынка\",\n");
|
||||
promptBuilder.append("\"competitorsDetails\": [ // Детали по 3-4 конкурентам\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"name\": \"Конкурент А\",\n");
|
||||
promptBuilder.append("\"priceStrategy\": \"Высокая/Средняя/Низкая\",\n");
|
||||
promptBuilder.append(
|
||||
"\"digitalMetrics\": { \"reach\": число, \"followers\": число, \"activityIndex\": число }, // activityIndex от 0 до 100\n");
|
||||
promptBuilder.append("\"channelsHeatmap\": { // Оценка (0-100) качества ведения канала\n");
|
||||
promptBuilder.append("\"Instagram\": число, \"TikTok\": число, \"Telegram\": число, \"YouTube\": число\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("],\n");
|
||||
promptBuilder.append("\"comparisonTable\": [ // Сравнительная таблица (5-6 характеристик)\n");
|
||||
promptBuilder.append(
|
||||
"{ \"feature\": \"Ценовая политика\", \"us\": \"Средняя\", \"compA\": \"Высокая\", \"compB\": \"Низкая\" },\n");
|
||||
promptBuilder.append(
|
||||
"{ \"feature\": \"УТП\", \"us\": \"Сервис\", \"compA\": \"Бренд\", \"compB\": \"Дешевизна\" }\n");
|
||||
promptBuilder.append("],\n");
|
||||
promptBuilder.append("\"swotCompetitors\": { // Краткий SWOT по рынку в целом\n");
|
||||
promptBuilder.append("\"strengths\": [\"...\"], \"weaknesses\": [\"...\"]\n");
|
||||
promptBuilder.append("},\n");
|
||||
promptBuilder.append("\"keyInsights\": [\"Вывод 1\", \"Вывод 2\"]\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("}\n\n");
|
||||
promptBuilder.append("ТРЕБОВАНИЯ:\n\n");
|
||||
promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n");
|
||||
promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n");
|
||||
promptBuilder.append("- Сумма всех value в marketShareChart должна быть = 100.\n");
|
||||
promptBuilder.append("- Все строки — на русском языке.\n");
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
logger.warn("Empty JSON response from AI for competitor analysis");
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
|
||||
Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
return jsonMap;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error generating competitor analysis: {}", e.getMessage(), e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация анализа сезонности и рынка
|
||||
*/
|
||||
private Map<String, Object> getSeasonalityAndMarket(MarketingAnalysisRequest request) {
|
||||
try {
|
||||
String context = buildContextString(request);
|
||||
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append(
|
||||
"Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
|
||||
promptBuilder.append(
|
||||
"Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ сезонности и рынка и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
|
||||
promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
|
||||
promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
|
||||
promptBuilder.append("---\n\n");
|
||||
promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
|
||||
promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
|
||||
promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
|
||||
.append(request.getBusinessNiche()).append(")\n");
|
||||
|
||||
List<String> regions = request.getRegion();
|
||||
String regionStr = (regions != null && !regions.isEmpty())
|
||||
? String.join(", ", regions)
|
||||
: "Не указан";
|
||||
promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
|
||||
promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");
|
||||
|
||||
promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
|
||||
promptBuilder.append("{\n");
|
||||
promptBuilder.append("\"seasonality\": {\n");
|
||||
promptBuilder.append("\"description\": \"Описание сезонных колебаний спроса\",\n");
|
||||
promptBuilder.append(
|
||||
"\"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n");
|
||||
promptBuilder.append("\"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("},\n");
|
||||
promptBuilder.append("\"market\": {\n");
|
||||
promptBuilder.append("\"size\": \"Размер рынка (например, 'средний', 'крупный')\",\n");
|
||||
promptBuilder.append("\"growthRate\": \"Темп роста рынка\",\n");
|
||||
promptBuilder.append("\"trends\": [\"Тренд 1\", \"Тренд 2\", \"Тренд 3\"],\n");
|
||||
promptBuilder.append("\"opportunities\": [\"Возможность 1\", \"Возможность 2\"],\n");
|
||||
promptBuilder.append("\"threats\": [\"Угроза 1\", \"Угроза 2\"],\n");
|
||||
promptBuilder.append("\"estimated\": boolean\n");
|
||||
promptBuilder.append("}\n");
|
||||
promptBuilder.append("}\n\n");
|
||||
promptBuilder.append("ТРЕБОВАНИЯ:\n\n");
|
||||
promptBuilder.append("- Верни ТОЛЬКО JSON, без пояснений.\n");
|
||||
promptBuilder.append("- Следи, чтобы JSON был синтаксически корректным.\n");
|
||||
promptBuilder.append("- Значения \"demandIndex\" должны быть от 0 до 100.\n");
|
||||
promptBuilder.append("- Все строки — на русском языке.\n");
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
String jsonResponse = openAIAnalyticsService.generateWithInstruction(context, prompt, "ru");
|
||||
|
||||
if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
|
||||
logger.warn("Empty JSON response from AI for seasonality and market analysis");
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
|
||||
Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
return jsonMap;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error generating seasonality and market analysis: {}", e.getMessage(), e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user