UNPKG

@iyio/convo-lang

Version:

A conversational language.

471 lines (467 loc) 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConvoGraphCtrl = void 0; const common_1 = require("@iyio/common"); const rxjs_1 = require("rxjs"); const Conversation_1 = require("./Conversation"); const convo_graph_lib_1 = require("./convo-graph-lib"); const convo_lib_1 = require("./convo-lib"); const convo_template_1 = require("./convo-template"); const convo_deps_1 = require("./convo.deps"); class ConvoGraphCtrl { get onMonitorEvent() { return this._onMonitorEvent; } get hasListeners() { return this._onMonitorEvent.observed; } triggerEvent(evt) { evt.time = Date.now(); this._onMonitorEvent.next(evt); } async getConvoOptionsAsync(tv, initConvo) { return { ...this.defaultConvoOptions, disableAutoFlatten: true, initConvo: ((await this.getSharedSourceAsync()) + ((initConvo ? (this.defaultConvoOptions.initConvo ? `${this.defaultConvoOptions.initConvo}\n\n${initConvo}` : initConvo) : this.defaultConvoOptions.initConvo) || '')) || undefined, defaultVars: { ...this.defaultConvoOptions.defaultVars, input: tv?.payload, sourceInput: tv?.payload, tState: tv?.state, graphCtrl: this, } }; } constructor({ store = (0, convo_deps_1.convoGraphStore)(), convoOptions = {} }) { this._onMonitorEvent = new rxjs_1.Subject(); this.disposables = new common_1.DisposeContainer(); this._isDisposed = false; this.store = store; this.defaultConvoOptions = convoOptions; } get isDisposed() { return this._isDisposed; } dispose() { if (this._isDisposed) { return; } this.disposables.dispose(); this._isDisposed = true; } async startRunAsync(options) { const tv = await this.startTraversalAsync(options); await this.runGroupAsync(tv); return tv; } async startTraversalAsync({ createTvOptions, edge, edgePattern, payload = {}, state, saveToStore = false, cancel = new common_1.CancelToken(), }) { if (typeof edge === 'string') { edge = { id: (0, common_1.shortUuid)(), to: edge, from: '' }; } let edges; if (edge) { edges = [edge]; } else if (edgePattern) { edges = await this.getEdgesAsync(edgePattern); } else { edges = []; } const traversers = await Promise.all(edges.map((edge) => this.createTvAsync(edge, createTvOptions, payload, state, saveToStore))); return { traversers: new rxjs_1.BehaviorSubject(traversers), saveToStore, createTvOptions, cancel }; } async createTvAsync(edge, options, payload, state, saveToStore, addTo) { const defaults = options?.defaults; const tv = { ...defaults, id: defaults?.id ?? (0, common_1.shortUuid)(), exeState: 'invoked', state: defaults?.state ?? {}, currentStepIndex: 0, }; tv.payload = payload; if (state) { for (const e in state) { tv.state[e] = state[e]; } } if (saveToStore) { await this.store.putTraverserAsync(tv); } if (this.hasListeners) { this.triggerEvent({ type: 'start-traversal', text: 'Graph traversal started', traverser: tv }); } await this.traverseEdgeAsync(tv, edge); if (addTo) { (0, common_1.pushBehaviorSubjectAry)(addTo, tv); } return tv; } /** * Moves the traverser to the "to" side of the edge and updates the traversers execution state. * If the target node of the edge can not be found the traverser's execution state will be * set to failed. */ async traverseEdgeAsync(tv, edge) { if (tv.exeState !== 'invoked') { throw new Error('ConvoTraverser execution state must be set to `invoked` before traversing an edge'); } edge = (0, common_1.deepClone)(edge); const targetNode = await this.store.getNodeAsync(edge.to); if (!targetNode) { tv.exeState = 'failed'; tv.errorMessage = `Target to node with id ${edge.to} does not exist`; if (this.hasListeners) { this.triggerEvent({ type: 'traversal-failed', text: tv.errorMessage, traverser: tv, edge }); } return undefined; } tv.currentNodeId = targetNode.id; if (edge.pause) { tv.exeState = 'paused'; tv.pause = edge.pause; if (tv.pause.delayMs !== undefined) { tv.resumeAt = Date.now() + tv.pause.delayMs; } else { delete tv.resumeAt; } } else { tv.exeState = 'ready'; delete tv.resumeAt; delete tv.pause; } if (!tv.path) { tv.path = []; } tv.path.push(edge); if (this.hasListeners) { this.triggerEvent({ type: 'edge-crossed', text: 'Traverser crossed edge', pause: tv.pause ? { ...tv.pause } : undefined, traverser: tv, edge, node: targetNode }); } return targetNode; } async runGroupAsync(group) { const running = []; const runPromises = []; const startSrc = (0, common_1.createPromiseSource)(); const sub = group.traversers.subscribe(ary => { for (const t of ary) { if (!running.includes(t)) { running.push(t); const runP = this.runAsync(t, group); runPromises.push(runP); runP.then(() => { (0, common_1.aryRemoveItem)(runPromises, runP); }); } } startSrc.resolve(); }); await startSrc.promise; while (runPromises.length) { await Promise.all(runPromises); } sub.unsubscribe(); } async runAsync(tv, group) { while ((await this.nextAsync(tv, group)) === 'ready' && !group?.cancel.isCanceled) { // do nothing } return tv.exeState; } /** * Executes the current node the traverser in on then traverses to the next node or stops if * no matching edges are found. */ async nextAsync(tv, group) { if (tv.exeState !== 'ready') { throw new Error('ConvoTraverser execution state must be set to `ready` before executing a node'); } if (!tv.currentNodeId) { throw new Error('ConvoTraverser does not have its currentNodeId set'); } try { const newState = await this._nextAsync(tv, group); if (newState === 'failed') { if (this.hasListeners) { this.triggerEvent({ type: 'traversal-failed', text: tv.errorMessage ?? 'Traversal failed', traverser: tv, }); } } return newState; } catch (ex) { tv.currentStepIndex = 0; //throw errors can be retired if (this.hasListeners) { this.triggerEvent({ type: 'traversal-failed', text: (0, common_1.getErrorMessage)(ex), traverser: tv, }); } return 'failed'; } } async _nextAsync(tv, group) { const node = await this.store.getNodeAsync(tv.currentNodeId ?? ''); if (!node) { tv.exeState = 'failed'; tv.errorMessage = `Target node with id ${tv.currentNodeId} not found while trying to execute`; return tv.exeState; } if (this.hasListeners) { this.triggerEvent({ type: 'start-exe', text: 'Starting execution of node', traverser: tv, node, }); } const exeCtx = await (0, convo_graph_lib_1.createConvoNodeExecCtxAsync)(node, await this.getConvoOptionsAsync(tv)); // transform input let transformStep = null; if (exeCtx.metadata.inputType?.name) { const inputType = exeCtx.typeMap[exeCtx.metadata.inputType.name]; if (inputType) { transformStep = await this.transformInputAsync(tv, node, inputType, exeCtx); } else { tv.exeState = 'failed'; tv.errorMessage = 'Input type not found for transforming'; } if (tv.exeState === 'failed') { return 'failed'; } } let invokeCall; tv.exeState = 'invoking'; for (let i = transformStep ? -1 : 0; i < exeCtx.steps.length; i++) { const step = i === -1 ? transformStep : exeCtx.steps[i]; if (!step) { continue; } tv.currentStepIndex = i; invokeCall = await this.executeStepAsync(tv, node, step, i, exeCtx); if (tv.exeState === 'failed') { tv.currentStepIndex = 0; return 'failed'; } } tv.currentStepIndex = 0; tv.exeState = 'invoked'; const edges = await this.getEdgesAsync({ from: node.id, fromFn: invokeCall?.name, fromType: invokeCall?.fn.returnType, input: tv.payload }); if (edges.length) { await Promise.all(edges.map((edge, i) => { if (i === 0) { return this.traverseEdgeAsync(tv, edge); } // create fork return this.createTvAsync(edge, group?.createTvOptions, tv.payload, tv.state, group?.saveToStore ?? false, group?.traversers); })); } else { tv.exeState = 'stopped'; this.triggerEvent({ type: 'traversal-stopped', text: 'Traversal stopped', traverser: tv, node, }); } return tv.exeState; } /** * Returns all edges that match the given pattern */ async getEdgesAsync({ from, fromType, fromFn, input }) { let edges = await this.store.getNodeEdgesAsync(from, 'from'); if (fromType || fromFn) { edges = edges.filter(e => (((fromType && e.fromType) ? e.fromType === fromType : true) && ((fromFn && e.fromFn) ? e.fromFn === fromFn : true))); } for (let i = 0; i < edges.length; i++) { const edge = edges[i]; if (!edge?.conditionConvo) { continue; } try { const conversation = new Conversation_1.Conversation({ disableAutoFlatten: true, initConvo: (await this.getSharedSourceAsync()) + edge.conditionConvo, defaultVars: { input }, }); const flat = await conversation.flattenAsync(); const accept = flat.exe.getVar('accept'); if (!accept) { edges.splice(i, 1); i--; } } catch (ex) { console.error('Edge condition error', edge, ex); edges.splice(i, 1); i--; } } return edges; } async getSharedSourceAsync() { const nodes = (await this.store.getSourceNodesAsync()).filter(s => s.shared); if (!nodes.length) { return ''; } return nodes.map(n => n.source ?? '').join('\n\n') + '\n\n'; } async transformInputAsync(tv, node, inputType, exeCtx) { const parsed = inputType.safeParse(tv.payload); if (parsed.success) { tv.payload = parsed.data; } else { const co = (0, common_1.zodCoerceObject)(inputType, (((typeof tv.payload === 'object') && tv.payload) ? tv.payload : { value: tv.payload })); if (co.error) { if (!node.disableAutoTransform) { const transformStep = { name: 'Auto transform', convo: (0, convo_template_1.convoScript) ` @output # Sets the current input to the newly transformed input > setConverted() Input -> ( return(__args) ) @errorCallback # Call this function if you are unable to convert the arguments > conversionFailed( errorMessage?:string ) -> ( return(or(errorMessage 'failed')) ) > user Call the setConverted function using the sourceInput below. Convert the sourceInput as needed to match the parameters of setConverted. sourceInput: {{input}} ` }; if (this.hasListeners) { this.triggerEvent({ type: 'auto-transformer-created', text: `Auto transformer created`, traverser: tv, node, step: transformStep }); } return { nodeStep: transformStep, convo: new Conversation_1.Conversation({ ...this.getConvoOptionsAsync(tv), defaultVars: exeCtx.defaultVars, initConvo: ((await this.getSharedSourceAsync()) + (node.sharedConvo ? node.sharedConvo + '\n\n' : '') + transformStep.convo) }) }; } } else { tv.payload = co.result; } } exeCtx.defaultVars['input'] = tv.payload; return null; } async executeStepAsync(tv, node, step, stepIndex, exeCtx) { if (this.hasListeners) { this.triggerEvent({ type: 'execute-step', text: `Executing step ${stepIndex}`, traverser: tv, step: step.nodeStep, stepIndex, node, }); } const call = await this.callTargetAsync(step.nodeStep.name ?? `Step ${stepIndex}`, tv, step.convo); if (call) { tv.payload = call.returnValue; exeCtx.defaultVars['input'] = tv.payload; const stepKey = stepIndex === -1 ? 'stepAuto' : `step${stepIndex}`; exeCtx.defaultVars[stepKey] = tv.payload; } return call; } async callTargetAsync(name, tv, convo) { const call = (await convo.completeAsync({ returnOnCalled: true })).lastFnCall; if (this.hasListeners) { this.triggerEvent({ type: 'convo-result', text: convo.convo, traverser: tv, }); } if (!call) { tv.exeState = 'failed'; tv.errorMessage = `${name} function not called`; return undefined; } const isSuccess = call?.message.tags?.some(t => t.name === convo_lib_1.convoTags.output); const isError = call?.message.tags?.some(t => t.name === convo_lib_1.convoTags.errorCallback); if (call && (isSuccess || isError)) { if (isError) { tv.exeState = 'failed'; tv.errorMessage = (`${name} failed. Error callback called.` + ((typeof call.returnValue === 'string') ? ' ' + call.returnValue : '')); return undefined; } return call; } else { tv.exeState = 'failed'; tv.errorMessage = `${name} failed. function not called`; return undefined; } } } exports.ConvoGraphCtrl = ConvoGraphCtrl; //# sourceMappingURL=ConvoGraphCtrl.js.map