UI elements

This commit is contained in:
DarioGii
2025-11-07 17:48:14 +00:00
committed by Dario Ghunney Ware
parent f6ad398fb3
commit 1973c55d10
11 changed files with 438 additions and 136 deletions

View File

@@ -626,6 +626,8 @@ public class ApplicationProperties {
private String primary = "gpt-5-nano";
private String fallback = "gpt-5-mini";
private String embedding = "text-embedding-3-small";
private long connectTimeoutMillis = 10000;
private long readTimeoutMillis = 60000;
}
@Data

View File

@@ -52,9 +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-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-starter-model-ollama'
implementation 'org.springframework.ai:spring-ai-redis-store'
api 'org.springframework.ai:spring-ai-starter-model-openai'
api 'org.springframework.ai:spring-ai-starter-model-ollama'
api '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,60 @@
package stirling.software.proprietary.configuration;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.Optional;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.Premium;
import stirling.software.common.model.ApplicationProperties.Premium.ProFeatures;
import stirling.software.common.model.ApplicationProperties.Premium.ProFeatures.Chatbot;
@Configuration
@ConditionalOnClass(RestClientCustomizer.class)
@ConditionalOnProperty(value = "spring.ai.openai.enabled", havingValue = "true")
public class ChatbotAiClientConfiguration {
@Bean
public RestClientCustomizer chatbotRestClientCustomizer(
ApplicationProperties applicationProperties) {
long connectTimeout = resolveConnectTimeout(applicationProperties);
long readTimeout = resolveReadTimeout(applicationProperties);
return builder -> builder.requestFactory(createRequestFactory(connectTimeout, readTimeout));
}
private JdkClientHttpRequestFactory createRequestFactory(
long connectTimeoutMillis, long readTimeoutMillis) {
HttpClient httpClient =
HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(connectTimeoutMillis))
.build();
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(httpClient);
factory.setReadTimeout((int) readTimeoutMillis);
return factory;
}
private long resolveConnectTimeout(ApplicationProperties properties) {
long configured = resolveChatbot(properties).getModels().getConnectTimeoutMillis();
return configured > 0 ? configured : 30000L;
}
private long resolveReadTimeout(ApplicationProperties properties) {
long configured = resolveChatbot(properties).getModels().getReadTimeoutMillis();
return configured > 0 ? configured : 120000L;
}
private Chatbot resolveChatbot(ApplicationProperties properties) {
return Optional.ofNullable(properties)
.map(ApplicationProperties::getPremium)
.map(Premium::getProFeatures)
.map(ProFeatures::getChatbot)
.orElseGet(Chatbot::new);
}
}

View File

@@ -99,17 +99,16 @@ public class ChatbotController {
private List<String> sessionWarnings(ChatbotSettings settings, ChatbotSession session) {
List<String> warnings = new ArrayList<>();
if (settings.alphaWarning()) {
warnings.add("Chatbot feature is in alpha and may change.");
}
warnings.add("Image-based content is not supported yet.");
if (session != null && session.isImageContentDetected()) {
warnings.add("Detected images will be ignored until image support ships.");
warnings.add("Images detected - Images are not currently supported.");
}
warnings.add("Only extracted text is sent for analysis.");
if (session != null && session.isOcrRequested()) {
warnings.add("OCR was requested extra processing charges may apply.");
}
return warnings;
}
}

View File

@@ -3,6 +3,7 @@ package stirling.software.proprietary.controller;
import java.time.Instant;
import java.util.Map;
import org.eclipse.jetty.client.HttpResponseException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -37,6 +38,14 @@ public class ChatbotExceptionHandler {
return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
}
@ExceptionHandler(HttpResponseException.class)
public ResponseEntity<Map<String, Object>> handleProvider(HttpResponseException ex) {
log.warn("Chatbot provider error", ex);
return buildResponse(
HttpStatus.BAD_GATEWAY,
"Chatbot provider rejected the request: " + ex.getMessage());
}
private ResponseEntity<Map<String, Object>> buildResponse(HttpStatus status, String message) {
Map<String, Object> payload =
Map.of(

View File

@@ -204,7 +204,19 @@ public class ChatbotConversationService {
List<ChatbotTextChunk> context,
Map<String, String> metadata) {
Prompt requestPrompt = buildPrompt(settings, model, prompt, session, context, metadata);
ChatResponse response = chatModel.call(requestPrompt);
ChatResponse response;
try {
response = chatModel.call(requestPrompt);
} catch (org.eclipse.jetty.client.HttpResponseException ex) {
throw new ChatbotException(
"Chat model rejected the request: " + sanitizeRemoteMessage(ex.getMessage()),
ex);
} catch (RuntimeException ex) {
throw new ChatbotException(
"Failed to contact chat model provider: "
+ sanitizeRemoteMessage(ex.getMessage()),
ex);
}
String content =
Optional.ofNullable(response)
.map(ChatResponse::getResults)
@@ -298,4 +310,11 @@ public class ChatbotConversationService {
private record ModelReply(
String answer, double confidence, boolean requiresEscalation, String rationale) {}
private String sanitizeRemoteMessage(String message) {
if (!StringUtils.hasText(message)) {
return "unexpected provider error";
}
return message.replaceAll("(?i)api[-_ ]?key\\s*=[^\\s]+", "api-key=***");
}
}

View File

@@ -138,7 +138,20 @@ public class ChatbotIngestionService {
if (chunkTexts.isEmpty()) {
throw new ChatbotException("Unable to split document text into retrievable chunks");
}
EmbeddingResponse response = embeddingModel.embedForResponse(chunkTexts);
EmbeddingResponse response;
try {
response = embeddingModel.embedForResponse(chunkTexts);
} catch (org.eclipse.jetty.client.HttpResponseException ex) {
throw new ChatbotException(
"Embedding provider rejected the request: "
+ sanitizeRemoteMessage(ex.getMessage()),
ex);
} catch (RuntimeException ex) {
throw new ChatbotException(
"Failed to compute embeddings for chatbot ingestion: "
+ sanitizeRemoteMessage(ex.getMessage()),
ex);
}
if (response.getResults().size() != chunkTexts.size()) {
throw new ChatbotException("Mismatch between chunks and embedding results");
}
@@ -165,4 +178,11 @@ public class ChatbotIngestionService {
chunks.size());
return chunks;
}
private String sanitizeRemoteMessage(String message) {
if (!StringUtils.hasText(message)) {
return "unexpected provider error";
}
return message.replaceAll("(?i)api[-_ ]?key\\s*=[^\\s]+", "api-key=***");
}
}