mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
improving context
This commit is contained in:
@@ -28,10 +28,12 @@ public class ChatbotVectorStoreConfig {
|
||||
public VectorStore chatbotVectorStore(
|
||||
ObjectProvider<JedisPooled> jedisProvider, EmbeddingModel embeddingModel) {
|
||||
JedisPooled jedis = jedisProvider.getIfAvailable();
|
||||
|
||||
if (jedis != null) {
|
||||
try {
|
||||
jedis.ping();
|
||||
log.info("Initialising Redis vector store for chatbot usage");
|
||||
|
||||
return RedisVectorStore.builder(jedis, embeddingModel)
|
||||
.indexName(DEFAULT_INDEX)
|
||||
.prefix(DEFAULT_PREFIX)
|
||||
@@ -45,6 +47,7 @@ public class ChatbotVectorStoreConfig {
|
||||
} else {
|
||||
log.info("No Redis connection detected; using SimpleVectorStore for chatbot.");
|
||||
}
|
||||
|
||||
return SimpleVectorStore.builder(embeddingModel).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ public class ChatbotController {
|
||||
warnings.add("Images detected - Images are not currently supported.");
|
||||
}
|
||||
|
||||
warnings.add("Only extracted text is sent for analysis.");
|
||||
warnings.add("Images are not yet supported. Only extracted text is sent for analysis.");
|
||||
if (session != null && session.isOcrRequested()) {
|
||||
warnings.add("OCR requested – uses credits .");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package stirling.software.proprietary.service.chatbot;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@Component
|
||||
public class ChatbotContextCompressor {
|
||||
|
||||
private static final int DEFAULT_SUMMARY_LIMIT = 3000;
|
||||
private static final int MIN_CHUNK_SNIPPET = 160;
|
||||
|
||||
public String summarize(List<Document> documents, int requestedLimit) {
|
||||
if (CollectionUtils.isEmpty(documents)) {
|
||||
return "No contextual snippets available for this session.";
|
||||
}
|
||||
int maxChars =
|
||||
requestedLimit > 0
|
||||
? Math.min(requestedLimit, DEFAULT_SUMMARY_LIMIT)
|
||||
: DEFAULT_SUMMARY_LIMIT;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int perChunkLimit = Math.max(MIN_CHUNK_SNIPPET, maxChars / Math.max(documents.size(), 1));
|
||||
for (Document doc : documents) {
|
||||
if (builder.length() >= maxChars) {
|
||||
break;
|
||||
}
|
||||
String chunkOrder = doc.getMetadata().getOrDefault("chunkOrder", "?").toString();
|
||||
String text = trimContent(doc.getText(), perChunkLimit);
|
||||
builder.append("Chunk ").append(chunkOrder).append(": ").append(text).append('\n');
|
||||
}
|
||||
if (builder.length() == 0) {
|
||||
return "Unable to summarise context; original content unavailable.";
|
||||
}
|
||||
return builder.substring(0, Math.min(builder.length(), maxChars)).trim();
|
||||
}
|
||||
|
||||
private String trimContent(String content, int perChunkLimit) {
|
||||
if (content == null || content.isBlank()) {
|
||||
return "(empty chunk)";
|
||||
}
|
||||
String normalized = content.replaceAll("\\s+", " ").trim();
|
||||
if (normalized.length() <= perChunkLimit) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.substring(0, Math.max(0, perChunkLimit - 3)) + "...";
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ public class ChatbotConversationService {
|
||||
private final ChatbotCacheService cacheService;
|
||||
private final ChatbotFeatureProperties featureProperties;
|
||||
private final ChatbotRetrievalService retrievalService;
|
||||
private final ChatbotContextCompressor contextCompressor;
|
||||
private final ChatbotMemoryService memoryService;
|
||||
private final ChatbotUsageService usageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AtomicBoolean modelSwitchVerified = new AtomicBoolean(false);
|
||||
@@ -79,6 +81,9 @@ public class ChatbotConversationService {
|
||||
List<Document> context =
|
||||
retrievalService.retrieveTopK(
|
||||
request.getSessionId(), request.getPrompt(), settings);
|
||||
String contextSummary =
|
||||
contextCompressor.summarize(
|
||||
context, (int) Math.max(settings.maxPromptCharacters() / 2, 1000));
|
||||
|
||||
ModelReply nanoReply =
|
||||
invokeModel(
|
||||
@@ -87,6 +92,7 @@ public class ChatbotConversationService {
|
||||
request.getPrompt(),
|
||||
session,
|
||||
context,
|
||||
contextSummary,
|
||||
cacheEntry.getMetadata());
|
||||
|
||||
boolean shouldEscalate =
|
||||
@@ -106,6 +112,7 @@ public class ChatbotConversationService {
|
||||
request.getPrompt(),
|
||||
session,
|
||||
context,
|
||||
contextSummary,
|
||||
cacheEntry.getMetadata());
|
||||
}
|
||||
|
||||
@@ -116,6 +123,8 @@ public class ChatbotConversationService {
|
||||
finalReply.completionTokens());
|
||||
session.setUsageSummary(usageSummary);
|
||||
|
||||
memoryService.recordTurn(session, request.getPrompt(), finalReply.answer());
|
||||
|
||||
return ChatbotResponse.builder()
|
||||
.sessionId(request.getSessionId())
|
||||
.modelUsed(
|
||||
@@ -200,8 +209,10 @@ public class ChatbotConversationService {
|
||||
String prompt,
|
||||
ChatbotSession session,
|
||||
List<Document> context,
|
||||
String contextSummary,
|
||||
Map<String, String> metadata) {
|
||||
Prompt requestPrompt = buildPrompt(settings, model, prompt, session, context, metadata);
|
||||
Prompt requestPrompt =
|
||||
buildPrompt(settings, model, prompt, session, context, contextSummary, metadata);
|
||||
ChatResponse response;
|
||||
try {
|
||||
response = chatModel.call(requestPrompt);
|
||||
@@ -244,16 +255,9 @@ public class ChatbotConversationService {
|
||||
String question,
|
||||
ChatbotSession session,
|
||||
List<Document> context,
|
||||
String contextSummary,
|
||||
Map<String, String> metadata) {
|
||||
StringBuilder contextBuilder = new StringBuilder();
|
||||
for (Document chunk : context) {
|
||||
contextBuilder
|
||||
.append("[Chunk ")
|
||||
.append(chunk.getMetadata().getOrDefault("chunkOrder", "?"))
|
||||
.append("]\n")
|
||||
.append(chunk.getText())
|
||||
.append("\n\n");
|
||||
}
|
||||
String chunkOutline = buildChunkOutline(context);
|
||||
String metadataSummary =
|
||||
metadata.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + ": " + entry.getValue())
|
||||
@@ -277,8 +281,10 @@ public class ChatbotConversationService {
|
||||
+ session.isOcrRequested()
|
||||
+ "\n"
|
||||
+ imageDirective
|
||||
+ "\nContext:\n"
|
||||
+ contextBuilder
|
||||
+ "\nContext summary:\n"
|
||||
+ contextSummary
|
||||
+ "\nContext outline:\n"
|
||||
+ chunkOutline
|
||||
+ "Question: "
|
||||
+ question;
|
||||
|
||||
@@ -298,6 +304,27 @@ public class ChatbotConversationService {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String buildChunkOutline(List<Document> context) {
|
||||
if (context == null || context.isEmpty()) {
|
||||
return "No chunks retrieved for this question.";
|
||||
}
|
||||
StringBuilder outline = new StringBuilder();
|
||||
for (Document chunk : context) {
|
||||
String order = chunk.getMetadata().getOrDefault("chunkOrder", "?").toString();
|
||||
String snippet = chunk.getText();
|
||||
if (snippet != null) {
|
||||
snippet = snippet.replaceAll("\\s+", " ").trim();
|
||||
if (snippet.length() > 240) {
|
||||
snippet = snippet.substring(0, 237) + "...";
|
||||
}
|
||||
} else {
|
||||
snippet = "(empty)";
|
||||
}
|
||||
outline.append("- Chunk ").append(order).append(": ").append(snippet).append("\n");
|
||||
}
|
||||
return outline.toString();
|
||||
}
|
||||
|
||||
private ModelReply parseModelResponse(
|
||||
String raw, long promptTokens, long completionTokens, long totalTokens) {
|
||||
if (!StringUtils.hasText(raw)) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package stirling.software.proprietary.service.chatbot;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.model.chatbot.ChatbotSession;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChatbotMemoryService {
|
||||
|
||||
private final VectorStore vectorStore;
|
||||
|
||||
public void recordTurn(ChatbotSession session, String prompt, String answer) {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasText(prompt) && !StringUtils.hasText(answer)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("sessionId", session.getSessionId());
|
||||
metadata.put("documentId", session.getDocumentId());
|
||||
metadata.put("turnType", "conversation");
|
||||
metadata.put("turnTimestamp", Instant.now().toString());
|
||||
metadata.put("userId", session.getUserId());
|
||||
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
if (StringUtils.hasText(prompt)) {
|
||||
contentBuilder.append("User: ").append(prompt.trim()).append("\n");
|
||||
}
|
||||
if (StringUtils.hasText(answer)) {
|
||||
contentBuilder.append("Assistant: ").append(answer.trim());
|
||||
}
|
||||
try {
|
||||
vectorStore.add(List.of(new Document(contentBuilder.toString(), metadata)));
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("Failed to persist chatbot conversation turn: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user