show reasoning in frontend

This commit is contained in:
2026-09-21 19:50:16 +02:00
parent 8c13f50bd1
commit ceb3c752ff
12 changed files with 104 additions and 40 deletions
+31 -17
View File
@@ -168,7 +168,7 @@ export function createChatService(repositories, modelService, config, webSearchP
return repositories.createChat(user, persona.name, persona.id, userPersonaId, scenarioPrompt, scenarioSummary);
};
const generateRoleplayOpener = async ({selectedModel = "qwen3:14b", assistantPersona, userPersona, scenarioPrompt, sceneSummary}) => {
const generateRoleplayOpener = async ({selectedModel = "qwen3:14b", assistantPersona, userPersona, scenarioPrompt, sceneSummary, onThinking = null}) => {
const baseMessages = [
{role: "system", content: buildPersonaPrompt(assistantPersona)},
...(userPersona ? [{role: "system", content: buildUserPersonaPrompt(userPersona)}] : []),
@@ -176,11 +176,12 @@ export function createChatService(repositories, modelService, config, webSearchP
{role: "user", content: buildRoleplayOpenerPrompt({assistantPersona, userPersona, scenarioPrompt, sceneSummary})}
];
let opener = await modelService.generateReply(selectedModel, baseMessages);
let opener = await modelService.generateReply(selectedModel, baseMessages, {onThinking});
if (!containsUserVoiceInRoleplayOpener(opener, userPersona)) {
return opener;
}
onThinking?.("", "");
return modelService.generateReply(selectedModel, [
...baseMessages,
{role: "assistant", content: opener || ""},
@@ -188,15 +189,16 @@ export function createChatService(repositories, modelService, config, webSearchP
role: "user",
content: "Rewrite this opening. Hard rule: only the assistant character may speak or act. Never write or imply user speech, thoughts, feelings, reactions, decisions, actions, or labels like 'User:'/'You:'. Stop before the user's reply."
}
]);
], {onThinking});
};
const generateValidatedRoleplayReply = async ({selectedModel, messagesPayload, userPersona, signal = null, generation = {}}) => {
let reply = await modelService.generateReply(selectedModel, messagesPayload, {signal, generation});
const generateValidatedRoleplayReply = async ({selectedModel, messagesPayload, userPersona, signal = null, generation = {}, onThinking = null}) => {
let reply = await modelService.generateReply(selectedModel, messagesPayload, {signal, generation, onThinking});
if (reply && !containsUserVoiceInRoleplayOpener(reply, userPersona)) {
return reply;
}
onThinking?.("", "");
reply = await modelService.generateReply(selectedModel, [
...messagesPayload,
{role: "assistant", content: reply || ""},
@@ -204,7 +206,7 @@ export function createChatService(repositories, modelService, config, webSearchP
role: "user",
content: "Rewrite your last reply. Hard rule: only the assistant character may speak or act. Never write or imply user speech, thoughts, feelings, reactions, decisions, or actions. Stop before the user's reply."
}
], {signal, generation});
], {signal, generation, onThinking});
if (!reply || containsUserVoiceInRoleplayOpener(reply, userPersona)) {
return null;
@@ -283,7 +285,9 @@ export function createChatService(repositories, modelService, config, webSearchP
if (!activePersona || !session.scenario_summary) {
return {error: "No previous user prompt found for retry"};
}
let thinking = "";
const opener = await generateRoleplayOpener({
onThinking: (_chunk, fullThinking) => { thinking = fullThinking; },
selectedModel,
assistantPersona: activePersona,
userPersona: activeUserPersona,
@@ -293,7 +297,7 @@ export function createChatService(repositories, modelService, config, webSearchP
if (!opener || containsUserVoiceInRoleplayOpener(opener, activeUserPersona)) {
return {error: "Failed to regenerate message"};
}
return {fullReply: opener, promptMessageId: null};
return {fullReply: opener, thinking, promptMessageId: null};
}
const {messagesPayload} = buildConversationContext({
@@ -306,15 +310,17 @@ export function createChatService(repositories, modelService, config, webSearchP
});
const styledPayload = appendRetryStyleInstruction(messagesPayload, retryStyle);
let thinking = "";
const onThinking = (_chunk, fullThinking) => { thinking = fullThinking; };
const fullReply = activePersona
? await generateValidatedRoleplayReply({selectedModel, messagesPayload: styledPayload, userPersona: activeUserPersona, signal, generation})
: await modelService.generateReply(selectedModel, styledPayload, {signal, generation});
? await generateValidatedRoleplayReply({selectedModel, messagesPayload: styledPayload, userPersona: activeUserPersona, signal, generation, onThinking})
: await modelService.generateReply(selectedModel, styledPayload, {signal, generation, onThinking});
if (!fullReply) return {error: "Failed to regenerate message"};
return {fullReply, promptMessageId: promptMessage.id};
return {fullReply, thinking, promptMessageId: promptMessage.id};
};
const generateChatTurn = ({user, chatId, message, model, webSearch = false, onStatus = null, onChunk = null, signal = null, parentMessageId = undefined}) =>
const generateChatTurn = ({user, chatId, message, model, webSearch = false, onStatus = null, onChunk = null, onThinking = null, signal = null, parentMessageId = undefined}) =>
withChatGenerationLock(chatId, async () => {
await (summaryQueues.get(chatId) || Promise.resolve());
const session = repositories.getChat(chatId, user);
@@ -335,6 +341,11 @@ export function createChatService(repositories, modelService, config, webSearchP
const userMessageId = Number(inserted?.lastInsertRowid);
try {
let sources = null;
let thinking = "";
const captureThinking = (chunk, fullThinking) => {
thinking = fullThinking;
onThinking?.(chunk, fullThinking);
};
let payload;
const fullReply = await withActiveGeneration(chatId, signal, async (activeSignal) => {
if (webSearch) {
@@ -375,11 +386,12 @@ export function createChatService(repositories, modelService, config, webSearchP
messagesPayload: payload.messagesPayload,
userPersona: payload.activeUserPersona,
signal: activeSignal,
generation
generation,
onThinking: captureThinking
})
: onChunk
? modelService.streamReply(effectiveModel, payload.messagesPayload, onChunk, {signal: activeSignal, generation})
: modelService.generateReply(effectiveModel, payload.messagesPayload, {signal: activeSignal, generation});
? modelService.streamReply(effectiveModel, payload.messagesPayload, onChunk, {signal: activeSignal, generation, onThinking: captureThinking})
: modelService.generateReply(effectiveModel, payload.messagesPayload, {signal: activeSignal, generation, onThinking: captureThinking});
});
if (!fullReply) {
@@ -394,7 +406,7 @@ export function createChatService(repositories, modelService, config, webSearchP
retryRetriesUsed: 0,
retryPromptMessageId: null
};
repositories.completeChatTurn(chatId, userMessageId, fullReply, retryState, effectiveModel, sources);
repositories.completeChatTurn(chatId, userMessageId, fullReply, retryState, effectiveModel, sources, thinking);
repositories.touchChat(chatId, user);
scheduleContextSummaryUpdate({user, chatId, selectedModel: effectiveModel});
return {reply: fullReply, userMessageId, ...(sources ? {sources} : {})};
@@ -471,6 +483,7 @@ export function createChatService(repositories, modelService, config, webSearchP
);
parentMessageId = Number(inserted.lastInsertRowid);
if (sourceMessage.sources) repositories.updateChatMessageSources(clonedChat.id, parentMessageId, parseSources(sourceMessage.sources));
if (sourceMessage.thinking) repositories.updateChatMessageThinking(clonedChat.id, parentMessageId, sourceMessage.thinking);
}
} catch (error) {
repositories.deleteChat(clonedChat.id, user);
@@ -498,8 +511,8 @@ export function createChatService(repositories, modelService, config, webSearchP
async sendChatMessage({user, chatId, message, model, webSearch = false}) {
return generateChatTurn({user, chatId, message, model, webSearch});
},
async streamChatMessage({user, chatId, message, model, webSearch = false, onStatus, onChunk, signal = null}) {
return generateChatTurn({user, chatId, message, model, webSearch, onStatus, onChunk, signal});
async streamChatMessage({user, chatId, message, model, webSearch = false, onStatus, onChunk, onThinking, signal = null}) {
return generateChatTurn({user, chatId, message, model, webSearch, onStatus, onChunk, onThinking, signal});
},
async retryMessage({user, chatId, selectedModel, targetMessage, retryStyle = null}) {
return withChatGenerationLock(chatId, async () => {
@@ -532,6 +545,7 @@ export function createChatService(repositories, modelService, config, webSearchP
chatId, "bot", retryResult.fullReply, retryState, model, "complete", null, freshTarget.parent_message_id
);
if (freshTarget.sources) repositories.updateChatMessageSources(chatId, Number(inserted.lastInsertRowid), parseSources(freshTarget.sources));
if (retryResult.thinking) repositories.updateChatMessageThinking(chatId, Number(inserted.lastInsertRowid), retryResult.thinking);
repositories.updateChatContextSummary(chatId, user, null, 0);
repositories.touchChat(chatId, user);
const message = formatChatMessage(repositories.getChatMessage(chatId, Number(inserted.lastInsertRowid)));