add optional web search functionality with UI updates and backend integration

This commit is contained in:
2026-09-20 20:41:01 +02:00
parent abd5238252
commit 781f15b503
16 changed files with 603 additions and 41 deletions
+43 -25
View File
@@ -14,10 +14,13 @@ import {
clampRetryActiveIndex,
formatChatMessage,
formatMessagesForSummary,
parseSources,
retryStylePrompts
} from "./chatMessageHelpers.js";
export function createChatService(repositories, modelService, config) {
import {SearXNGSearchProvider, WebSearchError, buildWebSearchContext} from "./webSearchService.js";
export function createChatService(repositories, modelService, config, webSearchProvider = new SearXNGSearchProvider(config)) {
const chatGenerationQueues = new Map();
const summaryQueues = new Map();
const activeGenerations = new Map();
@@ -219,7 +222,7 @@ export function createChatService(repositories, modelService, config) {
return repositories.getActiveUserPersona(user);
};
const buildConversationContext = ({user, chatId, session = null, message, upToMessageId = null}) => {
const buildConversationContext = ({user, chatId, session = null, message, upToMessageId = null, sources = null}) => {
const activeSession = session || repositories.getChat(chatId, user);
if (!activeSession) return null;
const activePersona = repositories.getAssistantPersonaForChat(chatId, user);
@@ -251,6 +254,7 @@ export function createChatService(repositories, modelService, config) {
if (memoryPrompt) messagesPayload.push({role: "system", content: memoryPrompt});
const personaStatePrompt = buildPersonaStatePrompt(repositories.getPersonaChatState(chatId, user));
if (personaStatePrompt) messagesPayload.push({role: "system", content: personaStatePrompt});
if (sources) messagesPayload.push(...buildWebSearchContext(sources));
messagesPayload.push(...conversation.map((entry) => ({
role: entry.role === "bot" ? "assistant" : "user",
content: entry.content
@@ -291,7 +295,8 @@ export function createChatService(repositories, modelService, config) {
chatId,
session,
message: null,
upToMessageId: promptMessage.id
upToMessageId: promptMessage.id,
sources: targetMessage.sources ? parseSources(targetMessage.sources) : null
});
const styledPayload = appendRetryStyleInstruction(messagesPayload, retryStyle);
@@ -303,7 +308,7 @@ export function createChatService(repositories, modelService, config) {
return {fullReply, promptMessageId: promptMessage.id};
};
const generateChatTurn = ({user, chatId, message, model, onChunk = null, signal = null, parentMessageId = undefined}) =>
const generateChatTurn = ({user, chatId, message, model, webSearch = false, onStatus = null, onChunk = null, signal = null, parentMessageId = undefined}) =>
withChatGenerationLock(chatId, async () => {
await (summaryQueues.get(chatId) || Promise.resolve());
const session = repositories.getChat(chatId, user);
@@ -323,21 +328,30 @@ export function createChatService(repositories, modelService, config) {
const inserted = repositories.insertBranchChatMessage(chatId, "user", message, null, null, "pending", null, effectiveParentId);
const userMessageId = Number(inserted?.lastInsertRowid);
try {
const payload = buildConversationContext({user, chatId, session, message: null});
const fullReply = await withActiveGeneration(chatId, signal, (activeSignal) => payload.activePersona
? generateValidatedRoleplayReply({
selectedModel: effectiveModel,
messagesPayload: payload.messagesPayload,
userPersona: payload.activeUserPersona,
signal: activeSignal,
generation
})
: onChunk
? modelService.streamReply(effectiveModel, payload.messagesPayload, onChunk, {signal: activeSignal, generation})
: modelService.generateReply(effectiveModel, payload.messagesPayload, {signal: activeSignal, generation}));
let sources = null;
let payload;
const fullReply = await withActiveGeneration(chatId, signal, async (activeSignal) => {
if (webSearch) {
onStatus?.({status: "searching"});
sources = await webSearchProvider.search(message, {signal: activeSignal, limit: 5});
onStatus?.({status: "generating", sourceCount: sources.length});
}
payload = buildConversationContext({user, chatId, session, message: null, sources});
return payload.activePersona
? generateValidatedRoleplayReply({
selectedModel: effectiveModel,
messagesPayload: payload.messagesPayload,
userPersona: payload.activeUserPersona,
signal: activeSignal,
generation
})
: onChunk
? modelService.streamReply(effectiveModel, payload.messagesPayload, onChunk, {signal: activeSignal, generation})
: modelService.generateReply(effectiveModel, payload.messagesPayload, {signal: activeSignal, generation});
});
if (!fullReply) {
repositories.failChatTurn(chatId, userMessageId, "Failed to generate a valid character reply", effectiveParentId);
repositories.failChatTurn(chatId, userMessageId, "Failed to generate a valid character reply", webSearch ? userMessageId : effectiveParentId);
return {error: "Failed to generate a valid character reply", status: 500};
}
if (payload.activePersona && onChunk) onChunk(fullReply, fullReply);
@@ -348,13 +362,14 @@ export function createChatService(repositories, modelService, config) {
retryRetriesUsed: 0,
retryPromptMessageId: null
};
repositories.completeChatTurn(chatId, userMessageId, fullReply, retryState, effectiveModel);
repositories.completeChatTurn(chatId, userMessageId, fullReply, retryState, effectiveModel, sources);
repositories.touchChat(chatId, user);
scheduleContextSummaryUpdate({user, chatId, selectedModel: effectiveModel});
return {reply: fullReply, userMessageId};
return {reply: fullReply, userMessageId, ...(sources ? {sources} : {})};
} catch (error) {
repositories.failChatTurn(chatId, userMessageId, String(error?.message || "Model request failed").slice(0, 500), effectiveParentId);
repositories.failChatTurn(chatId, userMessageId, String(error?.message || "Model request failed").slice(0, 500), webSearch ? userMessageId : effectiveParentId);
repositories.touchChat(chatId, user);
if (error instanceof WebSearchError) return {error: error.message, status: error.status};
throw error;
}
});
@@ -423,6 +438,7 @@ export function createChatService(repositories, modelService, config) {
parentMessageId
);
parentMessageId = Number(inserted.lastInsertRowid);
if (sourceMessage.sources) repositories.updateChatMessageSources(clonedChat.id, parentMessageId, parseSources(sourceMessage.sources));
}
} catch (error) {
repositories.deleteChat(clonedChat.id, user);
@@ -447,11 +463,11 @@ export function createChatService(repositories, modelService, config) {
containsUserVoiceInRoleplayOpener,
persistChatMessageRetryState,
buildMessagesPayload: buildConversationContext,
async sendChatMessage({user, chatId, message, model}) {
return generateChatTurn({user, chatId, message, model});
async sendChatMessage({user, chatId, message, model, webSearch = false}) {
return generateChatTurn({user, chatId, message, model, webSearch});
},
async streamChatMessage({user, chatId, message, model, onChunk, signal = null}) {
return generateChatTurn({user, chatId, message, model, onChunk, signal});
async streamChatMessage({user, chatId, message, model, webSearch = false, onStatus, onChunk, signal = null}) {
return generateChatTurn({user, chatId, message, model, webSearch, onStatus, onChunk, signal});
},
async retryMessage({user, chatId, selectedModel, targetMessage, retryStyle = null}) {
return withChatGenerationLock(chatId, async () => {
@@ -483,6 +499,7 @@ export function createChatService(repositories, modelService, config) {
const inserted = repositories.insertBranchChatMessage(
chatId, "bot", retryResult.fullReply, retryState, model, "complete", null, freshTarget.parent_message_id
);
if (freshTarget.sources) repositories.updateChatMessageSources(chatId, Number(inserted.lastInsertRowid), parseSources(freshTarget.sources));
repositories.updateChatContextSummary(chatId, user, null, 0);
repositories.touchChat(chatId, user);
const message = formatChatMessage(repositories.getChatMessage(chatId, Number(inserted.lastInsertRowid)));
@@ -490,7 +507,7 @@ export function createChatService(repositories, modelService, config) {
});
});
},
async editAndResendMessage({user, chatId, targetMessage, content, model, onChunk = null, signal = null}) {
async editAndResendMessage({user, chatId, targetMessage, content, model, webSearch = false, onChunk = null, signal = null}) {
if (!targetMessage || targetMessage.role !== "user") return {error: "Only user messages can be edited and resent", status: 400};
const sourceChat = repositories.getChat(chatId, user);
if (!sourceChat) return {error: "Chat not found", status: 404};
@@ -506,6 +523,7 @@ export function createChatService(repositories, modelService, config) {
chatId: cloned.chat.id,
message: content,
model,
webSearch,
onChunk,
signal
});