adjust attachments to hopefully fix them?

This commit is contained in:
Frederik Spirgi
2026-09-23 11:06:46 +02:00
parent b8399155dd
commit 4380f1f3d8
11 changed files with 305 additions and 62 deletions
+33 -22
View File
@@ -252,14 +252,14 @@ export function createChatService(repositories, modelService, config, webSearchP
return repositories.getActiveUserPersona(user);
};
const buildConversationContext = ({ user, chatId, session = null, message, upToMessageId = null, sources = null }) => {
const buildConversationContext = ({ user, chatId, session = null, message, upToMessageId = null, sources = null, historyOverride = null, visionSelection = null, contextSummaryOverride = undefined }) => {
const activeSession = session || repositories.getChat(chatId, user);
if (!activeSession) return null;
const activePersona = repositories.getAssistantPersonaForChat(chatId, user);
const activeUserPersona = getUserPersonaForSession(chatId, user, activeSession, activePersona);
const history = upToMessageId == null
const history = historyOverride || (upToMessageId == null
? repositories.getRecentChatMessages(chatId).reverse()
: repositories.getRecentChatMessagesUpToId(chatId, upToMessageId).reverse();
: repositories.getRecentChatMessagesUpToId(chatId, upToMessageId).reverse());
const conversation = message == null ? history : [...history, { role: "user", content: message }];
const messagesPayload = [];
// Result material remains a separate untrusted user-context message. The
@@ -282,7 +282,8 @@ export function createChatService(repositories, modelService, config, webSearchP
})
});
}
const contextSummaryPrompt = buildContextSummaryPrompt(upToMessageId == null ? activeSession.context_summary : null);
const contextSummaryPrompt = buildContextSummaryPrompt(contextSummaryOverride === undefined
? (upToMessageId == null ? activeSession.context_summary : null) : contextSummaryOverride);
if (contextSummaryPrompt) messagesPayload.push({ role: "system", content: contextSummaryPrompt });
const memoryPrompt = buildMemoryPrompt(repositories.listChatMemories(chatId, user));
if (memoryPrompt) messagesPayload.push({ role: "system", content: memoryPrompt });
@@ -294,7 +295,9 @@ export function createChatService(repositories, modelService, config, webSearchP
content: entry.content
}));
messagesPayload.push(...conversationMessages.slice(0, -1));
const visionContext = attachmentService?.context(history, conversation.at(-1)?.content || "");
const currentQuestion = conversation.at(-1)?.content || "";
const visionContext = attachmentService?.context(visionSelection || attachmentService.selectImages(
message == null ? history : [...history, {role: "user", content: message}], currentQuestion), currentQuestion);
if (visionContext) messagesPayload.push(...visionContext);
if (webSearchContext) messagesPayload.push(webSearchContext[0]);
if (webSearchContext?.[1]) messagesPayload.push(webSearchContext[1]);
@@ -302,7 +305,7 @@ export function createChatService(repositories, modelService, config, webSearchP
return { session: activeSession, activePersona, activeUserPersona, messagesPayload };
};
const buildRetryPayload = async ({ chatId, user, session, selectedModel, targetMessage, retryStyle = null, signal = null, generation = {}, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null }) => {
const buildRetryPayload = async ({ chatId, user, session, selectedModel, targetMessage, retryStyle = null, signal = null, generation = {}, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null, requestId = null }) => {
let thinking = "";
const captureThinking = (chunk, fullThinking) => {
if (!reasoning) return;
@@ -338,14 +341,16 @@ export function createChatService(repositories, modelService, config, webSearchP
}
const imageHistory = repositories.getRecentChatMessagesUpToId(chatId, promptMessage.id).reverse();
if (attachmentService?.hasImages(imageHistory)) onStatus?.({ status: "analyzing" });
await attachmentService?.analyzeContext(imageHistory, promptMessage.content, signal);
const visionSelection = attachmentService?.selectImages(imageHistory, promptMessage.content);
if (visionSelection && attachmentService.hasImages(visionSelection)) onStatus?.({ status: "analyzing" });
if (visionSelection) await attachmentService.analyzeContext(visionSelection, promptMessage.content, signal,
{requestId, chatId, triggeringMessageId: promptMessage.id});
let sources = null;
let searchState = "skipped";
if (webSearch) {
const search = await retrieveWebSearch(promptMessage.content, webSearchProvider, {
signal,
referenceText: attachmentService?.searchContext(imageHistory, promptMessage.content) || "",
referenceText: visionSelection ? attachmentService.searchContext(visionSelection, promptMessage.content) : "",
onSearching: () => onStatus?.({ status: "searching" })
});
sources = search.sources;
@@ -358,6 +363,8 @@ export function createChatService(repositories, modelService, config, webSearchP
session,
message: null,
upToMessageId: promptMessage.id,
historyOverride: imageHistory,
visionSelection,
sources
});
@@ -372,7 +379,7 @@ export function createChatService(repositories, modelService, config, webSearchP
return { fullReply, thinking, sources, searchState, promptMessageId: promptMessage.id };
};
const generateChatTurn = ({ user, chatId, message, model, attachments = [], reuseChatId = null, webSearch = false, reasoning = false, onStatus = null, onChunk = null, onThinking = null, signal = null, parentMessageId = undefined }) =>
const generateChatTurn = ({ user, chatId, message, model, attachments = [], reuseChatId = null, webSearch = false, reasoning = false, onStatus = null, onChunk = null, onThinking = null, signal = null, parentMessageId = undefined, requestId = null }) =>
withChatGenerationLock(chatId, async () => {
await (summaryQueues.get(chatId) || Promise.resolve());
const session = repositories.getChat(chatId, user);
@@ -408,13 +415,15 @@ export function createChatService(repositories, modelService, config, webSearchP
activeSignal.throwIfAborted();
onStatus?.({ status: "accepted" });
const imageHistory = repositories.getRecentChatMessagesUpToId(chatId, userMessageId).reverse();
if (attachmentService?.hasImages(imageHistory)) {
const visionSelection = attachmentService?.selectImages(imageHistory, message);
if (visionSelection && attachmentService.hasImages(visionSelection)) {
onStatus?.({ status: "analyzing" });
await attachmentService.analyzeContext(imageHistory, message, activeSignal);
await attachmentService.analyzeContext(visionSelection, message, activeSignal,
{requestId, chatId, triggeringMessageId: userMessageId});
}
if (webSearch) {
const search = await retrieveWebSearch(message, webSearchProvider, {
referenceText: attachmentService?.searchContext(imageHistory, message) || "",
referenceText: visionSelection ? attachmentService.searchContext(visionSelection, message) : "",
signal: activeSignal,
onSearching: () => onStatus?.({ status: "searching" })
});
@@ -437,7 +446,8 @@ export function createChatService(repositories, modelService, config, webSearchP
}
}
onStatus?.({ status: "generating", searchState, ...(sources !== null ? { sourceCount: sources.length } : {}) });
payload = buildConversationContext({ user, chatId, session, message: null, sources });
payload = buildConversationContext({ user, chatId, session, message: null, upToMessageId: userMessageId,
historyOverride: imageHistory, visionSelection, contextSummaryOverride: session.context_summary, sources });
return payload.activePersona
? generateValidatedRoleplayReply({
selectedModel: effectiveModel,
@@ -568,15 +578,15 @@ export function createChatService(repositories, modelService, config, webSearchP
containsUserVoiceInRoleplayOpener,
persistChatMessageRetryState,
buildMessagesPayload: buildConversationContext,
async sendChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false }) {
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning });
async sendChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, requestId = null }) {
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, requestId });
},
async streamChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, onStatus, onChunk, onThinking, signal = null }) {
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, onStatus, onChunk, onThinking, signal });
async streamChatMessage({ user, chatId, message, model, attachments = [], webSearch = false, reasoning = false, onStatus, onChunk, onThinking, signal = null, requestId = null }) {
return generateChatTurn({ user, chatId, message, model, attachments, webSearch, reasoning, onStatus, onChunk, onThinking, signal, requestId });
},
isGenerating: (chatId) => chatGenerationQueues.has(chatId),
isRegeneratingMessage: (chatId, messageId) => regeneratingMessages.get(chatId) === messageId,
async retryMessage({ user, chatId, selectedModel, targetMessage, retryStyle = null, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null, signal = null }) {
async retryMessage({ user, chatId, selectedModel, targetMessage, retryStyle = null, webSearch = false, reasoning = false, onChunk = null, onThinking = null, onStatus = null, signal = null, requestId = null }) {
if (chatGenerationQueues.has(chatId)) return { error: "A response is already being generated", status: 409 };
regeneratingMessages.set(chatId, targetMessage.id);
return withChatGenerationLock(chatId, async () => {
@@ -596,7 +606,7 @@ export function createChatService(repositories, modelService, config, webSearchP
activeSignal.throwIfAborted();
const retryResult = await buildRetryPayload({
chatId, user, session, selectedModel: model, targetMessage: freshTarget, retryStyle,
signal: activeSignal, generation, webSearch, reasoning: reasoning === true && supportsReasoning(model), onChunk, onThinking, onStatus
signal: activeSignal, generation, webSearch, reasoning: reasoning === true && supportsReasoning(model), onChunk, onThinking, onStatus, requestId
});
if (retryResult.error) return { error: retryResult.error, status: 400 };
activeSignal.throwIfAborted();
@@ -614,7 +624,7 @@ export function createChatService(repositories, modelService, config, webSearchP
throw error;
}).finally(() => regeneratingMessages.delete(chatId));
},
async editAndResendMessage({ user, chatId, targetMessage, content, model, webSearch = false, onChunk = null, signal = null }) {
async editAndResendMessage({ user, chatId, targetMessage, content, model, webSearch = false, onChunk = null, signal = null, requestId = 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 };
@@ -634,7 +644,8 @@ export function createChatService(repositories, modelService, config, webSearchP
model,
webSearch,
onChunk,
signal
signal,
requestId
});
if (generated.error) return { ...generated, chat: cloned.chat };
return {