UNPKG

@iyio/convo-lang

Version:

A conversational language.

636 lines 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConvoExecutionContext = exports.executeConvoFunction = void 0; const common_1 = require("@iyio/common"); const zod_1 = require("zod"); const ConvoError_1 = require("./ConvoError"); const convo_default_vars_1 = require("./convo-default-vars"); const convo_lib_1 = require("./convo-lib"); const convo_types_1 = require("./convo-types"); const convo_zod_1 = require("./convo-zod"); const argsCacheKey = Symbol('argsCacheKey'); const returnCacheKey = Symbol('returnCacheKey'); const executeConvoFunction = (fn, args = {}) => { const exe = new ConvoExecutionContext(); const r = exe.executeFunction(fn, args); return r.valuePromise ?? r.value; }; exports.executeConvoFunction = executeConvoFunction; const createDefaultScope = (vars) => { return { _d: true, vars, i: 0, s: { s: 0, e: 0 }, }; }; const copyDefaultScope = (scope) => { if (scope._d) { scope = { ...scope }; delete scope._d; } return scope; }; class ConvoExecutionContext { constructor(convo) { this.nextSuspendId = 1; this.suspendedScopes = {}; this.sharedSetters = []; this.print = convo_lib_1.defaultConvoPrintFunction; this.convo = { ...convo, exe: this, convoPipeSink: convo?.convoPipeSink ?? ((value) => { this.print('CONVO_PIPE <<', value); }) }; this.sharedVars = { ...convo_default_vars_1.defaultConvoVars, [convo_lib_1.convoGlobalRef]: this.convo }; } getUserSharedVars() { const vars = { ...this.sharedVars }; delete vars['convo']; for (const e in convo_default_vars_1.defaultConvoVars) { delete vars[e]; } return vars; } loadFunctions(messages, externFunctions) { for (const msg of messages) { if (msg.fn && !msg.fn.call && !msg.fn.topLevel) { this.setVar(true, (0, convo_lib_1.createConvoScopeFunction)({ usesLabels: true, catchReturn: true, sourceFn: msg.fn }, (scope, ctx) => { if (msg.fn?.body) { const r = this.executeFunction(msg.fn, (0, convo_lib_1.convoLabeledScopeParamsToObj)(scope)); return r.valuePromise ?? r.value; } else { const externFn = externFunctions?.[msg.fn?.name ?? '']; if (!externFn) { (0, convo_lib_1.setConvoScopeError)(scope, `No extern function provided for ${msg.fn?.name}`); return; } return externFn(scope, ctx); } }), msg.fn.name); } } if (externFunctions) { for (const e in externFunctions) { const fn = externFunctions[e]; if (!fn || this.sharedVars[e] !== undefined) { continue; } this.setVar(true, (0, convo_lib_1.createConvoScopeFunction)(fn), e); } } } clearSharedSetters() { this.sharedSetters.splice(0, this.sharedSetters.length); } executeStatement(statement) { const vars = {}; const scope = { i: 0, vars, s: statement, }; return this.execute(scope, vars); } executeFunction(fn, args = {}) { if (fn.call) { throw new ConvoError_1.ConvoError('proxy-call-not-supported', { fn }, 'executeFunction does not support proxy calls. Use executeFunctionAsync instead'); } const scheme = this.getConvoFunctionArgsScheme(fn); const parsed = scheme.safeParse(args); if (parsed.success === false) { throw new ConvoError_1.ConvoError('invalid-args', { fn }, `Invalid args passed to convo function. fn = ${fn.name}, message = ${parsed.error.message}`); } args = parsed.data; const vars = { [convo_lib_1.convoArgsName]: args }; let scope; if (fn.body) { scope = { i: 0, vars, s: { fn: convo_lib_1.convoBodyFnName, params: fn.body, s: 0, e: 0, }, }; for (const e in args) { this.setVar(false, args[e], e, undefined, scope); } if (scheme.shape) { for (const e in scheme.shape) { if (vars[e] === undefined) { this.setVar(false, undefined, e, undefined, scope); } } } } else { if (typeof this.sharedVars[fn.name] !== 'function') { throw new ConvoError_1.ConvoError('function-not-defined', { fn }, `No function defined by name ${fn.name}`); } const params = []; for (const e in args) { params.push({ s: 0, e: 0, label: e, value: args[e] }); } scope = { i: 0, vars, s: { fn: fn.name, params, s: 0, e: 0, }, }; } return this.execute(scope, vars, this.getConvoFunctionReturnScheme(fn)); } async executeFunctionAsync(fn, args = {}) { const result = await this.executeFunctionResultAsync(fn, args); if (result.valuePromise) { return await result.valuePromise; } else { return result.value; } } async executeFunctionResultAsync(fn, args = {}) { if (fn.call) { const callee = this.sharedVars[fn.name]?.[convo_types_1.convoFlowControllerKey]?.sourceFn; if (!callee) { throw new ConvoError_1.ConvoError('function-not-defined', { fn }, `executeFunctionResultAsync - No function defined by the name ${fn.name}`); } args = await this.paramsToObjAsync(fn.params); fn = callee; } return this.executeFunction(fn, args); } getConvoFunctionArgsValue(fn) { const r = this.paramsToObj(fn.params ?? []); if (r.valuePromise) { throw new ConvoError_1.ConvoError('function-call-args-suspended'); } return r.value; } getConvoFunctionArgsScheme(fn, cache = true) { if (cache) { const s = fn[argsCacheKey]; if (s) { return s; } } let scheme; if (fn.paramType) { const type = this.getVarAsType(fn.paramType); if (!type) { throw new ConvoError_1.ConvoError('function-args-type-not-defined', { fn }); } if (!(type instanceof zod_1.ZodObject)) { throw new ConvoError_1.ConvoError('function-args-type-not-an-object', { fn }); } scheme = type; } else { scheme = this.paramsToScheme(fn.params ?? []); } if (cache) { fn[argsCacheKey] = scheme; } return scheme; } getConvoFunctionReturnScheme(fn, cache = true) { if (!fn.returnType) { return undefined; } if (cache) { const s = fn[returnCacheKey]; if (s) { return s; } } const typeVar = this.sharedVars[fn.returnType]; if (!typeVar) { throw new ConvoError_1.ConvoError('function-return-type-not-defined', { fn }, `Function return type not defined. function = ${fn.name}, returnType = ${fn.returnType}`); } const scheme = (0, convo_zod_1.convoValueToZodType)(typeVar); if (cache) { fn[returnCacheKey] = scheme; } return scheme; } getVarAsType(name) { const typeVar = this.sharedVars[name]; if (!typeVar) { return undefined; } return (0, convo_zod_1.convoValueToZodType)(typeVar); } paramsToObj(params) { const vars = {}; const scope = this.executeScope({ i: 0, vars, s: { fn: convo_lib_1.convoMapFnName, params, s: 0, e: 0, } }, undefined, createDefaultScope(vars)); return this.execute(scope, vars); } async paramsToObjAsync(params) { const r = this.paramsToObj(params); if (r.valuePromise) { return await r.valuePromise; } else { return r.value; } } paramsToScheme(params) { const vars = {}; const scope = this.executeScope({ i: 0, vars, s: { fn: convo_lib_1.convoStructFnName, params, s: 0, e: 0, } }, undefined, createDefaultScope(vars)); if (scope.si) { throw new ConvoError_1.ConvoError('suspended-scheme-statements-not-supported', { statements: params }, 'scheme statements should not be suspended'); } const zType = (0, convo_zod_1.convoValueToZodType)(scope.v); if (!(zType instanceof zod_1.ZodObject)) { throw new ConvoError_1.ConvoError('zod-object-expected', { statements: params }, 'ZodObject expected when converting ConvoStatements to zod type'); } return zType; } execute(scope, vars, resultScheme) { scope = this.executeScope(scope, undefined, createDefaultScope(vars)); if (scope.si) { return { scope, valuePromise: new Promise((r, j) => { if (!scope.onComplete) { scope.onComplete = []; } if (!scope.onError) { scope.onError = []; } scope.onError.push(j); if (resultScheme) { scope.onComplete.push(value => { const parsed = resultScheme.safeParse(value); if (parsed.success === true) { r(parsed.data); } else if (parsed.success === false) { j(new ConvoError_1.ConvoError('invalid-return-value-type', { statement: scope.s }, `Invalid result value - ${parsed.error.message}`)); } else { r(value); } }); } else { scope.onComplete.push(r); } }) }; } else { if (scope.error) { throw scope.error; } else { if (resultScheme) { const parsed = resultScheme.safeParse(scope.v); if (parsed.success === true) { return { scope, value: parsed.data }; } else if (parsed.success === false) { throw new ConvoError_1.ConvoError('invalid-return-value-type', { statement: scope.s }, `Invalid result value - ${parsed.error.message}`); } } return { scope, value: scope.v }; } } } executeScope(scope, parent, defaultScope, resumeParamScope) { const statement = scope.s; let value = undefined; if (statement.fn) { scope = copyDefaultScope(scope); const fn = scope[convo_types_1.convoScopeFnKey] ?? (scope[convo_types_1.convoScopeFnKey] = statement.fnPath ? (0, common_1.getValueByAryPath)(this.sharedVars, statement.fnPath)?.[statement.fn] : this.sharedVars[statement.fn]) ?? this.dynamicFunctionCallback ?? this.convo.conversation?.dynamicFunctionCallback; if (typeof fn !== 'function') { const errPath = statement.fnPath ? statement.fnPath.join('.') + '.' + statement.fn : statement.fn; (0, convo_lib_1.setConvoScopeError)(scope, `${errPath} is not a function`); value = undefined; return scope; } if (!scope.paramValues) { scope.paramValues = []; } const flowCtrl = fn[convo_types_1.convoFlowControllerKey]; const parentStartIndex = parent?.i ?? 0; if (flowCtrl?.keepData && parent?.childCtrlData) { const dr = parent.childCtrlData[parentStartIndex.toString()]; if (dr) { scope.ctrlData = dr.ctrlData; scope.childCtrlData = dr.childCtrlData; } } const shouldExecute = flowCtrl?.shouldExecute?.(scope, parent, this) ?? true; if (shouldExecute) { delete scope.li; if (flowCtrl?.startParam) { const startI = flowCtrl.startParam(scope, parent, this); if (startI === false) { scope.i = statement.params?.length ?? 0; } else { scope.i = Math.max(0, startI); } } if (statement.params?.length) { if (flowCtrl?.usesLabels && !scope.labels) { scope.labels = {}; } while (scope.i < statement.params.length && (scope.bi !== scope.i)) { const paramStatement = statement.params[scope.i]; if (paramStatement) { let paramScope; if (resumeParamScope) { paramScope = resumeParamScope; resumeParamScope = undefined; } else { const d = defaultScope; d.s = paramStatement; paramScope = this.executeScope(d, scope, defaultScope); } if (paramScope.error) { (0, convo_lib_1.setConvoScopeError)(scope, paramScope.error); return scope; } if (paramScope.si) { this.suspendScope(scope, paramScope); paramScope.pi = scope.si; return scope; } if (flowCtrl?.discardParams) { scope.paramValues[0] = paramScope.v; } else { scope.paramValues.push(paramScope.v); } if (paramScope.r) { value = paramScope.v; scope.r = true; break; } if (paramScope.bl) { if (flowCtrl?.catchBreak) { break; } else if (scope.li === scope.i) { delete scope.fromIndex; delete scope.gotoIndex; delete scope.li; } else { scope.bl = true; return scope; } } if (scope.fromIndex === scope.i && scope.gotoIndex !== undefined) { scope.i = scope.gotoIndex; delete scope.fromIndex; delete scope.gotoIndex; } else if (flowCtrl?.nextParam) { const f = flowCtrl.nextParam(scope, parent, paramStatement, this); if (f === false) { break; } scope.i = f; } else { scope.i++; } } else { (0, convo_lib_1.setConvoScopeError)(scope, 'Parameter expected'); return scope; } } } if (!scope.r) { value = fn(scope, this); } } if (scope.r) { if (flowCtrl?.catchReturn) { scope.r = false; } } else if (flowCtrl) { if (flowCtrl.keepData && parent) { if (!parent.childCtrlData) { parent.childCtrlData = {}; } const dr = { ctrlData: scope.ctrlData, childCtrlData: scope.childCtrlData, }; parent.childCtrlData[parentStartIndex.toString()] = dr; } if (flowCtrl.transformResult) { value = flowCtrl.transformResult(value, scope, parent, this); } } } else if (statement.ref) { value = this.getVarEx(statement.ref, statement.refPath, scope); } else { value = statement.value; } if (scope.error) { return scope; } if ((0, common_1.isPromise)(value)) { scope = copyDefaultScope(scope); this.suspendScope(scope); value.then(v => { scope.v = v; this.completeScope(scope, parent, defaultScope); }).catch(e => { (0, convo_lib_1.setConvoScopeError)(scope, { message: `Promise throw error - ${e?.message}`, error: e, statement, }); this.completeScope(scope, parent, defaultScope); }); } else { scope.v = value; this.completeScope(scope, parent, defaultScope); } return scope; } suspendScope(scope, waitFor) { if (!scope.si) { scope.si = (this.nextSuspendId++).toString(); } this.suspendedScopes[scope.si] = scope; if (waitFor) { scope.wi = waitFor.si; } } completeScope(scope, parent, defaultScope) { if (scope.wi) { throw new ConvoError_1.ConvoError('scope-waiting', { statement: scope.s }, `scope waiting on scope(${scope.wi}) before resuming`); } const statement = scope.s; if (statement.set) { this.setVar(statement.shared, scope.v, statement.set, statement.setPath, scope); } if (statement.label && parent?.labels) { parent.labels[statement.label] = statement.opt ? (0, convo_lib_1.createOptionalConvoValue)(parent.i) : parent.i; } delete scope.pi; const resume = scope.si ? [] : null; if (scope.si) { const si = scope.si; delete scope.si; delete this.suspendedScopes[si]; for (const e in this.suspendedScopes) { const ss = this.suspendedScopes[e]; if (ss?.wi === si) { delete this.suspendedScopes[e]; delete ss.wi; resume.push(ss); } } } if (scope.onComplete) { const oc = scope.onComplete; delete scope.onComplete; delete scope.onError; for (let i = 0; i < oc.length; i++) { oc[i]?.(scope.v); } } if (resume) { for (const r of resume) { const parent = r.pi ? this.suspendedScopes[r.pi] : undefined; if (r.pi && !parent) { throw new ConvoError_1.ConvoError('suspension-parent-not-found', { statement: scope.s }); } delete r.pi; this.executeScope(r, parent, defaultScope, scope); } } } getRefValue(statement, scope, throwUndefined = true) { if (!statement) { return undefined; } if (!statement.ref) { throw new ConvoError_1.ConvoError('variable-ref-required', { statement }); } return this.getVarEx(statement.ref, statement.refPath, scope, throwUndefined); } getVarEx(name, path, scope, throwUndefined = true) { let value = scope?.vars[name] ?? this.sharedVars[name]; if (value === undefined && (scope ? !(name in scope.vars) : true) && !(name in this.sharedVars)) { if (throwUndefined) { (0, convo_lib_1.setConvoScopeError)(scope, `reference to undefined var - ${name}`); } } else if (path) { value = (0, common_1.getValueByAryPath)(value, path); } return value; } getVar(nameOrPath, scope, defaultValue) { let path = undefined; if (nameOrPath.includes('.')) { path = nameOrPath.split('.'); nameOrPath = path.shift() ?? ''; } return this.getVarEx(nameOrPath, path, scope ?? undefined, false) ?? defaultValue; } setRefValue(statement, value, scope) { if (!statement) { return value; } if (!statement.ref) { throw new ConvoError_1.ConvoError('variable-ref-required', { statement }); } this.setVar(statement.shared, value, statement.ref, statement.refPath, scope); return value; } setDefaultVarValue(value, name, path) { if (name in convo_default_vars_1.defaultConvoVars) { (0, convo_lib_1.setConvoScopeError)(null, `Overriding builtin var not allowed - ${name}`); return value; } if (this.sharedVars[name] !== undefined) { return this.sharedVars[name]; } return this.setVar(true, value, name, path); } setVar(shared, value, name, path, scope) { if (name in convo_default_vars_1.defaultConvoVars) { (0, convo_lib_1.setConvoScopeError)(scope, `Overriding builtin var not allowed - ${name}`); return value; } const vars = (shared || !scope) ? this.sharedVars : scope.vars; if (shared !== false && vars === this.sharedVars && (typeof value !== 'function') && !this.sharedSetters.includes(name)) { this.sharedSetters.push(name); } if (path) { let obj = vars[name]; if (obj === undefined || obj === null) { (0, convo_lib_1.setConvoScopeError)(scope, `reference to undefined var for setting path - ${name}`); return value; } if (path.length > 1) { obj = (0, common_1.getValueByAryPath)(obj, path, undefined, path.length - 1); if (obj === undefined || obj === null) { (0, convo_lib_1.setConvoScopeError)(scope, `reference to undefined var at path - ${name}.${path.join('.')}`); return value; } } obj[path[path.length - 1] ?? ''] = value; } else { vars[name] = value; } return value; } } exports.ConvoExecutionContext = ConvoExecutionContext; //# sourceMappingURL=ConvoExecutionContext.js.map