This commit is contained in:
root
2025-12-20 19:43:33 +05:00
parent f517907073
commit ecc6089040
@@ -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()) {