diff --git a/common/chat.ts b/common/chat.ts index c17eb67bc..c6388a205 100644 --- a/common/chat.ts +++ b/common/chat.ts @@ -42,7 +42,7 @@ export function toChatGraph(messages: AppSchema.ChatMessage[]): { tree: ChatTree return { tree, root: messages[0]?._id || '' } } -export function updateChatTree(tree: ChatTree, msg: AppSchema.ChatMessage) { +export function updateChatTreeNode(tree: ChatTree, msg: AppSchema.ChatMessage) { tree[msg._id] = { msg, children: new Set(), @@ -56,7 +56,19 @@ export function updateChatTree(tree: ChatTree, msg: AppSchema.ChatMessage) { parent.children.add(msg._id) } - return tree + return Object.assign({}, tree) +} + +export function removeChatTreeNodes(tree: ChatTree, ids: string[]) { + for (const id of ids) { + const parent = tree[id] + if (parent) { + parent.children.delete(id) + } + delete tree[id] + } + + return Object.assign({}, tree) } export function getChatDepths(tree: ChatTree) { diff --git a/common/presets/templates.ts b/common/presets/templates.ts index 668c4da46..80430df20 100644 --- a/common/presets/templates.ts +++ b/common/presets/templates.ts @@ -45,7 +45,7 @@ export const BUILTIN_FORMATS: { [key in ModelFormat]: FormatTags } = { }, ChatML: { openUser: '<|im_start|>user\n', - closeUser: '<|im_end>', + closeUser: '<|im_end|>', openBot: '<|im_start|>assistant\n', closeBot: '<|im_end|>', openSystem: '<|im_start|>system\n', diff --git a/common/prompt-order.ts b/common/prompt-order.ts index f7c715bcd..828cc2fa7 100644 --- a/common/prompt-order.ts +++ b/common/prompt-order.ts @@ -39,6 +39,7 @@ export const formatHolders: Record> = { {{/each}}`, post: `### Response:\n{{post}}`, system_prompt: `{{#if system_prompt}}### Instruction:\n{{system_prompt}}{{/if}}`, + ujb: `{{#if ujb}}({{ujb}}){{/if}}`, }, Vicuna: { preamble: neat`Below is an instruction that describes a task. Write a response that appropriately completes the request.\n @@ -49,6 +50,7 @@ export const formatHolders: Record> = { {{/each}}`, post: `ASSISTANT: {{post}}`, system_prompt: `{{#if system_prompt}}SYSTEM: {{system_prompt}}{{/if}}`, + ujb: `{{#if ujb}}({{ujb}}){{/if}}`, }, Mistral: { preamble: neat`Below is an instruction that describes a task. Write a response that appropriately completes the request.\n diff --git a/common/types/schema.ts b/common/types/schema.ts index e926218e1..eeb5e7ee5 100644 --- a/common/types/schema.ts +++ b/common/types/schema.ts @@ -137,7 +137,7 @@ export namespace AppSchema { level: number service: AIAdapter guidance: boolean - preset: GenSettings + preset: GenSettings & Pick } export interface AppConfig { diff --git a/common/util.ts b/common/util.ts index 521b44144..5fb707bcc 100644 --- a/common/util.ts +++ b/common/util.ts @@ -7,6 +7,11 @@ export function replace(id: string, list: T[], item: return list.map((li) => (li._id === id ? { ...li, ...item } : li)) } +export function exclude(list: T[], ids: string[]) { + const set = new Set(ids) + return list.filter((item) => !set.has(item._id)) +} + export function findOne(id: string, list: T[]): T | void { for (const item of list) { if (item._id === id) return item diff --git a/srv/api/chat/message.ts b/srv/api/chat/message.ts index ad47bf20c..b5bb64cca 100644 --- a/srv/api/chat/message.ts +++ b/srv/api/chat/message.ts @@ -326,6 +326,7 @@ export const generateMessageV2 = handle(async (req, res) => { } const responseText = body.kind === 'continue' ? `${body.continuing.msg} ${generated}` : generated + const parent = getNewMessageParent(body, userMsg) const actions: AppSchema.ChatAction[] = [] let treeLeafId = '' @@ -363,7 +364,7 @@ export const generateMessageV2 = handle(async (req, res) => { meta, retries, event: undefined, - parent: userMsg?._id, + parent, }) sendMany(members, { @@ -387,7 +388,7 @@ export const generateMessageV2 = handle(async (req, res) => { await store.msgs.editMessage(body.replacing._id, { msg: responseText, - parent: body.parent, + parent, actions, adapter, meta, @@ -419,7 +420,7 @@ export const generateMessageV2 = handle(async (req, res) => { meta, retries, event: undefined, - parent: body.parent, + parent, }) treeLeafId = requestId sendMany(members, { @@ -491,12 +492,14 @@ async function handleGuestGenerate(body: GenRequest, req: AppRequest, res: Respo characterId: body.impersonate?._id, ooc: body.kind === 'ooc', event: undefined, + parent: body.parent, }) } else if (body.kind.startsWith('send-event:')) { newMsg = newMessage(v4(), chatId, body.text!, { characterId: replyAs?._id, ooc: false, event: getScenarioEventType(body.kind), + parent: body.parent, }) } @@ -571,6 +574,7 @@ async function handleGuestGenerate(body: GenRequest, req: AppRequest, res: Respo const characterId = body.kind === 'self' ? undefined : body.replyAs?._id || body.char?._id const senderId = body.kind === 'self' ? 'anon' : undefined + const parent = getNewMessageParent(body, newMsg) if (body.kind === 'retry' && body.replacing) { retries = [body.replacing.msg].concat(retries).concat(body.replacing.retries || []) @@ -583,6 +587,7 @@ async function handleGuestGenerate(body: GenRequest, req: AppRequest, res: Respo meta, event: undefined, retries, + parent, }) switch (body.kind) { @@ -623,6 +628,7 @@ function newMessage( meta?: any event: undefined | AppSchema.ScenarioEventType retries?: string[] + parent?: string } ) { const userMsg: AppSchema.ChatMessage = { @@ -674,3 +680,25 @@ async function ensureBotMembership( update.characters = characters await store.chats.update(chat._id, update) } + +function getNewMessageParent(body: GenRequest, userMsg: AppSchema.ChatMessage | undefined): string { + switch (body.kind) { + case 'summary': + case 'chat-query': + return '' + + case 'retry': + case 'continue': + return body.parent || '' + + case 'request': + case 'ooc': + case 'self': + case 'send': + case 'send-event:character': + case 'send-event:hidden': + case 'send-event:ooc': + case 'send-event:world': + return userMsg?._id || '' + } +} diff --git a/srv/api/chat/remove.ts b/srv/api/chat/remove.ts index 3168c760b..88c01d784 100644 --- a/srv/api/chat/remove.ts +++ b/srv/api/chat/remove.ts @@ -5,7 +5,7 @@ import { sendMany } from '../ws' export const deleteMessages = handle(async ({ body, params, userId }) => { const chatId = params.id - assertValid({ ids: ['string'] }, body) + assertValid({ ids: ['string'], leafId: 'string?' }, body) const chat = await store.chats.getChatOnly(chatId) if (!chat) { @@ -17,6 +17,11 @@ export const deleteMessages = handle(async ({ body, params, userId }) => { } await store.msgs.deleteMessages(body.ids) + + if (body.leafId) { + await store.chats.update(chatId, { treeLeafId: body.leafId }) + } + sendMany(chat.memberIds.concat(chat.userId), { type: 'messages-deleted', ids: body.ids }) return { success: true } }) diff --git a/srv/app.ts b/srv/app.ts index 1091420bd..1caf99158 100644 --- a/srv/app.ts +++ b/srv/app.ts @@ -9,6 +9,7 @@ import { setupSockets } from './api/ws' import { config } from './config' import { createServer } from './server' import pipeline from './api/pipeline' +import { getDb } from './db/client' export function createApp() { const upload = multer({ @@ -41,6 +42,17 @@ export function createApp() { app.use('/v1', keyedRouter) app.use('/api', api) + if (config.db.host || config.db.uri) { + app.get('/healthcheck', (_, res) => { + try { + getDb() + res.status(200).json({ message: 'okay', status: true }) + } catch (ex) { + res.status(503).json({ message: 'Database not ready', status: false }) + } + }) + } + if (config.pipelineProxy) { app.use('/pipeline', pipeline) } diff --git a/srv/db/subscriptions.ts b/srv/db/subscriptions.ts index ac1f864d3..d6c396beb 100644 --- a/srv/db/subscriptions.ts +++ b/srv/db/subscriptions.ts @@ -86,6 +86,7 @@ export function getCachedSubscriptions(user?: AppSchema.User | null) { guidance: !!sub.guidanceCapable, preset: { name: sub.name, + maxContextLength: sub.maxContextLength, maxTokens: sub.maxTokens, frequencyPenalty: sub.frequencyPenalty, @@ -110,6 +111,15 @@ export function getCachedSubscriptions(user?: AppSchema.User | null) { encoderRepitionPenalty: sub.encoderRepitionPenalty, mirostatLR: sub.mirostatLR, mirostatTau: sub.mirostatTau, + allowGuestUsage: sub.allowGuestUsage, + dynatemp_exponent: sub.dynatemp_exponent, + dynatemp_range: sub.dynatemp_range, + modelFormat: sub.modelFormat, + penaltyAlpha: sub.penaltyAlpha, + smoothingCurve: sub.smoothingCurve, + smoothingFactor: sub.smoothingFactor, + skipSpecialTokens: sub.skipSpecialTokens, + tempLast: sub.tempLast, }, })) .sort((l, r) => (l.level === r.level ? l.name.localeCompare(r.name) : l.level - r.level)) diff --git a/web/pages/Chat/ChatDetail.tsx b/web/pages/Chat/ChatDetail.tsx index f0d4ee986..67ee111c0 100644 --- a/web/pages/Chat/ChatDetail.tsx +++ b/web/pages/Chat/ChatDetail.tsx @@ -428,7 +428,7 @@ const ChatDetail: Component = () => { -
+
diff --git a/web/pages/Chat/components/GraphModal.tsx b/web/pages/Chat/components/GraphModal.tsx index 76f1ea8e0..7c2150f32 100644 --- a/web/pages/Chat/components/GraphModal.tsx +++ b/web/pages/Chat/components/GraphModal.tsx @@ -37,8 +37,8 @@ export const ChatGraphModal: Component<{ footer={ <> diff --git a/web/pages/Settings/Agnaistic.tsx b/web/pages/Settings/Agnaistic.tsx index 35cbd56d2..0f608be35 100644 --- a/web/pages/Settings/Agnaistic.tsx +++ b/web/pages/Settings/Agnaistic.tsx @@ -20,6 +20,7 @@ export const AgnaisticSettings: Component<{ return config.subs .filter((sub) => sub.level <= level) + .filter((sub) => (level === -1 ? !!sub.preset.allowGuestUsage : true)) .map((sub) => ({ label: sub.name, value: sub._id, level: sub.level })) .sort((l, r) => r.level - l.level) }) diff --git a/web/shared/GenerationSettings.tsx b/web/shared/GenerationSettings.tsx index 7306a1fdd..47c82cf69 100644 --- a/web/shared/GenerationSettings.tsx +++ b/web/shared/GenerationSettings.tsx @@ -167,6 +167,7 @@ const GenerationSettings: Component void }> = (props) => pane={pane.showing()} setFormat={setFormat} tab={tabs[tab()]} + sub={sub()} /> void }> = (props) => format={format()} pane={pane.showing()} tab={tabs[tab()]} + sub={sub()} /> void format?: ThirdPartyFormat tab: string + sub?: AppSchema.SubscriptionOption } > = (props) => { const cfg = settingStore() + const maxCtx = createMemo(() => { + if (!props.sub?.preset.maxContextLength) return + + const max = Math.floor(props.sub?.preset.maxContextLength / 1000) + return `${max}K` + }) + const [replicate, setReplicate] = createStore({ model: props.inherit?.replicateModelName, type: props.inherit?.replicateModelType, @@ -261,27 +271,6 @@ const GeneralSettings: Component< return base }) - const CLAUDE_LABELS = { - ClaudeV2: 'Latest: Claude v2', - ClaudeV2_1: 'Claude v2.1', - ClaudeV2_0: 'Claude v2.0', - ClaudeV1_100k: 'Latest: Claude v1 100K', - ClaudeV1_3_100k: 'Claude v1.3 100K', - ClaudeV1: 'Latest: Claude v1', - ClaudeV1_3: 'Claude v1.3', - ClaudeV1_2: 'Claude v1.2', - ClaudeV1_0: 'Claude v1.0', - ClaudeInstantV1_100k: 'Latest: Claude Instant v1 100K', - ClaudeInstantV1_1_100k: 'Claude Instant v1.1 100K', - ClaudeInstantV1: 'Latest: Claude Instant v1', - ClaudeInstantV1_1: 'Claude Instant v1.1', - ClaudeInstantV1_0: 'Claude Instant v1.0', - ClaudeV3_Opus: 'Claude v3 Opus', - ClaudeV3_Sonnet: 'Claude v3 Sonnet', - ClaudeV3_Haiku: 'Claude v3 Haiku', - ClaudeV35_Sonnet: 'Claude v3.5 Sonnet', - } satisfies Record - const claudeModels: () => Option[] = createMemo(() => { const models = new Map(Object.entries(CLAUDE_MODELS) as [keyof typeof CLAUDE_MODELS, string][]) const labels = Object.entries(CLAUDE_LABELS) as [keyof typeof CLAUDE_MODELS, string][] @@ -520,6 +509,8 @@ const GeneralSettings: Component< disabled={props.disabled} format={props.format} onChange={(val) => setTokens(val)} + recommended={props.sub?.preset.maxTokens} + recommendLabel="Max" /> setContext(val)} + recommended={maxCtx()} + recommendLabel="Max" /> ): Option[] { const FORMATS = Object.keys(BUILTIN_FORMATS).map((label) => ({ label, value: label })) const PromptSettings: Component< - Props & { pane: boolean; format?: ThirdPartyFormat; tab: string } + Props & { + pane: boolean + format?: ThirdPartyFormat + tab: string + sub?: AppSchema.SubscriptionOption + } > = (props) => { const gaslights = presetStore((s) => ({ list: s.templates })) const [useAdvanced, setAdvanced] = createSignal( @@ -612,6 +610,7 @@ const PromptSettings: Component< items={FORMATS} value={props.inherit?.modelFormat || 'Alpaca'} hide={useAdvanced() === 'basic'} + recommend={props.sub?.preset.modelFormat} /> ids.has(msg._id) === false) - await localApi.saveMessages(chatId, next) + await localApi.saveMessages(chatId, exclude(msgs, msgIds)) + + const chats = await localApi.loadItem('chats') + const chat = chats.find((ch) => ch._id === chatId) + if (chat && leafId) { + const nextChat: AppSchema.Chat = { + ...chat, + treeLeafId: leafId, + updatedAt: new Date().toISOString(), + } + await localApi.saveChats(replace(chatId, chats, nextChat)) + } return localApi.result({ success: true }) } @@ -693,6 +702,7 @@ export async function getPromptEntities(): Promise { async function getGuestEntities() { const { active } = getStore('chat').getState() if (!active) return + const { msgs, messageHistory } = getStore('messages').getState() const chat = active.chat const char = active.char @@ -705,7 +715,6 @@ async function getGuestEntities() { const allScenarios = await loadItem('scenario') const profile = await loadItem('profile') - const messages = await localApi.getMessages(chat?._id) const user = await loadItem('config') const settings = await getGuestPreset(user, chat) const scenarios = allScenarios?.filter( @@ -722,7 +731,7 @@ async function getGuestEntities() { user, profile, book, - messages, + messages: messageHistory.concat(msgs), settings, members: [profile] as AppSchema.Profile[], chatBots: chatChars.list, diff --git a/web/store/data/storage.ts b/web/store/data/storage.ts index 4f722b059..ff6965b28 100644 --- a/web/store/data/storage.ts +++ b/web/store/data/storage.ts @@ -5,6 +5,7 @@ import { AppSchema } from '../../../common/types/schema' import { api } from '../api' import { toastStore } from '../toasts' import { storage } from '/web/shared/util' +import { replace } from '/common/util' type StorageKey = keyof typeof KEYS @@ -269,6 +270,15 @@ export async function saveChars(state: AppSchema.Character[]) { await saveItem('characters', state) } +export async function saveChat(chatId: string, update: Partial) { + const chats = await loadItem('chats') + const chat = chats.find((c) => c._id === chatId) + if (!chat) return + + const next = replace(chatId, chats, update) + await saveItem('chats', next) +} + export async function saveChats(state: AppSchema.Chat[]) { await saveItem('chats', state) } @@ -356,6 +366,7 @@ export function result(result: T) { export const localApi = { saveChars, + saveChat, saveChats, saveConfig, saveMessages, diff --git a/web/store/message.ts b/web/store/message.ts index c8391d31c..8e2ff6b76 100644 --- a/web/store/message.ts +++ b/web/store/message.ts @@ -16,8 +16,15 @@ import { VoiceSettings, VoiceWebSynthesisSettings } from '../../common/types/tex import { defaultCulture } from '../shared/CultureCodes' import { createSpeech, isNativeSpeechSupported, stopSpeech } from '../shared/Audio/speech' import { eventStore } from './event' -import { findOne, replace } from '/common/util' -import { ChatTree, resolveChatPath, sortAsc, toChatGraph, updateChatTree } from '/common/chat' +import { exclude, findOne, replace } from '/common/util' +import { + ChatTree, + removeChatTreeNodes, + resolveChatPath, + sortAsc, + toChatGraph, + updateChatTreeNode, +} from '/common/chat' import { embedApi } from './embeddings' const SOFT_PAGE_SIZE = 20 @@ -126,7 +133,18 @@ export const msgStore = createStore( }) => { data.messages.sort(sortAsc) const graph = toChatGraph(data.messages) - const leaf = data.leafId || data.messages.slice(-1)[0]?._id || '' + + let leaf = data.leafId || data.messages.slice(-1)[0]?._id || '' + + // If the leaf has been deleted then the path won't load + // So, if the leaf doesn't exist, use the most recent message + if (data.leafId) { + const node = graph.tree[data.leafId] + if (!node) { + leaf = data.messages.slice(-1)[0]?._id || '' + } + } + const fullPath = resolveChatPath(graph.tree, leaf) const recent = fullPath.splice(-SOFT_PAGE_SIZE) @@ -532,21 +550,27 @@ export const msgStore = createStore( msgStore.swapMessage(msgId, position, onSuccess) }, - async deleteMessages({ msgs, activeChatId }, fromId: string, deleteOne?: boolean) { + async deleteMessages({ msgs, activeChatId, graph }, fromId: string, deleteOne?: boolean) { const index = msgs.findIndex((m) => m._id === fromId) if (index === -1) { return toastStore.error(`Cannot delete message: Message not found`) } const deleteIds = deleteOne ? [fromId] : msgs.slice(index).map((m) => m._id) - const res = await msgsApi.deleteMessages(activeChatId, deleteIds) + const removed = new Set(deleteIds) + const nextMsgs = msgs.filter((msg) => !removed.has(msg._id)) + + const leafId = nextMsgs.slice(-1)[0]?._id || '' + const res = await msgsApi.deleteMessages(activeChatId, deleteIds, leafId) if (res.error) { return toastStore.error(`Failed to delete messages: ${res.error}`) } - const removed = new Set(deleteIds) - return { msgs: msgs.filter((msg) => !removed.has(msg._id)) } + return { + msgs: nextMsgs, + graph: { tree: removeChatTreeNodes(graph.tree, deleteIds), root: graph.root }, + } }, stopSpeech() { stopSpeech() @@ -857,7 +881,7 @@ subscribe( actions: [{ emote: 'string', action: 'string' }, '?'], } as const, async (body) => { - const { msgs, activeChatId, messageHistory, graph } = msgStore.getState() + const { msgs, activeChatId, graph } = msgStore.getState() if (activeChatId !== body.chatId) return const msg = body.msg as AppSchema.ChatMessage @@ -878,7 +902,7 @@ subscribe( }, textBeforeGenMore: undefined, graph: { - tree: updateChatTree(graph.tree, msg), + tree: updateChatTreeNode(graph.tree, msg), root: graph.root, }, }) @@ -896,7 +920,9 @@ subscribe( } if (!isLoggedIn()) { - await localApi.saveMessages(body.chatId, messageHistory.concat(nextMsgs)) + const allMsgs = await localApi.getMessages(body.chatId) + await localApi.saveChat(body.chatId, { treeLeafId: msg._id }) + await localApi.saveMessages(body.chatId, allMsgs.concat(msg)) } if (msg.userId && msg.userId != user?._id) { @@ -1008,23 +1034,40 @@ subscribe('message-warning', { warning: 'string' }, (body) => { subscribe('messages-deleted', { ids: ['string'] }, (body) => { const ids = new Set(body.ids) - const { msgs } = msgStore.getState() - msgStore.setState({ msgs: msgs.filter((msg) => !ids.has(msg._id)) }) + const { msgs, graph } = msgStore.getState() + + msgStore.setState({ + msgs: msgs.filter((msg) => !ids.has(msg._id)), + graph: { + tree: removeChatTreeNodes(graph.tree, body.ids), + root: graph.root, + }, + }) }) const updateMsgSub = (body: any) => { - const { msgs } = msgStore.getState() + const { msgs, graph } = msgStore.getState() const prev = findOne(body.messageId, msgs) - const nextMsgs = replace(body.messageId, msgs, { - imagePrompt: body.imagePrompt || prev?.imagePrompt, + + if (!prev) return + + const next: ChatMessageExt = { + ...prev, msg: body.message || prev?.msg, retries: body.retries || prev?.retries, actions: body.actions || prev?.actions, voiceUrl: undefined, extras: body.extras || prev?.extras, - }) + } + const nextMsgs = replace(body.messageId, msgs, next) - msgStore.setState({ msgs: nextMsgs }) + msgStore.setState({ + msgs: nextMsgs, + graph: { + tree: updateChatTreeNode(graph.tree, next), + root: graph.root, + }, + }) } subscribe( @@ -1098,23 +1141,27 @@ subscribe( 'guest-message-created', { msg: 'any', chatId: 'string', continue: 'boolean?', requestId: 'string?' }, async (body) => { - const { messageHistory, msgs, activeChatId, retrying } = msgStore.getState() + const { activeChatId, retrying, graph, msgs } = msgStore.getState() if (activeChatId !== body.chatId) return if (retrying) { body.msg._id = retrying._id } + const allMsgs = await localApi.getMessages(body.chatId) + const msg = body.msg as AppSchema.ChatMessage - const next = msgs.filter((m) => m._id !== retrying?._id).concat(msg) + const next = allMsgs.filter((m) => m._id !== retrying?._id).concat(msg) const speech = getMessageSpeechInfo(msg, userStore.getState().user) const chats = await localApi.loadItem('chats') - await localApi.saveChats(replace(body.chatId, chats, { updatedAt: new Date().toISOString() })) - await localApi.saveMessages(body.chatId, messageHistory.concat(next)) + await localApi.saveChats( + replace(body.chatId, chats, { updatedAt: new Date().toISOString(), treeLeafId: body.msg._id }) + ) + await localApi.saveMessages(body.chatId, next) msgStore.setState({ - msgs: next, + msgs: exclude(msgs, body.msg._id).concat(msg), retrying: undefined, partial: undefined, waiting: undefined, @@ -1127,6 +1174,10 @@ subscribe( messageId: body.msg._id, }, textBeforeGenMore: undefined, + graph: { + tree: updateChatTreeNode(graph.tree, msg), + root: graph.root, + }, }) if (speech) msgStore.textToSpeech(msg._id, msg.msg, speech.voice, speech?.culture)