improve reasoning toggle tests and refactor model capability checks
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
buildMemoryPrompt,
|
||||
buildPersonaStatePrompt,
|
||||
clampRetryActiveIndex,
|
||||
formatChatMessage,
|
||||
formatChatMessage as formatBaseChatMessage,
|
||||
formatMessagesForSummary,
|
||||
parseSources,
|
||||
retryStylePrompts
|
||||
@@ -21,7 +21,12 @@ import {
|
||||
import {SearXNGSearchProvider, WebSearchError, buildWebSearchContext, retrieveWebSearch} from "./webSearchService.js";
|
||||
import {supportsReasoning} from "../../public/app/modelCapabilities.js";
|
||||
|
||||
export function createChatService(repositories, modelService, config, webSearchProvider = new SearXNGSearchProvider(config)) {
|
||||
export function createChatService(repositories, modelService, config, webSearchProvider = new SearXNGSearchProvider(config), attachmentService = null) {
|
||||
const formatChatMessage = row => {
|
||||
const message = formatBaseChatMessage(row);
|
||||
const attachments = row && attachmentService?.list(row.id);
|
||||
return message && attachments?.length ? {...message, attachments} : message;
|
||||
};
|
||||
const chatGenerationQueues = new Map();
|
||||
const summaryQueues = new Map();
|
||||
const activeGenerations = new Map();
|
||||
@@ -272,6 +277,8 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
content: entry.content
|
||||
}));
|
||||
messagesPayload.push(...conversationMessages.slice(0, -1));
|
||||
const visionContext = attachmentService?.context(history);
|
||||
if (visionContext) messagesPayload.push(visionContext);
|
||||
if (webSearchContext) messagesPayload.push(webSearchContext[0]);
|
||||
if (webSearchContext?.[1]) messagesPayload.push(webSearchContext[1]);
|
||||
if (conversationMessages.length) messagesPayload.push(conversationMessages.at(-1));
|
||||
@@ -307,6 +314,7 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
return {fullReply: opener, thinking, promptMessageId: null};
|
||||
}
|
||||
|
||||
await attachmentService?.analyze(promptMessage.id, promptMessage.content, signal);
|
||||
const {messagesPayload} = buildConversationContext({
|
||||
user,
|
||||
chatId,
|
||||
@@ -327,7 +335,7 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
return {fullReply, thinking, promptMessageId: promptMessage.id};
|
||||
};
|
||||
|
||||
const generateChatTurn = ({user, chatId, message, model, 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}) =>
|
||||
withChatGenerationLock(chatId, async () => {
|
||||
await (summaryQueues.get(chatId) || Promise.resolve());
|
||||
const session = repositories.getChat(chatId, user);
|
||||
@@ -346,7 +354,8 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
const {model: effectiveModel, generation} = getGenerationSettings(session, model);
|
||||
const useReasoning = reasoning === true && supportsReasoning(effectiveModel);
|
||||
signal?.throwIfAborted();
|
||||
const inserted = repositories.insertBranchChatMessage(chatId, "user", message, null, null, "pending", null, effectiveParentId);
|
||||
const insert = () => repositories.insertBranchChatMessage(chatId, "user", message, null, null, "pending", null, effectiveParentId);
|
||||
const inserted = attachmentService ? attachmentService.createMessage(attachments, user, chatId, insert, reuseChatId) : insert();
|
||||
const userMessageId = Number(inserted?.lastInsertRowid);
|
||||
try {
|
||||
let sources = null;
|
||||
@@ -361,8 +370,13 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
const fullReply = await withActiveGeneration(chatId, signal, async (activeSignal) => {
|
||||
activeSignal.throwIfAborted();
|
||||
onStatus?.({status: "accepted"});
|
||||
if (attachments.length) {
|
||||
onStatus?.({status: "analyzing"});
|
||||
await attachmentService.analyze(userMessageId, message, activeSignal);
|
||||
}
|
||||
if (webSearch) {
|
||||
const search = await retrieveWebSearch(message, webSearchProvider, {
|
||||
referenceText: attachments.length ? attachmentService.searchContext(userMessageId) : "",
|
||||
signal: activeSignal,
|
||||
onSearching: () => onStatus?.({status: "searching"})
|
||||
});
|
||||
@@ -489,6 +503,7 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
parentMessageId
|
||||
);
|
||||
parentMessageId = Number(inserted.lastInsertRowid);
|
||||
attachmentService?.copy(sourceMessage.id, parentMessageId);
|
||||
if (sourceMessage.sources) repositories.updateChatMessageSources(clonedChat.id, parentMessageId, parseSources(sourceMessage.sources));
|
||||
if (sourceMessage.thinking) repositories.updateChatMessageThinking(clonedChat.id, parentMessageId, sourceMessage.thinking);
|
||||
}
|
||||
@@ -515,11 +530,11 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
containsUserVoiceInRoleplayOpener,
|
||||
persistChatMessageRetryState,
|
||||
buildMessagesPayload: buildConversationContext,
|
||||
async sendChatMessage({user, chatId, message, model, webSearch = false, reasoning = false}) {
|
||||
return generateChatTurn({user, chatId, message, model, webSearch, reasoning});
|
||||
async sendChatMessage({user, chatId, message, model, attachments = [], webSearch = false, reasoning = false}) {
|
||||
return generateChatTurn({user, chatId, message, model, attachments, webSearch, reasoning});
|
||||
},
|
||||
async streamChatMessage({user, chatId, message, model, webSearch = false, reasoning = false, onStatus, onChunk, onThinking, signal = null}) {
|
||||
return generateChatTurn({user, chatId, message, model, webSearch, reasoning, onStatus, onChunk, onThinking, signal});
|
||||
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 retryMessage({user, chatId, selectedModel, targetMessage, retryStyle = null}) {
|
||||
return withChatGenerationLock(chatId, async () => {
|
||||
@@ -574,6 +589,8 @@ export function createChatService(repositories, modelService, config, webSearchP
|
||||
const generated = await generateChatTurn({
|
||||
user,
|
||||
chatId: cloned.chat.id,
|
||||
attachments: attachmentService?.list(targetMessage.id).map(item => item.id) || [],
|
||||
reuseChatId: chatId,
|
||||
message: content,
|
||||
model,
|
||||
webSearch,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {supportsReasoning} from "../../public/app/modelCapabilities.js";
|
||||
import {AttachmentError} from "./attachmentService.js";
|
||||
|
||||
class ModelServiceError extends Error {
|
||||
constructor(code, message, status = 500) {
|
||||
@@ -196,7 +197,7 @@ export function createModelService(config, overrides = {}) {
|
||||
const response = await fetchImpl(`${config.modelApiBaseUrl}/chat`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(buildRequestBody(model, messagesPayload, options.generation, options.reasoning)),
|
||||
body: JSON.stringify({...buildRequestBody(model, messagesPayload, options.generation, options.reasoning), ...(options.format ? {format: options.format} : {})}),
|
||||
signal
|
||||
});
|
||||
|
||||
@@ -332,6 +333,7 @@ export function createModelService(config, overrides = {}) {
|
||||
};
|
||||
},
|
||||
mapError(error) {
|
||||
if (error instanceof AttachmentError) return {status: error.status, body: {error: error.message}};
|
||||
if (error instanceof ModelServiceError) {
|
||||
return {status: error.status, body: {error: error.message, code: error.code}};
|
||||
}
|
||||
|
||||
@@ -106,14 +106,16 @@ export function buildWebSearchQueries(message, decision = decideWebSearch(messag
|
||||
}
|
||||
|
||||
/** Retrieve before generation; only try a second date query when the first has no usable evidence. */
|
||||
export async function retrieveWebSearch(message, provider, {signal, onSearching = () => {}} = {}) {
|
||||
export async function retrieveWebSearch(message, provider, {signal, onSearching = () => {}, referenceText = ""} = {}) {
|
||||
const decision = decideWebSearch(message);
|
||||
if (!decision.shouldSearch) return {decision, state: "skipped", queries: [], candidates: [], sources: null};
|
||||
onSearching();
|
||||
const queries = [];
|
||||
let candidates = [];
|
||||
let sources = [];
|
||||
for (const query of buildWebSearchQueries(message, decision)) {
|
||||
// Image evidence may clarify "is this accurate?"; it must never alter the user's tool decision.
|
||||
const searchMessage = referenceText ? `${message} ${referenceText}` : message;
|
||||
for (const query of buildWebSearchQueries(searchMessage, decision)) {
|
||||
queries.push(query);
|
||||
try {
|
||||
const results = await provider.search(query, {
|
||||
@@ -125,7 +127,7 @@ export async function retrieveWebSearch(message, provider, {signal, onSearching
|
||||
const merged = [...results, ...candidates].sort((a, b) =>
|
||||
Number(Boolean(b?.snippet || b?.content)) - Number(Boolean(a?.snippet || a?.content)));
|
||||
candidates = normalizeSearchCandidates(merged, MAX_CANDIDATES * 2);
|
||||
sources = selectSearchResults(message, candidates, decision);
|
||||
sources = selectSearchResults(searchMessage, candidates, decision);
|
||||
if (sources.length) break;
|
||||
} catch (error) {
|
||||
if (signal?.aborted || queries.length === 1) throw error;
|
||||
|
||||
@@ -86,6 +86,7 @@ function validateMessage(message, chatIndex, messageIndex, errors) {
|
||||
content,
|
||||
model_name: optionalText(message.model_name || message.modelName),
|
||||
thinking: optionalText(message.thinking),
|
||||
attachments: asArray(message.attachments).filter(item => item && typeof item.id === "string" && /^[a-f0-9-]{36}$/.test(item.id)).slice(0, 100).map(item => ({id: item.id})),
|
||||
...(message.sources ? {sources: parseSources(message.sources)} : {}),
|
||||
retryVariants,
|
||||
retryActiveIndex: Number.isInteger(Number(message.retryActiveIndex ?? message.retry_active_index))
|
||||
|
||||
Reference in New Issue
Block a user