@convo-lang/convo-lang
Version:
The language of AI
681 lines (677 loc) • 26.5 kB
JavaScript
"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 {
store;
_tokenUsage = new rxjs_1.BehaviorSubject((0, convo_lib_1.createEmptyConvoTokenUsage)());
get tokenUsageSubject() { return this._tokenUsage; }
get tokenUsage() { return this._tokenUsage.value; }
_onMonitorEvent = new rxjs_1.Subject();
get onMonitorEvent() { return this._onMonitorEvent; }
get hasListeners() { return this._onMonitorEvent.observed || this.logEventsToConsole; }
triggerEvent(evt) {
evt.time = Date.now();
this._onMonitorEvent.next(evt);
if (this.logEventsToConsole) {
console.info((0, convo_graph_lib_1.getConvoGraphEventString)(evt, this._tokenUsage.value));
}
}
logEventsToConsole;
beforeNext;
defaultConvoOptions;
async getConvoOptionsAsync(tv, initConvo, defaultVarsOverride) {
return {
...this.defaultConvoOptions,
disableAutoFlatten: true,
onTokenUsage: usage => this.addTokenUsage(usage),
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,
workflow: tv?.state,
graphCtrl: this,
traverser: tv,
...defaultVarsOverride
}
};
}
addTokenUsage(usage) {
if ((0, convo_lib_1.isConvoTokenUsageEmpty)(usage)) {
return;
}
const u = { ...this._tokenUsage.value };
(0, convo_lib_1.addConvoUsageTokens)(u, usage);
this._tokenUsage.next(u);
}
maxConcurrentStepExe;
stepLock;
constructor({ store = (0, convo_deps_1.convoGraphStore)(), convoOptions = {}, maxConcurrentStepExe = convo_graph_lib_1.maxConvoGraphConcurrentStepExe, logEventsToConsole = false, beforeNext, }) {
this.store = store;
this.defaultConvoOptions = convoOptions;
this.maxConcurrentStepExe = Math.max(1, maxConcurrentStepExe);
this.stepLock = new common_1.Lock(this.maxConcurrentStepExe);
this.logEventsToConsole = logEventsToConsole;
this.beforeNext = beforeNext;
}
disposables = new common_1.DisposeContainer();
_isDisposed = false;
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(), traversers: traversersOpt, }) {
if (typeof edge === 'string') {
edge = {
id: (0, common_1.shortUuid)(),
to: edge,
from: ''
};
}
const traversers = [];
if (traversersOpt) {
traversers.push(...traversersOpt);
}
else {
let edges;
if (edge) {
edges = [edge];
}
else if (edgePattern) {
edges = await this.getEdgesAsync(edgePattern);
}
else {
edges = [];
}
const traversersArrays = await Promise.all(edges.map((edge) => this.createTvAsync(edge, createTvOptions, payload, state, saveToStore)));
for (const ta of traversersArrays) {
traversers.push(...ta);
}
}
const first = traversers.find(t => t.controlPath);
if (first) {
(0, convo_graph_lib_1.applyConvoTraverserControlPath)(first);
}
return {
traversers: new rxjs_1.BehaviorSubject(traversers),
saveToStore,
createTvOptions,
cancel
};
}
async createTvAsync(edge, options, payload, state, saveToStore, addTo) {
if (edge.selectPath) {
payload = (0, common_1.getValueByPath)(payload, edge.selectPath);
}
if (edge.loop && Array.isArray(payload)) {
return await Promise.all(payload.map(p => this._createTvAsync(edge, options, edge.loopSelectPath ? (0, common_1.getValueByPath)(p, edge.loopSelectPath) : p, state, saveToStore, addTo)));
}
else {
return [await this._createTvAsync(edge, options, payload, state, saveToStore, addTo)];
}
}
async _createTvAsync(edge, options, payload, state, saveToStore, addTo) {
let defaults = options?.defaults;
if (typeof defaults === 'function') {
defaults = defaults(edge, options, payload, state, saveToStore);
}
const tv = {
...defaults,
id: defaults?.id ?? (0, common_1.shortUuid)(),
exeState: 'invoked',
state: defaults?.state ?? {},
currentStepIndex: 0,
saveToStore
};
tv.payload = payload;
if (state) {
for (const e in state) {
tv.state[e] = state[e];
}
}
await options?.initTraverser?.(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
});
}
if (tv.saveToStore) {
await this.saveTraverserAsync(tv);
}
return undefined;
}
tv.currentNodeId = targetNode.id;
if (!tv.startingNodeId) {
tv.startingNodeId = 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.id);
if (this.hasListeners) {
this.triggerEvent({
type: 'edge-crossed',
text: 'Traverser crossed edge',
pause: tv.pause ? { ...tv.pause } : undefined,
traverser: tv,
edge,
node: targetNode
});
}
if (tv.saveToStore) {
await this.saveTraverserAsync(tv);
}
return targetNode;
}
async saveTraverserAsync(tv) {
(0, convo_graph_lib_1.applyConvoTraverserControlPath)(tv);
const putP = this.store.putTraverserAsync(tv);
if (this.store.putTraverserProxiesAsync) {
await Promise.all([putP, this.store.putTraverserProxiesAsync(tv)]);
}
else {
await putP;
}
}
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) {
await this.store.loadTraverserProxiesAsync?.(tv);
while ((!this.beforeNext || await this.beforeNext(tv, group, this)) &&
(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 getNodeMetadataAsync(node) {
const convoOptions = await this.getConvoOptionsAsync(undefined);
const defaultVars = {
...convoOptions?.defaultVars
};
return await (0, convo_graph_lib_1.getConvoNodeMetadataAsync)((node.sharedConvo || node.steps.length) ?
(0, convo_graph_lib_1.createConvoNodeExecCtxConvo)(node, defaultVars, convoOptions, (node.steps[node.steps.length - 1]?.convo ?? '')) :
null);
}
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 startSuffix = tv.state[convo_graph_lib_1.convoTraverserStateStoreSuffix];
const startProxyPaths = {};
let map = tv.state[convo_graph_lib_1.convoTraverserProxyVar];
if (this.store.loadTraverserProxiesAsync && map && (typeof map === 'object')) {
for (const e in map) {
const p = map[e];
if (!p) {
continue;
}
if (typeof p === 'string') {
startProxyPaths[e] = p;
}
else {
startProxyPaths[e] = p.path;
}
}
}
const checkProxyAsync = async (suffixChanged) => {
map = tv.state[convo_graph_lib_1.convoTraverserProxyVar];
if (this.store.loadTraverserProxiesAsync && map && (typeof map === 'object')) {
const changes = [];
for (const e in map) {
const p = map[e];
if (!p) {
continue;
}
let path;
if (typeof p === 'string') {
path = p;
}
else {
path = p.path;
}
if (path !== startProxyPaths[e]) {
changes.push(e);
}
}
if (changes.length || suffixChanged) {
await this.store.loadTraverserProxiesAsync(tv, changes);
}
}
};
const exeCtx = await (0, convo_graph_lib_1.createConvoNodeExecCtxAsync)(node, await this.getConvoOptionsAsync(tv));
await checkProxyAsync(false);
// 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';
}
await checkProxyAsync(false);
}
tv.currentStepIndex = 0;
tv.exeState = 'invoked';
const newSuffix = tv.state[convo_graph_lib_1.convoTraverserStateStoreSuffix];
const suffixChanged = (newSuffix && startSuffix !== newSuffix) ? true : false;
if (suffixChanged) {
const stateTv = await this.store.getTraverserAsync(tv.id, newSuffix);
if (stateTv) {
for (const e in stateTv.state) {
if (e === convo_graph_lib_1.convoTraverserStateStoreSuffix) {
continue;
}
tv.state[e] = stateTv.state[e];
}
}
}
await checkProxyAsync(suffixChanged);
await exeCtx.convo.flattenAsync();
const edges = await this.getEdgesAsync({
from: node.id,
fromFn: invokeCall?.call?.fn.name,
fromType: invokeCall?.type,
input: tv.payload,
workflow: tv?.state,
});
if (edges.length) {
await Promise.all(edges.map(async (edge, i) => {
if (i === 0 && !edge.loop) {
if (edge.loop) {
tv.exeState = 'stopped';
this.triggerEvent({
type: 'traversal-stopped',
text: 'Traversal yeilded to loop',
traverser: tv,
node,
});
if (tv.saveToStore) {
await this.saveTraverserAsync(tv);
}
}
else {
if (edge.selectPath) {
tv.payload = (0, common_1.getValueByPath)(tv.payload, edge.selectPath);
}
await this.traverseEdgeAsync(tv, edge);
return;
}
}
// create fork
await 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,
});
if (tv.saveToStore) {
await this.saveTraverserAsync(tv);
}
}
return tv.exeState;
}
/**
* Returns all edges that match the given pattern
*/
async getEdgesAsync({ from, fromType, fromFn, input, workflow, }) {
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(await this.getConvoOptionsAsync(undefined, `\n> edgeConditionEvalFunction() -> ( ${edge.conditionConvo} )\n` +
`> do` +
`edgeConditionResult=edgeConditionEvalFunction()`, { input, workflow }));
if (edge.userData) {
const varName = edge.userDataVarName === undefined ? convo_graph_lib_1.defaultConvoGraphUserDataVarName : edge.userDataVarName;
if (varName && conversation.defaultVars[varName] === undefined) {
conversation.defaultVars[varName] = { ...edge.userData };
}
}
const flat = await conversation.flattenAsync();
const accept = flat.exe.getVar('edgeConditionResult');
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',
resetConvo: true,
convo: (0, convo_template_1.convoScript) `
# Sets the current input to the newly transformed input
> setConverted() Input -> (
return(__args)
)
# 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({
// ...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) {
// lock here
const release = await this.stepLock.waitAsync();
try {
if (this.hasListeners) {
this.triggerEvent({
type: 'execute-step',
text: `Executing step ${stepIndex} ${node.key ?? node.id}`,
traverser: tv,
step: step.nodeStep,
stepIndex,
node,
});
}
if (step.nodeStep.resetConvo) {
(0, convo_graph_lib_1.resetConvoNodeExecCtxConvo)(exeCtx);
}
const msgCount = exeCtx.convo.messages.length;
exeCtx.convo.append(step.nodeStep.convo);
const call = await this.callTargetAsync(step.nodeStep.name ?? `Step ${stepIndex}`, tv, exeCtx.convo, msgCount, stepIndex === exeCtx.steps.length - 1);
if (call) {
tv.payload = call.value;
exeCtx.defaultVars['input'] = tv.payload;
const stepKey = stepIndex === -1 ? 'stepAuto' : `step${stepIndex}`;
exeCtx.defaultVars[stepKey] = tv.payload;
if (step.nodeStep.name) {
exeCtx.defaultVars[step.nodeStep.name] = tv.payload;
}
}
return call;
}
finally {
release();
}
}
async callTargetAsync(name, tv, convo, msgStartIndex, isLast) {
const outputFn = (0, convo_lib_1.getConvoFnByTag)(convo_lib_1.convoTags.output, convo.messages, msgStartIndex);
const errFn = (0, convo_lib_1.getConvoFnByTag)(convo_lib_1.convoTags.errorCallback, convo.messages, msgStartIndex);
const result = await convo.completeAsync({ returnOnCalled: isLast, toolChoice: outputFn ? { name: outputFn.name } : undefined });
const call = result.lastFnCall;
if (this.hasListeners) {
this.triggerEvent({
type: 'convo-result',
text: convo.convo,
traverser: tv,
});
}
if (!call && (outputFn || errFn)) {
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,
value: call.returnValue,
type: call.fn.returnType,
};
}
else if (!outputFn) {
if (call) {
return {
call,
value: call.returnValue,
type: call.fn.returnType,
};
}
else {
const value = (result.message?.format === 'json') ? JSON.parse(result.message.content?.trim() || 'null') : result.message?.content;
return {
value,
type: typeof value,
};
}
}
else {
tv.exeState = 'failed';
tv.errorMessage = `${name} failed. function not called`;
return undefined;
}
}
}
exports.ConvoGraphCtrl = ConvoGraphCtrl;
//# sourceMappingURL=ConvoGraphCtrl.js.map