.
This commit is contained in:
@@ -15,7 +15,6 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@@ -1671,8 +1670,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("JSON ДАННЫЕ:\n");
|
||||
promptBuilder.append(jsonString);
|
||||
|
||||
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName,
|
||||
2);
|
||||
// Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini)
|
||||
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, null, 2);
|
||||
|
||||
if (!isValidAiResponse(result)) {
|
||||
logger.warn("genSectionMarket: AI returned invalid or refusal response");
|
||||
@@ -1695,6 +1694,11 @@ public class MarketingAnalysisService {
|
||||
String jsonString = objectMapper.writeValueAsString(contextJson);
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
||||
promptBuilder.append(
|
||||
"КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. "
|
||||
+ "Ты обязан найти объект `audienceAnalysis` в переданном тебе тексте. "
|
||||
+ "Обязательно вставь теги [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]]. "
|
||||
+ "ЗАПРЕЩЕНО писать фразу \"К сожалению, данных нет\" (и любые вариации).\n\n");
|
||||
appendMandatoryJsonUsageWarning(promptBuilder);
|
||||
appendPremiumConsultingStyleGuide(promptBuilder);
|
||||
promptBuilder.append(
|
||||
@@ -1761,8 +1765,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("JSON ДАННЫЕ:\n");
|
||||
promptBuilder.append(jsonString);
|
||||
|
||||
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName,
|
||||
2);
|
||||
// Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini)
|
||||
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, null, 2);
|
||||
|
||||
if (!isValidAiResponse(result)) {
|
||||
logger.warn("genSectionAudience: AI returned invalid or refusal response");
|
||||
@@ -1785,6 +1789,11 @@ public class MarketingAnalysisService {
|
||||
String jsonString = objectMapper.writeValueAsString(contextJson);
|
||||
StringBuilder promptBuilder = new StringBuilder();
|
||||
promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
|
||||
promptBuilder.append(
|
||||
"КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. "
|
||||
+ "Ты обязан найти объект `competitorAnalysis` в переданном тебе тексте. "
|
||||
+ "Обязательно вставь теги [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]]. "
|
||||
+ "ЗАПРЕЩЕНО писать фразу \"К сожалению, данных нет\" (и любые вариации).\n\n");
|
||||
appendMandatoryJsonUsageWarning(promptBuilder);
|
||||
appendPremiumConsultingStyleGuide(promptBuilder);
|
||||
promptBuilder.append(
|
||||
@@ -1849,8 +1858,8 @@ public class MarketingAnalysisService {
|
||||
promptBuilder.append("JSON ДАННЫЕ:\n");
|
||||
promptBuilder.append(jsonString);
|
||||
|
||||
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, miniModelName,
|
||||
2);
|
||||
// Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini)
|
||||
String result = generateWithRetry(jsonString, promptBuilder.toString(), "ru", textModelName, null, 2);
|
||||
|
||||
if (!isValidAiResponse(result)) {
|
||||
logger.warn("genSectionCompetitors: AI returned invalid or refusal response");
|
||||
@@ -3523,6 +3532,10 @@ public class MarketingAnalysisService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!chartsData.containsKey(mapping.dataKey)) {
|
||||
logger.warn("replaceChartPlaceholders: chartsData missing key '{}' for placeholder {}",
|
||||
mapping.dataKey, placeholderVariant);
|
||||
}
|
||||
Object chartData = chartsData.get(mapping.dataKey);
|
||||
if (!isEmptyChartData(chartData)) {
|
||||
try {
|
||||
@@ -3575,6 +3588,10 @@ public class MarketingAnalysisService {
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
if (mapping != null && !chartsData.containsKey(mapping.dataKey)) {
|
||||
logger.warn("replaceChartPlaceholders: chartsData missing key '{}' for remaining placeholder {}",
|
||||
mapping.dataKey, placeholderVariant);
|
||||
}
|
||||
logger.warn("replaceChartPlaceholders: Removing leftover placeholder: {}", placeholderVariant);
|
||||
result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant);
|
||||
}
|
||||
@@ -3615,6 +3632,10 @@ public class MarketingAnalysisService {
|
||||
// делаем мягче
|
||||
sectionToPlaceholders.put("VI\\.\\s*SWOT[-\\s]?анализ", List.of("[[CHART_SWOT]]"));
|
||||
|
||||
// Умный fallback: если модель НЕ ВСТАВИЛА плейсхолдер в текст секции,
|
||||
// а данные для него есть — добавляем сам плейсхолдер в КОНЕЦ секции.
|
||||
// Далее replaceChartPlaceholders() (final pass) заменит плейсхолдер на
|
||||
// ```json:<key> блок.
|
||||
for (Map.Entry<String, List<String>> sectionEntry : sectionToPlaceholders.entrySet()) {
|
||||
String sectionPattern = sectionEntry.getKey();
|
||||
List<String> placeholders = sectionEntry.getValue();
|
||||
@@ -3622,87 +3643,100 @@ public class MarketingAnalysisService {
|
||||
continue;
|
||||
}
|
||||
|
||||
StringBuilder blocksToInsert = new StringBuilder();
|
||||
boolean hasAnyBlock = false;
|
||||
// 1) Найти старт секции по заголовку (с ## или без)
|
||||
java.util.regex.Pattern sectionStartPattern = java.util.regex.Pattern.compile(
|
||||
"(?im)^(?:##+\\s*)?" + sectionPattern + ".*$");
|
||||
java.util.regex.Matcher sectionStartMatcher = sectionStartPattern.matcher(result);
|
||||
if (!sectionStartMatcher.find()) {
|
||||
logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern);
|
||||
continue;
|
||||
}
|
||||
|
||||
int sectionStart = sectionStartMatcher.start();
|
||||
int scanFrom = sectionStartMatcher.end();
|
||||
|
||||
// 2) Найти конец секции (следующий заголовок вида IV./V./VI... или Markdown
|
||||
// heading)
|
||||
java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern.compile(
|
||||
"(?im)^(?:##+\\s*)?[IVX]+\\.\\s+.*$");
|
||||
java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(result);
|
||||
int sectionEnd = result.length();
|
||||
if (nextSectionMatcher.find(scanFrom)) {
|
||||
sectionEnd = nextSectionMatcher.start();
|
||||
}
|
||||
|
||||
String sectionText = result.substring(sectionStart, sectionEnd);
|
||||
StringBuilder tagsToAppend = new StringBuilder();
|
||||
boolean willAppendAnything = false;
|
||||
|
||||
for (String placeholder : placeholders) {
|
||||
String normalizedPlaceholder = normalizePlaceholderToken(placeholder);
|
||||
if (placeholder == null || placeholder.isBlank()
|
||||
|| processedPlaceholders.contains(normalizedPlaceholder)) {
|
||||
if (placeholder == null || placeholder.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
ChartMapping mapping = placeholderMap.get(placeholder);
|
||||
if (mapping == null) {
|
||||
continue;
|
||||
}
|
||||
Object chartData = chartsData.get(mapping.dataKey);
|
||||
if (isEmptyChartData(chartData)) {
|
||||
|
||||
// Если плейсхолдер уже есть в тексте секции (даже с пробелами/регистром) — не
|
||||
// добавляем
|
||||
if (sectionContainsPlaceholder(sectionText, placeholder)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData);
|
||||
String jsonBlock = "\n\n```json:" + mapping.chartKey + "\n" + jsonStr + "\n```\n";
|
||||
blocksToInsert.append(jsonBlock);
|
||||
hasAnyBlock = true;
|
||||
processedPlaceholders.add(normalizedPlaceholder);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("Failed to serialize chart data for fallback {}: {}", mapping.dataKey, e.getMessage());
|
||||
|
||||
// Если данные отсутствуют — не добавляем тег (он будет удален final pass-ом)
|
||||
if (chartsData == null || !chartsData.containsKey(mapping.dataKey)) {
|
||||
logger.warn(
|
||||
"addMissingChartsToSections: chartsData missing key '{}' for placeholder {} in section {}",
|
||||
mapping.dataKey, placeholder, sectionPattern);
|
||||
continue;
|
||||
}
|
||||
|
||||
Object chartData = chartsData.get(mapping.dataKey);
|
||||
if (isEmptyChartData(chartData)) {
|
||||
logger.warn(
|
||||
"addMissingChartsToSections: chartData empty for key '{}' (placeholder {}) in section {}",
|
||||
mapping.dataKey, placeholder, sectionPattern);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Добавляем тег в конец секции. Это решает кейс: «модель забыла вставить
|
||||
// [[TABLE_COMPARISON]]»
|
||||
tagsToAppend.append("\n\n").append(placeholder).append("\n");
|
||||
willAppendAnything = true;
|
||||
|
||||
// Не считаем как processed (пусть final pass заменит его на JSON-блок)
|
||||
// но помечаем, чтобы ниже не пытаться добавлять повторно в рамках этой итерации
|
||||
processedPlaceholders.add(normalizePlaceholderToken(placeholder));
|
||||
}
|
||||
|
||||
if (!hasAnyBlock) {
|
||||
if (!willAppendAnything) {
|
||||
continue;
|
||||
}
|
||||
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern
|
||||
.compile("(##+\\s*" + sectionPattern + "[^#]*)", java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
java.util.regex.Matcher matcher = pattern.matcher(result);
|
||||
boolean found = matcher.find();
|
||||
|
||||
if (!found) {
|
||||
pattern = java.util.regex.Pattern
|
||||
.compile("(^|\\n)\\s*" + sectionPattern + "[^\\n]*(\\n|$)",
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE);
|
||||
matcher = pattern.matcher(result);
|
||||
found = matcher.find();
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
logger.debug("addMissingChartsToSections: Section not found for pattern: {}", sectionPattern);
|
||||
continue;
|
||||
}
|
||||
|
||||
int insertPos = matcher.end();
|
||||
String remainingText = result.substring(insertPos);
|
||||
|
||||
java.util.regex.Pattern nextSectionPattern = java.util.regex.Pattern.compile(
|
||||
"^(##+\\s*[IVX]+\\.|\\s*[IVX]+\\.\\s+[A-ZА-Я])",
|
||||
java.util.regex.Pattern.MULTILINE);
|
||||
java.util.regex.Matcher nextSectionMatcher = nextSectionPattern.matcher(remainingText);
|
||||
|
||||
String blocks = blocksToInsert.toString();
|
||||
|
||||
if (nextSectionMatcher.find()) {
|
||||
int nextSectionPos = insertPos + nextSectionMatcher.start();
|
||||
result = result.substring(0, nextSectionPos) + blocks + result.substring(nextSectionPos);
|
||||
} else {
|
||||
java.util.regex.Pattern romanNumeralPattern = java.util.regex.Pattern
|
||||
.compile("\\n\\s*[IVX]+\\.\\s+[A-ZА-Я]", java.util.regex.Pattern.MULTILINE);
|
||||
java.util.regex.Matcher romanMatcher = romanNumeralPattern.matcher(remainingText);
|
||||
if (romanMatcher.find()) {
|
||||
int nextSectionPos = insertPos + romanMatcher.start();
|
||||
result = result.substring(0, nextSectionPos) + blocks + result.substring(nextSectionPos);
|
||||
} else {
|
||||
result = result + blocks;
|
||||
}
|
||||
}
|
||||
logger.debug("addMissingChartsToSections: Added fallback blocks to section matching pattern: {}",
|
||||
result = result.substring(0, sectionEnd) + tagsToAppend + result.substring(sectionEnd);
|
||||
logger.debug("addMissingChartsToSections: Added missing placeholder tags to section pattern: {}",
|
||||
sectionPattern);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean sectionContainsPlaceholder(String sectionText, String canonicalPlaceholder) {
|
||||
if (sectionText == null || sectionText.isEmpty() || canonicalPlaceholder == null
|
||||
|| canonicalPlaceholder.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
// canonicalPlaceholder like [[CHART_MARKET_SHARE]]
|
||||
String inner = canonicalPlaceholder.replace("[[", "").replace("]]", "").trim();
|
||||
if (inner.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String regex = "\\[\\[\\s*" + java.util.regex.Pattern.quote(inner) + "\\s*\\]\\]";
|
||||
return java.util.regex.Pattern.compile(regex, java.util.regex.Pattern.CASE_INSENSITIVE).matcher(sectionText)
|
||||
.find();
|
||||
}
|
||||
|
||||
public MarketingAnalysisResult getAnalysisResult(String analysisId) {
|
||||
Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
|
||||
if (optAnalysis.isEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user