diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index d688a1300..dfbc1b569 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -4,9 +4,8 @@ logging.level.org.springframework.security=WARN logging.level.org.hibernate=WARN logging.level.org.eclipse.jetty=WARN #logging.level.org.springframework.security.oauth2=DEBUG -#logging.level.org.springframework.security=DEBUG #logging.level.org.opensaml=DEBUG -#logging.level.stirling.software.proprietary.security=DEBUG +logging.level.stirling.software.proprietary.security=DEBUG logging.level.com.zaxxer.hikari=WARN logging.level.stirling.software.SPDF.service.PdfJsonConversionService=INFO logging.level.stirling.software.common.service.JobExecutorService=INFO @@ -52,14 +51,43 @@ server.servlet.session.timeout:30m springdoc.api-docs.path=/v1/api-docs # Set the URL of the OpenAPI JSON for the Swagger UI springdoc.swagger-ui.url=/v1/api-docs -springdoc.swagger-ui.path=/swagger-ui.html + +# Spring AI OpenAI Configuration +# Uses GPT-5-nano as primary model and GPT-5-mini as fallback (configured in settings.yml) +spring.ai.openai.enabled=true +spring.ai.openai.api-key=# todo +spring.ai.openai.base-url=https://api.openai.com +spring.ai.openai.chat.enabled=true +spring.ai.openai.chat.options.model=gpt-5-nano +# Note: Some models only support default temperature value of 1.0 +spring.ai.openai.chat.options.temperature=1.0 +# For newer models, use max-completion-tokens instead of max-tokens +spring.ai.openai.chat.options.max-completion-tokens=4000 +spring.ai.openai.embedding.enabled=true +spring.ai.openai.embedding.options.model=text-embedding-ada-002 +# Increase timeout for OpenAI API calls (default is 10 seconds) +spring.ai.openai.chat.options.connection-timeout=60s +spring.ai.openai.chat.options.read-timeout=60s +spring.ai.openai.embedding.options.connection-timeout=60s +spring.ai.openai.embedding.options.read-timeout=60s + +# Spring AI Ollama Configuration (disabled to avoid bean conflicts) +spring.ai.ollama.enabled=false +spring.ai.ollama.base-url=http://localhost:11434 +spring.ai.ollama.chat.enabled=false +spring.ai.ollama.chat.options.model=llama3 +spring.ai.ollama.chat.options.temperature=1.0 +spring.ai.ollama.embedding.enabled=false +spring.ai.ollama.embedding.options.model=nomic-embed-text + # Force OpenAPI 3.0 specification version +springdoc.swagger-ui.path=/swagger-ui.html springdoc.api-docs.version=OPENAPI_3_0 + posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq posthog.host=https://eu.i.posthog.com spring.main.allow-bean-definition-overriding=true -spring.ai.openai.enabled=false # Set up a consistent temporary directory location java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf} diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index 2646c42d1..8c8721218 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -52,8 +52,9 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-cache' api 'com.github.ben-manes.caffeine:caffeine' api 'io.swagger.core.v3:swagger-core-jakarta:2.2.38' - implementation 'org.springframework.ai:spring-ai-openai' - implementation 'org.springframework.ai:spring-ai-ollama' + implementation 'org.springframework.ai:spring-ai-starter-model-openai' + implementation 'org.springframework.ai:spring-ai-starter-model-ollama' + implementation 'org.springframework.ai:spring-ai-redis-store' implementation 'com.bucket4j:bucket4j_jdk17-core:8.15.0' // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/config/SpringAIConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/config/SpringAIConfig.java new file mode 100644 index 000000000..2724207fd --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/config/SpringAIConfig.java @@ -0,0 +1,82 @@ +package stirling.software.proprietary.config; + +import java.time.Duration; + +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; + +import lombok.extern.slf4j.Slf4j; + +/** + * Spring AI Configuration for Stirling PDF Chatbot + * + *

This configuration enables Spring AI auto-configuration for chatbot features. The actual + * ChatModel and EmbeddingModel beans are provided by Spring Boot's auto-configuration based on the + * spring.ai.* properties in application-proprietary.properties + * + *

For OpenAI: - spring.ai.openai.enabled=true - spring.ai.openai.api-key=your-api-key + * + *

For Ollama (as fallback): - spring.ai.ollama.enabled=true - + * spring.ai.ollama.base-url=http://localhost:11434 + */ +@Configuration +@Slf4j +public class SpringAIConfig { + + public SpringAIConfig() { + log.info("Spring AI Configuration enabled for Stirling PDF Chatbot"); + log.info( + "ChatModel and EmbeddingModel beans will be auto-configured based on spring.ai.* properties"); + } + + /** Primary ChatModel bean that delegates to OpenAI's auto-configured bean */ + @Bean + @Primary + public ChatModel primaryChatModel(@Qualifier("openAiChatModel") ChatModel openAiChatModel) { + log.info("Using OpenAI ChatModel as primary"); + return openAiChatModel; + } + + /** Primary EmbeddingModel bean that delegates to OpenAI's auto-configured bean */ + @Bean + @Primary + public EmbeddingModel primaryEmbeddingModel( + @Qualifier("openAiEmbeddingModel") EmbeddingModel openAiEmbeddingModel) { + log.info("Using OpenAI EmbeddingModel as primary"); + return openAiEmbeddingModel; + } + + /** + * Custom RestTemplate for Spring AI OpenAI client with increased timeouts. This helps prevent + * timeout errors when processing large documents or complex queries. + */ + @Bean(name = "openAiRestTemplate") + public RestTemplate openAiRestTemplate(RestTemplateBuilder builder) { + log.info("Creating custom RestTemplate for OpenAI with 60s timeouts"); + return builder.connectTimeout(Duration.ofSeconds(60)) + .readTimeout(Duration.ofSeconds(60)) + .build(); + } + + /** + * Custom RestClient for Spring AI OpenAI with increased timeouts. Spring AI 1.0.3+ prefers + * RestClient over RestTemplate. + */ + @Bean(name = "openAiRestClient") + public RestClient openAiRestClient() { + log.info("Creating custom RestClient for OpenAI with 60s timeouts"); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(Duration.ofSeconds(60)); + factory.setReadTimeout(Duration.ofSeconds(60)); + + return RestClient.builder().requestFactory(factory).build(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotController.java index 6f8ffd48b..052941828 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotController.java @@ -3,8 +3,6 @@ package stirling.software.proprietary.controller; import java.util.ArrayList; import java.util.List; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -31,11 +29,11 @@ import stirling.software.proprietary.service.chatbot.ChatbotSessionRegistry; import stirling.software.proprietary.service.chatbot.exception.ChatbotException; @RestController -@RequestMapping("/api/internal/chatbot") +@RequestMapping("/api/v1/internal/chatbot") @RequiredArgsConstructor @Slf4j -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") -@ConditionalOnBean(ChatbotService.class) +// @ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") +// @ConditionalOnBean(ChatbotService.class) public class ChatbotController { private final ChatbotService chatbotService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotExceptionHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotExceptionHandler.java index 1d9b091bb..ae8403d0e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotExceptionHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/ChatbotExceptionHandler.java @@ -4,7 +4,6 @@ import java.time.Instant; import java.util.Map; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -18,7 +17,7 @@ import stirling.software.proprietary.service.chatbot.exception.NoTextDetectedExc @RestControllerAdvice(assignableTypes = ChatbotController.class) @Slf4j -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") +// @ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") @ConditionalOnBean(ChatbotService.class) public class ChatbotExceptionHandler { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotCacheService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotCacheService.java index a12659ef0..efc0d1ad2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotCacheService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotCacheService.java @@ -9,7 +9,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import com.github.benmanes.caffeine.cache.Cache; @@ -26,7 +25,7 @@ import stirling.software.proprietary.model.chatbot.ChatbotTextChunk; import stirling.software.proprietary.service.chatbot.exception.ChatbotException; @Service -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") +// @ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") @Slf4j public class ChatbotCacheService { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotConversationService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotConversationService.java index 368b7be6a..6730b3ec0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotConversationService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotConversationService.java @@ -1,5 +1,7 @@ package stirling.software.proprietary.service.chatbot; +import static stirling.software.proprietary.service.chatbot.ChatbotFeatureProperties.ChatbotSettings.ModelProvider.OLLAMA; + import java.io.IOException; import java.time.Instant; import java.util.ArrayList; @@ -15,11 +17,8 @@ import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -35,14 +34,11 @@ import stirling.software.proprietary.model.chatbot.ChatbotResponse; import stirling.software.proprietary.model.chatbot.ChatbotSession; import stirling.software.proprietary.model.chatbot.ChatbotTextChunk; import stirling.software.proprietary.service.chatbot.ChatbotFeatureProperties.ChatbotSettings; -import stirling.software.proprietary.service.chatbot.ChatbotFeatureProperties.ChatbotSettings.ModelProvider; import stirling.software.proprietary.service.chatbot.exception.ChatbotException; @Service @Slf4j @RequiredArgsConstructor -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") -@ConditionalOnBean(ChatModel.class) public class ChatbotConversationService { private final ChatModel chatModel; @@ -159,7 +155,7 @@ public class ChatbotConversationService { } private void ensureModelSwitchCapability(ChatbotSettings settings) { - ModelProvider provider = settings.models().provider(); + ChatbotSettings.ModelProvider provider = settings.models().provider(); switch (provider) { case OPENAI -> { if (!(chatModel instanceof OpenAiChatModel)) { @@ -262,22 +258,15 @@ public class ChatbotConversationService { + "Question: " + question; - Object options = buildChatOptions(settings, model); + OpenAiChatOptions options = buildChatOptions(model); return new Prompt( List.of(new SystemMessage(systemPrompt), new UserMessage(userPrompt)), options); } - private Object buildChatOptions(ChatbotSettings settings, String model) { - return switch (settings.models().provider()) { - case OPENAI -> - OpenAiChatOptions.builder() - .model(model) - .temperature(0.2) - .responseFormat("json_object") - .build(); - case OLLAMA -> OllamaOptions.builder().model(model).temperature(0.2).build(); - }; + private OpenAiChatOptions buildChatOptions(String model) { + // Note: Some models only support default temperature value of 1.0 + return OpenAiChatOptions.builder().model(model).temperature(1.0).build(); } private ModelReply parseModelResponse(String raw) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotIngestionService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotIngestionService.java index 1c0c2f8d7..8886368c1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotIngestionService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotIngestionService.java @@ -9,8 +9,6 @@ import java.util.UUID; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -25,8 +23,6 @@ import stirling.software.proprietary.service.chatbot.exception.ChatbotException; import stirling.software.proprietary.service.chatbot.exception.NoTextDetectedException; @Service -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") -@ConditionalOnBean(EmbeddingModel.class) @Slf4j @RequiredArgsConstructor public class ChatbotIngestionService { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotRetrievalService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotRetrievalService.java index c2030633b..c2a35620d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotRetrievalService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotRetrievalService.java @@ -7,8 +7,6 @@ import java.util.Optional; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -23,8 +21,6 @@ import stirling.software.proprietary.service.chatbot.exception.ChatbotException; @Service @RequiredArgsConstructor @Slf4j -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") -@ConditionalOnBean(EmbeddingModel.class) public class ChatbotRetrievalService { private final ChatbotCacheService cacheService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotService.java index 27c0762eb..492ca21c0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/chatbot/ChatbotService.java @@ -3,8 +3,6 @@ package stirling.software.proprietary.service.chatbot; import java.util.HashMap; import java.util.Map; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -18,8 +16,6 @@ import stirling.software.proprietary.service.AuditService; import stirling.software.proprietary.service.chatbot.exception.ChatbotException; @Service -@ConditionalOnProperty(value = "premium.proFeatures.chatbot.enabled", havingValue = "true") -@ConditionalOnBean({ChatbotIngestionService.class, ChatbotConversationService.class}) @Slf4j @RequiredArgsConstructor public class ChatbotService { diff --git a/app/proprietary/src/main/resources/application-proprietary.properties b/app/proprietary/src/main/resources/application-proprietary.properties index dc6b3d73e..e69de29bb 100644 --- a/app/proprietary/src/main/resources/application-proprietary.properties +++ b/app/proprietary/src/main/resources/application-proprietary.properties @@ -1,2 +0,0 @@ -spring.ai.openai.enabled=true -spring.ai.openai.api-key=your-proprietary-api-key-here diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/chatbot/ChatbotServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/chatbot/ChatbotServiceTest.java index 805933c85..16a9b9bfa 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/service/chatbot/ChatbotServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/chatbot/ChatbotServiceTest.java @@ -48,7 +48,11 @@ class ChatbotServiceTest { true, 4000, 0.5D, - new ChatbotSettings.ModelSettings("gpt-5-nano", "gpt-5-mini", "embed"), + new ChatbotSettings.ModelSettings( + ChatbotSettings.ModelProvider.OPENAI, + "gpt-5-nano", + "gpt-5-mini", + "embed"), new ChatbotSettings.RagSettings(512, 128, 4), new ChatbotSettings.CacheSettings(60, 10, 1000), new ChatbotSettings.OcrSettings(false), @@ -60,7 +64,11 @@ class ChatbotServiceTest { true, 4000, 0.5D, - new ChatbotSettings.ModelSettings("gpt-5-nano", "gpt-5-mini", "embed"), + new ChatbotSettings.ModelSettings( + ChatbotSettings.ModelProvider.OPENAI, + "gpt-5-nano", + "gpt-5-mini", + "embed"), new ChatbotSettings.RagSettings(512, 128, 4), new ChatbotSettings.CacheSettings(60, 10, 1000), new ChatbotSettings.OcrSettings(false), diff --git a/frontend/src/core/services/chatbotService.ts b/frontend/src/core/services/chatbotService.ts index 56fb1f03f..9915a2822 100644 --- a/frontend/src/core/services/chatbotService.ts +++ b/frontend/src/core/services/chatbotService.ts @@ -40,12 +40,12 @@ export interface ChatbotMessageResponse { } export async function createChatbotSession(payload: ChatbotSessionPayload) { - const { data } = await apiClient.post('/api/internal/chatbot/session', payload); + const { data } = await apiClient.post('/api/v1/internal/chatbot/session', payload); return data; } export async function sendChatbotPrompt(payload: ChatbotQueryPayload) { - const { data } = await apiClient.post('/api/internal/chatbot/query', payload); + const { data } = await apiClient.post('/api/v1/internal/chatbot/query', payload); return data; }