UNPKG

@convo-lang/convo-lang

Version:
639 lines 24 kB
import { aryDuplicateRemoveItem, findSceneAction, shortUuid, zodTypeToJsonScheme } from "@iyio/common"; import { BehaviorSubject, Subject } from "rxjs"; import { z } from "zod"; import { Conversation } from "./Conversation.js"; import { LocalStorageConvoDataStore } from "./LocalStorageConvoDataStore.js"; import { getConvoPromptMediaUrl } from "./convo-lang-ui-lib.js"; import { convoVars, removeDanglingConvoUserMessage } from "./convo-lib.js"; export class ConversationUiCtrl { get lastCompletionSubject() { return this._lastCompletion; } get lastCompletion() { return this._lastCompletion.value; } get onAppend() { return this._onAppend; } get currentTaskSubject() { return this._currentTask; } get currentTask() { return this._currentTask.value; } get templateSubject() { return this._template; } get template() { return this._template.value; } set template(value) { if (value == this._template.value) { return; } this._template.next(value); } ; get removeDanglingUserMessagesSubject() { return this._removeDanglingUserMessages; } get removeDanglingUserMessages() { return this._removeDanglingUserMessages.value; } set removeDanglingUserMessages(value) { if (value == this._removeDanglingUserMessages.value) { return; } this._removeDanglingUserMessages.next(value); } get convoSubject() { return this._convo; } get convo() { return this._convo.value; } get showSourceSubject() { return this._showSource; } /** * If true the source convo-lang syntax should be displayed to the user */ get showSource() { return this._showSource.value; } set showSource(value) { if (value == this._showSource.value) { return; } this._showSource.next(value); } get editorModeSubject() { return this._editorMode; } get editorMode() { return this._editorMode.value; } set editorMode(value) { if (value == this._editorMode.value) { return; } this._editorMode.next(value); } get showSystemMessagesSubject() { return this._showSystemMessages; } /** * If true the system messages should be displayed to the user */ get showSystemMessages() { return this._showSystemMessages.value; } set showSystemMessages(value) { if (value == this._showSystemMessages.value) { return; } this._showSystemMessages.next(value); } get showResultsSubject() { return this._showResults; } get showResults() { return this._showResults.value; } set showResults(value) { if (value == this._showResults.value) { return; } this._showResults.next(value); } get enabledInitMessageSubject() { return this._enabledInitMessage; } get enabledInitMessage() { return this._enabledInitMessage.value; } set enabledInitMessage(value) { if (value == this._enabledInitMessage.value) { return; } if (value && this.convo?.initMessageReady()) { this.convo.completeAsync(); } this._enabledInitMessage.next(value); } get showFunctionsSubject() { return this._showFunctions; } /** * If true function calls should be displayed to the user */ get showFunctions() { return this._showFunctions.value; } set showFunctions(value) { if (value == this._showFunctions.value) { return; } this._showFunctions.next(value); } get enabledSlashCommandsSubject() { return this._enabledSlashCommands; } /** * If messages appended to the conversation using the appendUiMessage will be checked for messages * starting with a forward slash and be interpreted as a command. */ get enabledSlashCommands() { return this._enabledSlashCommands.value; } set enabledSlashCommands(value) { if (value == this._enabledSlashCommands.value) { return; } this._enabledSlashCommands.next(value); } get themeSubject() { return this._theme; } get theme() { return this._theme.value; } set theme(value) { if (value == this._theme.value) { return; } this._theme.next(value); } get mediaQueueSubject() { return this._mediaQueue; } get mediaQueue() { return this._mediaQueue.value; } get collapsedSubject() { return this._collapsed; } /** * Often used to indicate if the conversation is display in a collapsed state */ get collapsed() { return this._collapsed.value; } set collapsed(value) { if (value == this._collapsed.value) { return; } this._collapsed.next(value); } get onClear() { return this._onClear; } get onAppendUiMessage() { return this._onAppendUiMessage; } get expandOnUiMessageSubject() { return this._expandOnUiMessage; } get expandOnUiMessage() { return this._expandOnUiMessage.value; } set expandOnUiMessage(value) { if (value == this._expandOnUiMessage.value) { return; } this._expandOnUiMessage.next(value); } get beforeCreateExeCtxSubject() { return this._beforeCreateExeCtx; } get beforeCreateExeCtx() { return this._beforeCreateExeCtx.value; } set beforeCreateExeCtx(value) { if (value == this._beforeCreateExeCtx.value) { return; } if (this.convo && value !== undefined) { this.convo.beforeCreateExeCtx = value ?? undefined; } this._beforeCreateExeCtx.next(value); } get defaultVarsSubject() { return this._defaultVars; } get defaultVars() { return this._defaultVars.value; } set defaultVars(value) { if (value == this._defaultVars.value) { return; } if (this.convo) { const current = this._defaultVars.value; for (const e in current) { if (this.convo.defaultVars[e] === current[e]) { delete this.convo.defaultVars[e]; } } for (const e in value) { this.convo.defaultVars[e] = value[e]; } } this._defaultVars.next(value); } get externFunctionsSubject() { return this._externFunctions; } get externFunctions() { return this._externFunctions.value; } set externFunctions(value) { if (value == this._externFunctions.value) { return; } if (this.convo) { const current = this._externFunctions.value; for (const e in current) { if (this.convo.externFunctions[e] === current[e]) { delete this.convo.externFunctions[e]; } } for (const e in value) { const fn = value[e]; if (fn) { this.convo.implementExternFunction(e, fn); } } } this._externFunctions.next(value); } get getStartOfConversationSubject() { return this._getStartOfConversation; } get getStartOfConversation() { return this._getStartOfConversation.value; } set getStartOfConversation(value) { if (value == this._getStartOfConversation.value) { return; } if (this.convo && value !== undefined) { this.convo.getStartOfConversation = value?.cb; } this._getStartOfConversation.next(value); } constructor({ id, autoLoad, convo, initConvo, convoOptions, template, autoSave = false, removeDanglingUserMessages = false, store = 'localStorage', enableSlashCommand = false, sceneCtrl, defaultVars, externFunctions, imagePathConverter, imageRenderer, } = {}) { this._lastCompletion = new BehaviorSubject(null); this._onAppend = new Subject(); this.tasks = []; this._currentTask = new BehaviorSubject(null); this._convo = new BehaviorSubject(null); this._showSource = new BehaviorSubject(false); this._editorMode = new BehaviorSubject('code'); this._showSystemMessages = new BehaviorSubject(false); /** * If true function call results will be displayed to the user */ this._showResults = new BehaviorSubject(false); this._enabledInitMessage = new BehaviorSubject(false); this._showFunctions = new BehaviorSubject(false); this._theme = new BehaviorSubject({}); this._mediaQueue = new BehaviorSubject([]); this._collapsed = new BehaviorSubject(true); this._onClear = new Subject(); this._onAppendUiMessage = new Subject(); this._expandOnUiMessage = new BehaviorSubject(false); this._beforeCreateExeCtx = new BehaviorSubject(undefined); this._getStartOfConversation = new BehaviorSubject(undefined); this.componentRenderers = {}; this._isDisposed = false; this.convoTaskCount = 0; this.convoCleanup = null; this.isAutoSaving = false; this.autoSaveRequested = false; this.messageRenderers = []; this.id = id ?? shortUuid(); this.sceneCtrl = sceneCtrl; this.imagePathConverter = imagePathConverter; this.imageRenderer = imageRenderer; this._defaultVars = new BehaviorSubject(defaultVars ? { ...defaultVars } : {}); this._externFunctions = new BehaviorSubject(externFunctions ? { ...externFunctions } : {}); this._removeDanglingUserMessages = new BehaviorSubject(removeDanglingUserMessages); this._enabledSlashCommands = new BehaviorSubject(enableSlashCommand); this.convoOptions = convoOptions; this.initConvoCallback = initConvo; this._template = new BehaviorSubject(template); this.autoSave = autoSave; this.store = store === 'localStorage' ? new LocalStorageConvoDataStore() : store; if (id !== undefined && autoLoad) { if (convo) { this.setConvo(convo); } this.loadAsync(); } else { this.setConvo(convo ?? this.createConvo(true)); } } get isDisposed() { return this._isDisposed; } dispose() { if (this._isDisposed) { return; } this._isDisposed = true; this._currentTask.next('disposed'); const cleanup = this.convoCleanup; this.convoCleanup = null; cleanup?.(); } initConvo(convo, options) { if (this.initConvoCallback) { this.initConvoCallback(convo); } for (const e in this.defaultVars) { convo.defaultVars[e] = this.defaultVars[e]; } for (const e in this.externFunctions) { const f = this.externFunctions[e]; if (f) { convo.implementExternFunction(e, f); } } if (this.getStartOfConversation !== undefined) { convo.getStartOfConversation = this.getStartOfConversation?.cb; } if (this.beforeCreateExeCtx !== undefined) { convo.beforeCreateExeCtx = this.beforeCreateExeCtx ?? undefined; } if (this.sceneCtrl) { convo.defaultVars[convoVars.__sceneCtrl] = this.sceneCtrl; convo.define({ fns: { executeSceneAction: { description: 'Used to execute actions defined in the scenes', paramsType: z.object({ id: z.string().describe('Id of the action to execute'), data: z.any().optional().describe('Data to pass to the action. If the action defines a dataScheme the passed ' + 'data should conform to the scheme.') }), scopeCallback: (scope, ctx) => { const scene = ctx.getVar(convoVars.__lastDescribedScene); if (!scene) { return 'Unable to executed action. Last described scene not found'; } const id = scope.paramValues?.[0]; if (typeof id !== 'string') { return 'Invalid scene action id'; } const action = findSceneAction(id, scene); if (!action) { return `No action found by id ${id}`; } let data = scope.paramValues?.[1]; if (action.dataScheme) { const parsed = action.dataScheme.safeParse(data); if (!parsed.success) { return `Invalid action data received. The action's data should conform to the JSON scheme of <JSON_SCHEME>${zodTypeToJsonScheme(action.dataScheme)}</JSON_SCHEME>`; } else { data = parsed.data; } } return action.callback?.(data) ?? 'action completed'; }, } } }); } if (options.appendTemplate && this.template) { try { convo.append(this.template); } catch (ex) { console.error('Invalid convo template supplied to ConversationUiCtrl', ex); } } } createConvo(appendTemplate) { const convo = new Conversation(this.convoOptions); this.initConvo(convo, { appendTemplate }); return convo; } async loadAsync() { if (this.isDisposed || this.currentTask || !this.store?.loadConvo) { return false; } this.pushTask('loading'); try { const str = await this.store?.loadConvo?.(this.id); if (this.isDisposed) { return false; } const convo = this.createConvo(str ? false : true); if (str) { convo.append(str); } this.setConvo(convo); } finally { this.popTask('loading'); } return true; } clear() { if (this.isDisposed || this.currentTask) { return false; } this.setConvo(this.createConvo(true)); if (this.autoSave) { this.queueAutoSave(); } this._onClear.next(); return true; } pushTask(task) { this.tasks.push(task); if (this._currentTask.value !== task) { this._currentTask.next(task); } } popTask(task) { if (this.isDisposed) { return; } const i = this.tasks.lastIndexOf(task); if (i === -1) { console.error(`ConversationUiCtrl.popTask out of sync. (${task}) not in current list`); return; } this.tasks.splice(i, 1); const next = this.tasks[this.tasks.length - 1] ?? null; if (this._currentTask.value !== next) { this._currentTask.next(next); } } setConvo(convo) { const prevCleanup = this.convoCleanup; this.convoCleanup = null; prevCleanup?.(); while (this.tasks.includes('completing')) { this.popTask('completing'); } const sub = convo.activeTaskCountSubject.subscribe(n => { if (n === 1) { if (!this.tasks.includes('completing')) { this.pushTask('completing'); } } else if (n === 0) { if (this.tasks.includes('completing')) { this.popTask('completing'); } } }); const sub2 = convo.onAppend.subscribe(v => { this._onAppend.next(v); }); this.convoCleanup = () => { sub.unsubscribe(); sub2.unsubscribe(); }; this._convo.next(convo); if (this.enabledInitMessage && convo.initMessageReady()) { convo.completeAsync(); } } replace(convo) { if (this.isDisposed || this.currentTask) { return false; } if (this.removeDanglingUserMessages) { convo = removeDanglingConvoUserMessage(convo); } const c = this.createConvo(false); try { c.append(convo); } catch { return false; } this.setConvo(c); if (this.autoSave) { this.queueAutoSave(); } return true; } async replaceAndCompleteAsync(convo) { if (!this.replace(convo)) { return false; } if (this.currentTask) { return false; } await this.convo?.completeAsync(); this._lastCompletion.next(this.convo?.convo ?? null); if (this.autoSave) { this.queueAutoSave(); } return true; } isSlashCommand(message) { return cmdReg.test(message); } appendDefineVars(vars) { return this.convo?.appendDefineVars(vars); } appendDefineVar(name, value) { return this.convo?.appendDefineVar(name, value); } async appendUiMessageAsync(message) { if (this.isDisposed) { return false; } if (cmdReg.test(message)) { if (!this._enabledSlashCommands.value) { return false; } message = message.trim(); switch (message) { case '/source': this.showSource = this.editorMode === 'code' ? !this.showSource : true; this.editorMode = 'code'; break; case '/imports ': this.showSource = this.editorMode === 'imports' ? !this.showSource : true; this.editorMode = 'imports'; break; case '/modules': this.showSource = this.editorMode === 'modules' ? !this.showSource : true; this.editorMode = 'modules'; break; case '/ui': this.showSource = false; break; case '/vars': this.showSource = this.editorMode === 'vars' ? !this.showSource : true; this.editorMode = 'vars'; break; case '/flat': this.showSource = this.editorMode === 'flat' ? !this.showSource : true; this.editorMode = 'flat'; break; case '/text': this.showSource = this.editorMode === 'text' ? !this.showSource : true; this.editorMode = 'text'; break; case '/models': this.showSource = this.editorMode === 'models' ? !this.showSource : true; this.editorMode = 'models'; break; case '/tree': this.showSource = this.editorMode === 'tree' ? !this.showSource : true; this.editorMode = 'tree'; break; case '/system': this.showSystemMessages = !this.showSystemMessages; break; case '/results': this.showResults = !this.showResults; break; case '/function': this.showFunctions = !this.showFunctions; break; case '/convert': this.showSource = this.editorMode === 'model' ? !this.showSource : true; this.editorMode = 'model'; break; case '/clear': this.clear(); break; case '/help': this.printHelp(); break; } this.triggerAppendUiMessageEvt(message, true); return 'command'; } const convo = this.convo; if (!convo || convo.isCompleting || this.currentTask) { return false; } if (this.mediaQueue.length) { message += '\n\n' + this.mediaQueue.map(i => { const url = getConvoPromptMediaUrl(i, 'prompt'); if (!url) { return ''; } return `![](${encodeURI(url)})`; }).join('\n'); this._mediaQueue.next([]); } if (message.trim()) { convo.appendUserMessage(message); this.triggerAppendUiMessageEvt(message); } await convo.completeAsync(); this._lastCompletion.next(convo.convo ?? null); if (this.autoSave) { this.queueAutoSave(); } return true; } triggerAppendUiMessageEvt(message, isCommand = false) { if (this._onAppendUiMessage.observed) { this._onAppendUiMessage.next({ isCommand, message, }); } if (this.expandOnUiMessage) { this.collapsed = false; } } printHelp() { this.convo?.appendAssistantMessage(/*convo*/ ` /source - Display convo script source /imports - Display convo script source and imported modules /modules - Display convo script source and registered modules /models - Display all registered LLM models /ui - Display convo chat conversation ui /flat - Display the convo as flat messages /text - Display as convo script with all text content evaluated /vars - Display all defined user variables /tree - Displays the syntax tree /system - Display system messages /function - Display function messages /results - Display function call results /convert - Display messages in the format of the current model /clear - Clears all messages /help - Prints this help message `); } queueMedia(media) { this._mediaQueue.next([...this._mediaQueue.value, (typeof media === 'string') ? { url: media } : media]); } dequeueMedia(media) { if (typeof media === 'string') { const match = this._mediaQueue.value.find(m => m.url === media); if (!match) { return false; } media = match; } if (!this._mediaQueue.value.includes(media)) { return false; } this._mediaQueue.next(aryDuplicateRemoveItem(this._mediaQueue.value, media)); return true; } queueAutoSave() { if (this.isDisposed || !this.store) { return; } if (this.isAutoSaving) { this.autoSaveRequested = true; return; } this._autoSaveAsync(); } async _autoSaveAsync() { if (this.isAutoSaving || !this.store) { return; } this.isAutoSaving = true; do { try { await this.store?.saveConvo?.(this.id, this.convo?.convo ?? ''); } catch (ex) { console.error('ConversationUiCtrl auto save failed', ex); } } while (this.autoSaveRequested && !this.isDisposed); } renderMessage(message, index) { for (const r of this.messageRenderers) { const v = r(message, index, this); if (v !== undefined && v !== null) { return v; } } return undefined; } } const cmdReg = /^\s*\//; //# sourceMappingURL=ConversationUiCtrl.js.map