wip - working on timeout issue

This commit is contained in:
Dario Ghunney Ware 2025-11-06 18:17:06 +00:00
parent 98d4949930
commit 3695a9a70a
13 changed files with 141 additions and 51 deletions

View File

@ -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 <API-KEY-HERE>
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}

View File

@ -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

View File

@ -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
*
* <p>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
*
* <p>For OpenAI: - spring.ai.openai.enabled=true - spring.ai.openai.api-key=your-api-key
*
* <p>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();
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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;

View File

@ -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 {

View File

@ -1,2 +0,0 @@
spring.ai.openai.enabled=true
spring.ai.openai.api-key=your-proprietary-api-key-here

View File

@ -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),

View File

@ -40,12 +40,12 @@ export interface ChatbotMessageResponse {
}
export async function createChatbotSession(payload: ChatbotSessionPayload) {
const { data } = await apiClient.post<ChatbotSessionInfo>('/api/internal/chatbot/session', payload);
const { data } = await apiClient.post<ChatbotSessionInfo>('/api/v1/internal/chatbot/session', payload);
return data;
}
export async function sendChatbotPrompt(payload: ChatbotQueryPayload) {
const { data } = await apiClient.post<ChatbotMessageResponse>('/api/internal/chatbot/query', payload);
const { data } = await apiClient.post<ChatbotMessageResponse>('/api/v1/internal/chatbot/query', payload);
return data;
}