show reasoning in frontend
This commit is contained in:
@@ -41,6 +41,7 @@ export function formatChatMessage(row) {
|
||||
id: row.id,
|
||||
role: row.role,
|
||||
content: row.content,
|
||||
...(row.thinking ? {thinking: row.thinking} : {}),
|
||||
...(row.sources ? {sources: parseSources(row.sources)} : {}),
|
||||
modelName: row.model_name || null,
|
||||
retryVariants,
|
||||
|
||||
+31
-17
@@ -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)));
|
||||
|
||||
@@ -74,7 +74,7 @@ export function createModelService(config, overrides = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const readStreamingReply = async (response, onChunk = null) => {
|
||||
const readStreamingReply = async (response, onChunk = null, onThinking = null) => {
|
||||
if (!response.body) {
|
||||
throw new ModelServiceError("MODEL_BAD_RESPONSE", "Model backend returned no body.", 502);
|
||||
}
|
||||
@@ -83,6 +83,18 @@ export function createModelService(config, overrides = {}) {
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let fullReply = "";
|
||||
let fullThinking = "";
|
||||
const consume = (line) => {
|
||||
const data = JSON.parse(line);
|
||||
if (data.message?.thinking) {
|
||||
fullThinking += data.message.thinking;
|
||||
onThinking?.(data.message.thinking, fullThinking);
|
||||
}
|
||||
if (data.message?.content) {
|
||||
fullReply += data.message.content;
|
||||
onChunk?.(data.message.content, fullReply);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const {value, done} = await reader.read();
|
||||
@@ -93,20 +105,12 @@ export function createModelService(config, overrides = {}) {
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
const data = JSON.parse(trimmed);
|
||||
if (data.message?.content) {
|
||||
fullReply += data.message.content;
|
||||
if (onChunk) onChunk(data.message.content, fullReply);
|
||||
}
|
||||
consume(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
const data = JSON.parse(buffer.trim());
|
||||
if (data.message?.content) {
|
||||
fullReply += data.message.content;
|
||||
if (onChunk) onChunk(data.message.content, fullReply);
|
||||
}
|
||||
consume(buffer.trim());
|
||||
}
|
||||
|
||||
return fullReply.trim();
|
||||
@@ -146,7 +150,7 @@ export function createModelService(config, overrides = {}) {
|
||||
throw new ModelServiceError("MODEL_UPSTREAM_ERROR", `Model backend returned ${response.status}.`, 502);
|
||||
}
|
||||
|
||||
return readStreamingReply(response);
|
||||
return readStreamingReply(response, null, options.onThinking);
|
||||
}, options.signal);
|
||||
lastHealth = {ok: true, checkedAt: new Date().toISOString(), error: null, latencyMs: Date.now() - startedAt};
|
||||
return fullReply;
|
||||
@@ -184,7 +188,7 @@ export function createModelService(config, overrides = {}) {
|
||||
throw new ModelServiceError("MODEL_UPSTREAM_ERROR", `Model backend returned ${response.status}.`, 502);
|
||||
}
|
||||
|
||||
return readStreamingReply(response, onChunk);
|
||||
return readStreamingReply(response, onChunk, options.onThinking);
|
||||
}, options.signal);
|
||||
lastHealth = {ok: true, checkedAt: new Date().toISOString(), error: null, latencyMs: Date.now() - startedAt};
|
||||
return fullReply;
|
||||
|
||||
@@ -85,6 +85,7 @@ function validateMessage(message, chatIndex, messageIndex, errors) {
|
||||
role: role === "assistant" ? "bot" : role,
|
||||
content,
|
||||
model_name: optionalText(message.model_name || message.modelName),
|
||||
thinking: optionalText(message.thinking),
|
||||
...(message.sources ? {sources: parseSources(message.sources)} : {}),
|
||||
retryVariants,
|
||||
retryActiveIndex: Number.isInteger(Number(message.retryActiveIndex ?? message.retry_active_index))
|
||||
|
||||
Reference in New Issue
Block a user