UNPKG

@convo-lang/convo-lang

Version:
309 lines 12.7 kB
import { getDirectoryName, getFileNameNoExt, getTimestampString, joinPaths, normalizePath } from "@iyio/common"; import { BehaviorSubject } from "rxjs"; import { convoRoles, convoTags, escapeConvo, parseBaseConvoImport } from "../convo-lib.js"; import { parseConvoCode } from "../convo-parser.js"; import { convoMessageSourcePathKey } from "../convo-types.js"; export class ConvoWorker { get errorSubject() { return this._error; } get error() { return this._error.value; } constructor({ ctx, path, }) { this._error = new BehaviorSubject(null); this._isDisposed = false; this._loaded = new BehaviorSubject(false); this._source = new BehaviorSubject(null); this._parsed = new BehaviorSubject(false); this._messages = new BehaviorSubject(null); this._inputs = new BehaviorSubject(null); this._outputs = new BehaviorSubject(null); this._autoManageMemory = new BehaviorSubject(null); this._runReady = new BehaviorSubject(null); this._ioChecked = new BehaviorSubject(false); this._existingInputs = new BehaviorSubject(null); this._existingOutputs = new BehaviorSubject(null); this._ioReady = new BehaviorSubject(false); this._memoryLoaded = new BehaviorSubject(false); this._memory = new BehaviorSubject(null); this._threadCompleted = new BehaviorSubject(false); this._threadName = new BehaviorSubject(null); this._threadPath = new BehaviorSubject(null); this._thread = new BehaviorSubject(null); this.name = getFileNameNoExt(path); if (!path.startsWith('/')) { path = normalizePath(joinPaths(ctx.basePath, path)); } this.ctx = ctx; this.path = path; this.dir = path.includes('.') ? getDirectoryName(path) : '.'; this.memoryPath = `${path}.md`; } get isDisposed() { return this._isDisposed; } dispose() { if (this._isDisposed) { return; } this._isDisposed = true; } log(...args) { this.ctx.log(`[${this.path}]`, ...args); } logError(...args) { const msg = args.join(' '); this._error.next(msg); this.ctx.log(`[${this.path}]`, ...args); } runAsync() { return this.runPromise ?? (this.runPromise = this._runAsync()); } async _runAsync() { const loaded = await this.loadAsync(); if (!loaded || this.isDisposed) { console.log('hio 👋 👋 👋 WORKER', this); return; } const parsed = this.parse(); if (!parsed || this.isDisposed) { console.log('hio 👋 👋 👋 WORKER', this); return; } const ioChecked = await this.checkIoAsync(); if (!ioChecked || !this.ioReady || this.isDisposed) { console.log('hio 👋 👋 👋 WORKER', this); return; } const memoryLoaded = await this.loadMemoryAsync(); if (!memoryLoaded || this.isDisposed) { console.log('hio 👋 👋 👋 WORKER', this); return; } const threadCompleted = await this.runThreadAsync(); if (!threadCompleted || this.isDisposed) { console.log('hio 👋 👋 👋 WORKER', this); return; } console.log('hio 👋 👋 👋 WORKER', this); } get loadedSubject() { return this._loaded; } get loaded() { return this._loaded.value; } get sourceSubject() { return this._source; } get source() { return this._source.value; } async loadAsync() { try { const source = await this.ctx.readAsync(this.path); this._source.next(source); this._loaded.next(true); return true; } catch (ex) { this.logError(`Failed to load - ${this.path}`, ex); return false; } } get parsedSubject() { return this._parsed; } get parsed() { return this._parsed.value; } get messagesSubject() { return this._messages; } get messages() { return this._messages.value; } get inputsSubject() { return this._inputs; } get inputs() { return this._inputs.value; } get outputsSubject() { return this._outputs; } get outputs() { return this._outputs.value; } get autoManageMemorySubject() { return this._autoManageMemory; } get autoManageMemory() { return this._autoManageMemory.value; } get runReadySubject() { return this._runReady; } get runReady() { return this._runReady.value; } parse() { try { if (this.source === null) { throw new Error('Source not loaded'); } const r = parseConvoCode(this.source); if (r.error) { throw r.error; } const messages = r.result ?? []; const inputs = []; const outputs = []; let runReady = false; let autoManageMemory = false; for (const msg of messages) { msg[convoMessageSourcePathKey] = this.path; if (msg.role === convoRoles.run) { runReady = true; } if (!msg.tags) { continue; } for (const tag of msg.tags) { if (tag.name === convoTags.autoMemory) { autoManageMemory = true; } if (tag.name === convoTags.import || tag.name === convoTags.output) { const { name } = parseBaseConvoImport(tag.value ?? ''); if (name.startsWith('./') || name.startsWith('../')) { (tag.name === convoTags.import ? inputs : outputs).push(normalizePath(joinPaths(this.dir, name))); } } } } this._messages.next(messages); this._inputs.next(inputs); this._outputs.next(outputs); this._runReady.next(runReady); this._autoManageMemory.next(autoManageMemory); this._parsed.next(true); return true; } catch (ex) { this.logError('Parse failed', ex); return false; } } get ioCheckedSubject() { return this._ioChecked; } get ioChecked() { return this._ioChecked.value; } get existingInputsSubject() { return this._existingInputs; } get existingInputs() { return this._existingInputs.value; } get existingOutputsSubject() { return this._existingOutputs; } get existingOutputs() { return this._existingOutputs.value; } get ioReadySubject() { return this._ioReady; } get ioReady() { return this._ioReady.value; } async checkIoAsync() { try { if (!this.inputs) { throw new Error('Inputs not set'); } if (!this.outputs) { throw new Error('Outputs not set'); } const existingInputs = []; const existingOutputs = []; await Promise.all([ ...this.inputs.map(async (p) => { const item = await this.ctx.vfs.getItemAsync(p); if (item) { existingInputs.push(p); } }), ...this.outputs.map(async (p) => { const item = await this.ctx.vfs.getItemAsync(p); if (item) { existingOutputs.push(p); } }), ]); this._ioReady.next(this.inputs.length === existingInputs.length && this.outputs.length > existingOutputs.length); this._existingInputs.next(existingInputs); this._existingOutputs.next(existingOutputs); this._ioChecked.next(true); return true; } catch (ex) { this.logError('IO check failed', ex); return false; } } get memoryLoadedSubject() { return this._memoryLoaded; } get memoryLoaded() { return this._memoryLoaded.value; } get memorySubject() { return this._memory; } get memory() { return this._memory.value; } async loadMemoryAsync() { try { let memory = ''; if (await this.ctx.existsAsync(this.memoryPath)) { memory = await this.ctx.readAsync(this.memoryPath); } this._memory.next(memory); this._memoryLoaded.next(true); return true; } catch (ex) { this.logError('Load memory failed', ex); return false; } } get threadCompletedSubject() { return this._threadCompleted; } get threadCompleted() { return this._threadCompleted.value; } get threadNameSubject() { return this._threadName; } get threadName() { return this._threadName.value; } get threadPathSubject() { return this._threadPath; } get threadPath() { return this._threadPath.value; } get threadSubject() { return this._thread; } get thread() { return this._thread.value; } async runThreadAsync() { try { const messages = this.messages; if (!messages) { throw new Error('messages not set'); } if (!this.source) { throw new Error('Source not set'); } const threadName = getTimestampString(); const threadPath = `${this.path}.thread.${threadName}.convo`; const conversation = this.ctx.createConversationFor(this); this._threadName.next(threadName); this._threadPath.next(threadPath); this._thread.next(conversation); let sourceI = 0; const flushThreadLogAsync = async () => { if (this.ctx.disableThreadLogging) { return; } const source = conversation.convo; if (sourceI === source.length) { return; } await this.ctx.appendAsync(threadPath, source.substring(sourceI)); sourceI = source.length; }; messageLoop: for (const msg of messages) { conversation.appendMessageObject(msg, { appendCode: true }); if (msg.role === convoRoles.user) { if (msg.tags) { for (const tag of msg.tags) { if (tag.name === convoTags.output && tag.value && await this.ctx.vfs.getItemAsync(joinPaths(this.dir, tag.value))) { const content = await this.ctx.readAsync(joinPaths(this.dir, tag.value)); if (this.isDisposed) { return false; } conversation.append(`> ${convoRoles.assistant}\n${escapeConvo(content)}`); continue messageLoop; } } } await flushThreadLogAsync(); const response = await conversation.completeAsync(); await flushThreadLogAsync(); if (this.isDisposed) { return false; } if (msg.tags) { for (const tag of msg.tags) { if (tag.name === convoTags.output && tag.value) { await this.ctx.writeResponse(joinPaths(this.dir, tag.value), response); if (this.isDisposed) { return false; } } if (tag.name === convoTags.saveMemory) { await this.ctx.writeResponse(this.memoryPath, response); if (this.isDisposed) { return false; } } } } } } await flushThreadLogAsync(); this._threadCompleted.next(true); return true; } catch (ex) { this.logError('thread failed', ex); return false; } } } //# sourceMappingURL=ConvoWorker.js.map