diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index edc2a8d..3ed3dea 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -27,11 +27,15 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @Service public class MarketingAnalysisService { + private final Semaphore openAiSemaphore = new Semaphore(3); // Разрешить только 3 активных запроса к OpenAI на всё + // приложение + private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. " + "Никогда не пиши, что данных нет, если JSON не пустой.\n\n"; private static final String AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION = "" @@ -1556,42 +1560,52 @@ public class MarketingAnalysisService { String fallbackModel, int attempts, String sectionId) { - int maxAttempts = Math.max(1, attempts); - String last = null; - DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson); - String systemPrompt = buildSystemPromptForSection(detailLevel, sectionId); - Integer maxTokensOverride = detailLevel == DetailLevel.ПОДРОБНО ? REPORT_BASE_MAX_TOKENS * 2 : null; + try { - for (int i = 1; i <= maxAttempts; i++) { - String modelToUse = primaryModel; - // На последней попытке можно переключиться на запасную модель (если задана) - if (i == maxAttempts && fallbackModel != null && !fallbackModel.isBlank()) { - modelToUse = fallbackModel; - } - try { - last = openAIAnalyticsService.generateWithInstructionWithModel( - jsonContext, prompt, lang, modelToUse, systemPrompt, maxTokensOverride); - } catch (Exception e) { - logger.warn("generateWithRetry: attempt {} failed with exception: {}", i, e.getMessage()); - last = null; - } + openAiSemaphore.acquire(); + int maxAttempts = Math.max(1, attempts); + String last = null; + DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson); + String systemPrompt = buildSystemPromptForSection(detailLevel, sectionId); + Integer maxTokensOverride = detailLevel == DetailLevel.ПОДРОБНО ? REPORT_BASE_MAX_TOKENS * 2 : null; - if (isValidAiResponse(last)) { - return last; - } - - // Небольшая пауза между попытками, чтобы сгладить кратковременные сбои - if (i < maxAttempts) { + for (int i = 1; i <= maxAttempts; i++) { + String modelToUse = primaryModel; + // На последней попытке можно переключиться на запасную модель (если задана) + if (i == maxAttempts && fallbackModel != null && !fallbackModel.isBlank()) { + modelToUse = fallbackModel; + } try { - Thread.sleep(250L); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - break; + last = openAIAnalyticsService.generateWithInstructionWithModel( + jsonContext, prompt, lang, modelToUse, systemPrompt, maxTokensOverride); + } catch (Exception e) { + logger.warn("generateWithRetry: attempt {} failed with exception: {}", i, e.getMessage()); + last = null; + } + + if (isValidAiResponse(last)) { + return last; + } + + // Небольшая пауза между попытками, чтобы сгладить кратковременные сбои + if (i < maxAttempts) { + try { + Thread.sleep(250L); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } } } - } - return last; + return last; + } catch (Exception e) { + logger.error("generateWithRetryForSection: Error generating section: {}", e.getMessage(), e); + logger.error("generateWithRetryForSection: Stack trace: ", e); + return null; + } finally { + openAiSemaphore.release(); + } } // Backward-compatible helper (legacy signature). Kept because file historically