@convo-lang/convo-lang
Version:
The language of AI
309 lines • 12.7 kB
JavaScript
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