@iyio/convo-lang
Version:
A conversational language.
1,253 lines (1,252 loc) • 68 kB
JavaScript
import { aryRemoveItem, asArray, delayAsync, parseMarkdown, pushBehaviorSubjectAryMany, removeBehaviorSubjectAryValue, removeBehaviorSubjectAryValueMany, safeParseNumber, shortUuid } from "@iyio/common";
import { BehaviorSubject, Subject } from "rxjs";
import { ConvoError } from "./ConvoError";
import { ConvoExecutionContext } from "./ConvoExecutionContext";
import { addConvoUsageTokens, containsConvoTag, convoDescriptionToComment, convoDisableAutoCompleteName, convoFunctions, convoLabeledScopeParamsToObj, convoMessageToString, convoRagDocRefToMessage, convoResultReturnName, convoRoles, convoStringToComment, convoTagMapToCode, convoTags, convoTagsToMap, convoTaskTriggers, convoUsageTokensToString, convoVars, defaultConvoPrintFunction, defaultConvoRagTol, defaultConvoTask, defaultConvoVisionSystemMessage, escapeConvoMessageContent, formatConvoMessage, getConvoDateString, getConvoTag, getLastCompletionMessage, isConvoThreadFilterMatch, mapToConvoTags, parseConvoJsonMessage, parseConvoMessageTemplate, spreadConvoArgs, validateConvoFunctionName, validateConvoTypeName, validateConvoVarName } from "./convo-lib";
import { parseConvoCode } from "./convo-parser";
import { convoObjFlag, isConvoCapability, isConvoRagMode } from "./convo-types";
import { convoTypeToJsonScheme, schemeToConvoTypeString, zodSchemeToConvoTypeString } from "./convo-zod";
import { convoCompletionService } from "./convo.deps";
import { createConvoVisionFunction } from "./createConvoVisionFunction";
export class Conversation {
get convo() { return this._convo.join('\n\n'); }
getConvoStrings() {
return [...this._convo];
}
get messages() { return this._messages; }
get onAppend() { return this._onAppend; }
get activeTaskCountSubject() { return this._activeTaskCount; }
get activeTaskCount() { return this._activeTaskCount.value; }
get trackTimeSubject() { return this._trackTime; }
get trackTime() { return this._trackTime.value; }
set trackTime(value) {
if (value == this._trackTime.value) {
return;
}
this._trackTime.next(value);
}
get trackTokensSubject() { return this._trackTokens; }
get trackTokens() { return this._trackTokens.value; }
set trackTokens(value) {
if (value == this._trackTokens.value) {
return;
}
this._trackTokens.next(value);
}
get trackModelSubject() { return this._trackModel; }
get trackModel() { return this._trackModel.value; }
set trackModel(value) {
if (value == this._trackModel.value) {
return;
}
this._trackModel.next(value);
}
get debugModeSubject() { return this._debugMode; }
get debugMode() { return this._debugMode.value; }
set debugMode(value) {
if (value == this._debugMode.value) {
return;
}
this._debugMode.next(value);
}
get flatSubject() { return this._flat; }
/**
* A reference to the last flattening of the conversation
*/
get flat() { return this._flat.value; }
get subTasksSubject() { return this._subTasks; }
get subTasks() { return this._subTasks.value; }
get beforeAppend() { return this._beforeAppend; }
constructor(options = {}) {
this._convo = [];
this._messages = [];
this._onAppend = new Subject();
this._activeTaskCount = new BehaviorSubject(0);
this._debugMode = new BehaviorSubject(false);
this._flat = new BehaviorSubject(null);
this._subTasks = new BehaviorSubject([]);
this._beforeAppend = new Subject();
/**
* Unregistered variables will be available during execution but will not be added to the code
* of the conversation. For example the __cwd var is often used to set the current working
* directory but is not added to the conversation code.
*/
this.unregisteredVars = {};
this.externFunctions = {};
this.print = defaultConvoPrintFunction;
this._isDisposed = false;
this._defaultApiKey = null;
this.enabledCapabilities = [];
this.autoFlattenId = 0;
this.autoFlatPromiseRef = null;
this._isCompleting = new BehaviorSubject(false);
this.importMessages = [];
this.debugToConversation = (...args) => {
if (!args.length) {
return;
}
const out = [];
for (const v of args) {
if (typeof v === 'string') {
out.push(v);
}
else {
try {
out.push(JSON.stringify(v, null, 4) ?? '');
}
catch {
out.push(v?.toString() ?? '');
}
}
}
const debugComment = convoStringToComment(out.join('\n'));
this.append(`> debug\n${debugComment}`);
};
this.definitionItems = [];
this.preAssignMessages = [];
const { userRoles = ['user'], roleMap = {}, completionService = convoCompletionService.get(), capabilities = [], serviceCapabilities = [], maxAutoCompleteDepth = 10, trackTime = false, trackTokens = false, trackModel = false, disableAutoFlatten = false, autoFlattenDelayMs = 30, ragCallback, debug, debugMode, disableMessageCapabilities = false, initConvo, defaultVars, onConstructed, define, } = options;
this.defaultOptions = options;
this.defaultVars = defaultVars ? defaultVars : {};
this.userRoles = userRoles;
this.roleMap = roleMap;
this.completionService = completionService;
this.capabilities = [...capabilities];
this.disableMessageCapabilities = disableMessageCapabilities;
this.serviceCapabilities = serviceCapabilities;
this.maxAutoCompleteDepth = maxAutoCompleteDepth;
this._trackTime = new BehaviorSubject(trackTime);
this._trackTokens = new BehaviorSubject(trackTokens);
this._trackModel = new BehaviorSubject(trackModel);
this.disableAutoFlatten = disableAutoFlatten;
this.autoFlattenDelayMs = autoFlattenDelayMs;
this.ragCallback = ragCallback;
this.debug = debug;
if (debugMode) {
this.debugMode = true;
}
if (initConvo) {
this.append(initConvo, true);
}
if (define) {
this.define(define);
}
onConstructed?.(this);
}
getMessageListCapabilities(msgs) {
let firstMsg;
let lastMsg;
for (let i = 0; i < msgs.length; i++) {
const f = msgs[i];
if (f && (!f.fn || f.fn.topLevel)) {
firstMsg = f;
break;
}
}
for (let i = msgs.length - 1; i >= 0; i--) {
const f = msgs[i];
if (f && (!f.fn || f.fn.topLevel)) {
lastMsg = f;
break;
}
}
if (!firstMsg && !lastMsg) {
return [];
}
if (firstMsg === lastMsg) {
lastMsg = undefined;
}
const tags = [];
if (firstMsg?.tags) {
const t = mapToConvoTags(firstMsg.tags);
tags.push(...t);
}
if (lastMsg?.tags) {
const t = mapToConvoTags(lastMsg.tags);
tags.push(...t);
}
return this.getMessageCapabilities(tags) ?? [];
}
/**
* Gets the capabilities enabled by the given tags. If disableMessageCapabilities is true
* undefined is always returned
*/
getMessageCapabilities(tags) {
if (!tags || this.disableMessageCapabilities) {
return undefined;
}
let capList;
for (const tag of tags) {
switch (tag.name) {
case convoTags.capability:
if (tag.value) {
const caps = tag.value.split(',');
for (const c of caps) {
const cap = c.trim();
if (isConvoCapability(cap) && !capList?.includes(cap)) {
if (!capList) {
capList = [];
}
capList.push(cap);
}
}
}
break;
case convoTags.enableVision:
if (!capList?.includes('vision')) {
if (!capList) {
capList = [];
}
capList.push('vision');
}
}
}
if (!capList) {
return undefined;
}
for (const cap of capList) {
this.enableCapability(cap);
}
return capList;
}
get isDisposed() { return this._isDisposed; }
dispose() {
if (this._isDisposed) {
return;
}
this.autoFlattenId++;
this._isDisposed = true;
}
setDefaultApiKey(key) {
this._defaultApiKey = key;
}
getDefaultApiKey() { return this._defaultApiKey; }
parseCode(code) {
return parseConvoCode(code, { parseMarkdown: this.defaultOptions.parseMarkdown });
}
enableCapability(cap) {
if (this.enabledCapabilities.includes(cap)) {
return;
}
this.enabledCapabilities.push(cap);
switch (cap) {
case 'vision':
this.define({
hidden: true,
fn: createConvoVisionFunction()
}, true);
break;
}
}
autoUpdateCompletionService() {
if (!this.completionService) {
this.completionService = convoCompletionService.get();
}
}
createChild(options) {
const convo = new Conversation({
...this.defaultOptions,
debug: this.debugToConversation,
debugMode: this.shouldDebug(),
...options
});
if (this._defaultApiKey) {
convo.setDefaultApiKey(this._defaultApiKey);
}
return convo;
}
/**
* Creates a new Conversation and appends the messages of this conversation to the newly
* created conversation.
*/
clone(options) {
const conversation = new Conversation(this.defaultOptions);
let messages = this.messages;
if (options?.noFunctions) {
messages = messages.filter(m => !m.fn || m.fn.topLevel);
}
if (options?.systemOnly) {
messages = messages.filter(m => m.role === 'system' || m.fn?.topLevel || m.fn?.name === convoFunctions.getState);
}
conversation.appendMessageObject(messages);
for (const e in this.externFunctions) {
const f = this.externFunctions[e];
if (f) {
conversation.externFunctions[e] = f;
}
}
return conversation;
}
/**
* Creates a new Conversation and appends the system messages of this conversation to the newly
* created conversation.
*/
cloneSystem() {
return this.clone({ systemOnly: true });
}
/**
* Creates a new Conversation and appends the non-function messages of this conversation to the newly
* created conversation.
*/
cloneWithNoFunctions() {
return this.clone({ noFunctions: true });
}
/**
* Appends new messages to the conversation and by default does not add code to the conversation.
*/
appendMessageObject(message, { disableAutoFlatten, appendCode } = {}) {
const messages = asArray(message);
this._messages.push(...messages);
if (appendCode) {
for (const msg of messages) {
let messages = convoMessageToString(msg);
if (this._beforeAppend.observed) {
messages = this.transformMessageBeforeAppend(messages);
}
this._convo.push(messages);
}
}
this._onAppend.next({
text: '',
messages,
});
if (!this.disableAutoFlatten && !disableAutoFlatten) {
this.autoFlattenAsync(false);
}
}
transformMessageBeforeAppend(messages) {
const append = {
text: messages,
messages: []
};
this._beforeAppend.next(append);
return append.text;
}
append(messages, mergeWithPrev = false, throwOnError = true) {
let visibleContent = undefined;
let hasHidden = false;
if (Array.isArray(messages)) {
hasHidden = messages.some(m => (typeof m === 'object') ? m.hidden : false);
if (hasHidden) {
visibleContent = messages.filter(m => (typeof m === 'string') || !m.hidden).map(m => (typeof m === 'string') ? m : m.content).join('');
messages = messages.map(m => (typeof m === 'string') ? m : m.content).join('');
}
else {
messages = messages.map(m => (typeof m === 'string') ? m : m.content).join('');
}
}
if (this._beforeAppend.observed) {
messages = this.transformMessageBeforeAppend(messages);
}
const r = this.parseCode(messages);
if (r.error) {
if (!throwOnError) {
return r;
}
throw r.error;
}
if (hasHidden) {
messages = visibleContent ?? '';
}
if (messages) {
if (mergeWithPrev && this._convo.length) {
this._convo[this._convo.length - 1] += '\n' + messages;
}
else {
this._convo.push(messages);
}
}
if (r.result) {
for (const m of r.result) {
this._messages.push(m);
}
}
this._onAppend.next({
text: messages,
messages: r.result ?? []
});
if (!this.disableAutoFlatten) {
this.autoFlattenAsync(false);
}
return r;
}
async autoFlattenAsync(skipDelay) {
const id = ++this.autoFlattenId;
if (this.autoFlattenDelayMs > 0 && !skipDelay) {
await delayAsync(this.autoFlattenDelayMs);
if (this.isDisposed || id !== this.autoFlattenId) {
return undefined;
}
}
return await this.getAutoFlattenPromise(id);
}
getAutoFlattenPromise(id) {
if (this.autoFlatPromiseRef?.id === id) {
return this.autoFlatPromiseRef.promise;
}
const promise = this.setFlattenAsync(id);
this.autoFlatPromiseRef = {
id,
promise,
};
return promise;
}
async setFlattenAsync(id) {
const flat = await this.flattenAsync(undefined, { setCurrent: false });
if (this.isDisposed || id !== this.autoFlattenId) {
return undefined;
}
this.setFlat(flat, false);
return flat;
}
/**
* Get the flattened version of this Conversation.
* @param noCache If true the Conversation will not used the current cached version of the
* flattening and will be re-flattened.
*/
async getLastAutoFlatAsync(noCache = false) {
if (noCache) {
return (await this.autoFlattenAsync(true)) ?? this.flat ?? undefined;
}
return (this.flat ??
(await this.getAutoFlattenPromise(this.autoFlattenId)) ??
this.flat ??
undefined);
}
appendUserMessage(message, options) {
this.append(formatConvoMessage('user', message, this.getPrefixTags(options)));
}
appendAssistantMessage(message, options) {
this.append(formatConvoMessage('assistant', message, this.getPrefixTags(options)));
}
appendMessage(role, message, options) {
this.append(formatConvoMessage(role, message, this.getPrefixTags(options)));
}
appendDefine(defineCode, description) {
return this.append((description ? convoDescriptionToComment(description) + '\n' : '') + '> define\n' + defineCode);
}
appendTopLevel(defineCode, description) {
return this.append((description ? convoDescriptionToComment(description) + '\n' : '') + '> do\n' + defineCode);
}
getVar(nameOrPath, defaultValue) {
return this._flat.value?.exe?.getVar(nameOrPath, null, defaultValue);
}
getPrefixTags(options) {
let tags = '';
const msg = options?.msg;
if (this.trackTime || this.getVar(convoVars.__trackTime)) {
tags += `@${convoTags.time} ${getConvoDateString()}\n`;
}
if (!msg) {
return tags;
}
if (options?.includeTokenUsage && (this.trackTokens || this.getVar(convoVars.__trackTokenUsage))) {
tags += `@${convoTags.tokenUsage} ${convoUsageTokensToString(msg)}\n`;
}
if (msg.model && (this.trackModel || this.getVar(convoVars.__trackModel))) {
tags += `@${convoTags.model} ${msg.model}\n`;
}
if (msg.endpoint) {
tags += `@${convoTags.endpoint} ${msg.endpoint}\n`;
}
if (msg.format) {
tags += `@${convoTags.format} ${msg.format}\n`;
}
if (msg.assignTo) {
tags += `@${convoTags.assign} ${msg.assignTo}\n`;
}
return tags;
}
setFlat(flat, dup = true) {
if (this.isDisposed) {
return;
}
this.autoFlattenId++;
this._flat.next(dup ? { ...flat } : flat);
}
async callFunctionAsync(fn, args = {}, options) {
const c = await this.tryCompleteAsync(options?.task, options, flat => {
if (typeof fn === 'object') {
flat.exe.loadFunctions([{
fn,
role: 'function'
}]);
}
return [{
callFn: typeof fn === 'string' ? fn : fn.name,
callParams: args
}];
});
return c.returnValues?.[0];
}
appendFunctionCall(functionName, args) {
this.append(`@${convoTags.toolId} call_${shortUuid()}\n> call ${functionName}(${args === undefined ? '' : spreadConvoArgs(args, true)})`);
}
completeWithFunctionCallAsync(name, args, options) {
this.appendFunctionCall(name, args);
return this.completeAsync(options);
}
/**
* Appends a user message then competes the conversation
* @param append Optional message to append before submitting
*/
completeUserMessageAsync(userMessage) {
this.appendUserMessage(userMessage);
return this.completeAsync();
}
/**
* Submits the current conversation and optionally appends messages to the conversation before
* submitting.
* @param append Optional message to append before submitting
*/
async completeAsync(appendOrOptions) {
if (typeof appendOrOptions === 'string') {
this.append(appendOrOptions);
appendOrOptions = undefined;
}
if (appendOrOptions?.append) {
this.append(appendOrOptions.append);
}
const completionService = this.completionService;
if (appendOrOptions?.debug) {
console.info('Conversation.completeAsync:\n', appendOrOptions.append);
}
const result = await this.tryCompleteAsync(appendOrOptions?.task, appendOrOptions, flat => completionService?.completeConvoAsync(flat) ?? []);
if (appendOrOptions?.debug) {
console.info('Conversation.completeAsync Result:\n', result.messages ? (result.messages.length === 1 ? result.messages[0] : result.messages) : result);
}
return result;
}
/**
* Completes the conversation and returns the last message as JSON. It is recommended using
* `@json` mode with the last message that is appended.
*/
async completeJsonAsync(appendOrOptions) {
const r = await this.completeAsync(appendOrOptions);
if (r.message?.content === undefined) {
return undefined;
}
try {
return parseConvoJsonMessage(r.message.content);
}
catch {
return undefined;
}
}
/**
* Completes the conversation and returns the last message as JSON. It is recommended using
* `@json` mode with the last message that is appended.
*/
async completeJsonSchemeAsync(params, userMessage) {
const r = await this.completeAsync(/*convo*/ `
> define
JsonScheme=${zodSchemeToConvoTypeString(params)}
@json JsonScheme
> user
${escapeConvoMessageContent(userMessage)}
`);
if (r.message?.content === undefined) {
return undefined;
}
try {
return parseConvoJsonMessage(r.message.content);
}
catch {
return undefined;
}
}
/**
* Completes the conversation and returns the last message call params. The last message of the
* conversation should instruct the LLM to call a function.
*/
async callStubFunctionAsync(appendOrOptions) {
if (appendOrOptions === undefined) {
appendOrOptions = {};
}
else if (typeof appendOrOptions === 'string') {
appendOrOptions = { append: appendOrOptions };
}
appendOrOptions.returnOnCall = true;
const r = await this.completeAsync(appendOrOptions);
return r.message?.callParams;
}
async tryCompleteAsync(task, additionalOptions, getCompletion, autoCompleteDepth = 0, prevCompletion, preReturnValues) {
if (this._isCompleting.value) {
return {
status: 'busy',
messages: [],
task: task ?? defaultConvoTask,
};
}
else {
this._isCompleting.next(true);
try {
return await this._completeAsync(additionalOptions?.usage, task, additionalOptions, getCompletion, autoCompleteDepth, prevCompletion, preReturnValues);
}
finally {
this._isCompleting.next(false);
}
}
}
get isCompletingSubject() { return this._isCompleting; }
get isCompleting() { return this._isCompleting.value; }
async _completeAsync(usage, task, additionalOptions, getCompletion, autoCompleteDepth, prevCompletion, preReturnValues, templates, updateTaskCount = true, lastFnCall) {
if (task === undefined) {
task = defaultConvoTask;
}
const isDefaultTask = task === defaultConvoTask;
if (updateTaskCount) {
this._activeTaskCount.next(this.activeTaskCount + 1);
}
const messageStartIndex = this._messages.length;
try {
const append = [];
const flatExe = this.createConvoExecutionContext(append);
const flat = await this.flattenAsync(flatExe, {
setCurrent: false,
task,
discardTemplates: !isDefaultTask || templates !== undefined,
threadFilter: additionalOptions?.threadFilter,
});
if (flat.templates && !templates) {
templates = flat.templates;
}
const exe = flat.exe;
if (this._isDisposed) {
return { messages: [], status: 'disposed', task };
}
for (const e in this.unregisteredVars) {
flat.exe.setVar(false, this.unregisteredVars[e], e);
}
let ragDoc;
let ragMsg;
const lastFlatMsg = getLastCompletionMessage(flat.messages);
if (lastFlatMsg &&
task === defaultConvoTask &&
flat.ragMode &&
this.ragCallback &&
lastFlatMsg?.role === convoRoles.user) {
const ragParams = flat.exe.getVar(convoVars.__ragParams);
const tol = flat.exe.getVar(convoVars.__ragTol);
ragDoc = await this.ragCallback({
params: ragParams && (typeof ragParams === 'object') ? ragParams : {},
lastMessage: lastFlatMsg,
flat,
conversation: this,
tolerance: (typeof tol === 'number') ? tol : defaultConvoRagTol
});
}
if (ragDoc) {
ragMsg = convoRagDocRefToMessage(ragDoc, convoRoles.rag);
flat.messages.push(this.flattenMsg(ragMsg, true));
this.applyRagMode(flat.messages, flat.ragMode);
this.appendMessageObject(ragMsg, { disableAutoFlatten: true, appendCode: true });
}
if (isDefaultTask) {
this.setFlat(flat);
}
const lastMsg = this.messages[this.messages.length - 1];
const lastMsgIsFnCall = lastMsg?.fn?.call ? true : false;
const completion = (lastMsg?.fn?.call ?
[{
callFn: lastMsg.fn.name,
callParams: exe.getConvoFunctionArgsValue(lastMsg.fn),
tags: { toolId: getConvoTag(lastMsg.tags, convoTags.toolId)?.value ?? '' }
}] :
await getCompletion(flat));
if (this._isDisposed) {
return { messages: [], status: 'disposed', task };
}
let cMsg = undefined;
let returnValues = undefined;
let lastResultValue = undefined;
for (const msg of completion) {
let includeTokenUsage = (msg.inputTokens || msg.outputTokens) ? true : false;
const tagsCode = msg.tags ? convoTagMapToCode(msg.tags, '\n') : '';
if (msg.format === 'json' && msg.content) {
let json = parseConvoJsonMessage(msg.content);
if (msg.formatIsArray && !Array.isArray(json) && Array.isArray(json.values)) {
json = json.values;
}
msg.content = JSON.stringify(json, null, 4);
}
if (msg.content) {
cMsg = msg;
if (isDefaultTask) {
this.append(`${this.getPrefixTags({ includeTokenUsage, msg })}${tagsCode}> ${this.getReversedMappedRole(msg.role)}\n${escapeConvoMessageContent(msg.content)}\n`);
}
includeTokenUsage = false;
}
if (additionalOptions?.returnOnCall && msg.callFn) {
cMsg = msg;
}
else if (msg.callFn) {
let callMessage;
if (lastMsgIsFnCall) {
callMessage = lastMsg;
if (!callMessage) {
throw new Error('Call last message failed');
}
}
else {
const callMsg = `${this.getPrefixTags({ includeTokenUsage, msg })}${tagsCode}> call ${msg.callFn}(${msg.callParams === undefined ? '' : spreadConvoArgs(msg.callParams, true)})`;
const result = isDefaultTask ? this.append(callMsg) : this.parseCode(callMsg);
includeTokenUsage = false;
callMessage = result.result?.[0];
if (result.result?.length !== 1 || !callMessage) {
throw new ConvoError('function-call-parse-count', { completion: msg }, 'failed to parse function call. Exactly 1 function call should have been parsed');
}
}
exe.clearSharedSetters();
if (!callMessage.fn?.call) {
continue;
}
const callResult = await exe.executeFunctionResultAsync(callMessage.fn);
if (this._isDisposed) {
return { messages: [], status: 'disposed', task };
}
const callResultValue = callResult.valuePromise ? (await callResult.valuePromise) : callResult.value;
const target = this._messages.find(m => m.fn && !m.fn.call && m.fn?.name === callMessage?.fn?.name);
const disableAutoComplete = (!isDefaultTask ||
exe.getVarEx(convoDisableAutoCompleteName, undefined, callResult.scope, false) === true ||
containsConvoTag(target?.tags, convoTags.disableAutoComplete));
if (!returnValues) {
returnValues = [];
}
returnValues.push(callResultValue);
if (target?.fn) {
lastFnCall = {
name: target.fn.name,
message: target,
fn: target.fn,
returnValue: callResultValue
};
}
lastResultValue = (typeof callResultValue === 'function') ? undefined : callResultValue;
const lines = [`${this.getPrefixTags()}> result`];
if (exe.sharedSetters) {
for (const s of exe.sharedSetters) {
lines.push(`${s}=${JSON.stringify(exe.sharedVars[s], null, 4)}`);
}
}
if ((typeof lastResultValue === 'string') &&
lastResultValue.length > 50 &&
lastResultValue.includes('\n') &&
!lastResultValue.includes('---')) {
lines.push(`${convoResultReturnName}=---\n${lastResultValue}\n---`);
}
else {
lines.push(`${convoResultReturnName}=${JSON.stringify(lastResultValue, null, 4)}`);
}
lines.push('');
if (isDefaultTask) {
this.append(lines.join('\n'), true);
}
if (disableAutoComplete) {
lastResultValue = undefined;
}
if (isDefaultTask) {
this.setFlat(flat);
}
}
if (includeTokenUsage && isDefaultTask) {
this.append(`${this.getPrefixTags({ includeTokenUsage, msg })}> define\n// token usage placeholder`);
includeTokenUsage = false;
}
}
if (prevCompletion) {
completion.unshift(...prevCompletion);
}
if (preReturnValues) {
if (returnValues) {
returnValues.unshift(...preReturnValues);
}
else {
returnValues = preReturnValues;
}
}
if (append.length) {
if (isDefaultTask) {
for (const a of append) {
this.append(a);
}
}
}
else if (lastResultValue !== undefined && autoCompleteDepth < this.maxAutoCompleteDepth && !additionalOptions?.returnOnCalled) {
return await this._completeAsync(undefined, task, additionalOptions, getCompletion, autoCompleteDepth + 1, completion, returnValues, templates, undefined, lastFnCall);
}
if (isDefaultTask && templates?.length) {
this.writeTemplates(templates, flat);
}
if (!this.disableAutoFlatten && isDefaultTask) {
this.autoFlattenAsync(false);
}
if (flat.taskTriggers?.[convoTaskTriggers.onResponse]) {
this.startSubTasks(flat, convoTaskTriggers.onResponse, getCompletion, autoCompleteDepth + 1, additionalOptions);
}
return {
status: 'complete',
message: cMsg,
messages: completion,
exe,
returnValues,
lastFnCall,
task
};
}
finally {
if (usage) {
addConvoUsageTokens(usage, this.getTokenUsage(messageStartIndex));
}
if (updateTaskCount) {
this._activeTaskCount.next(this.activeTaskCount - 1);
}
}
}
writeTemplates(templates, flat) {
for (let i = 0; i < templates.length; i++) {
const tmpl = templates[i];
if (!tmpl?.watchPath || !tmpl.message.statement?.source) {
continue;
}
const value = flat.exe.getVar(tmpl.watchPath);
if (value !== tmpl.startValue &&
(tmpl.matchValue === undefined ?
true :
(value?.toString() ?? '') === tmpl.matchValue)) {
const tags = [];
if (tmpl.message.tags) {
for (let t = 0; t < tmpl.message.tags.length; t++) {
const tag = tmpl.message.tags[t];
if (!tag || tag.name === convoTags.template) {
continue;
}
tags.push(`@${tag.name}${tag.value ? ' ' + tag.value : ''}\n`);
}
}
const tmplMsg = `@${convoTags.sourceTemplate}${tmpl.name ? ' ' + tmpl.name : ''}\n${tags.join('')}${tmpl.message.statement.source}`;
this.append(tmplMsg);
}
}
}
startSubTasks(flat, trigger, getCompletion, autoCompleteDepth, additionalOptions) {
const tasks = flat.taskTriggers?.[trigger];
if (!tasks?.length) {
return;
}
let added = false;
const remove = [];
const subs = tasks.map(task => {
const promise = this._completeAsync(undefined, task, additionalOptions, getCompletion, autoCompleteDepth);
const sub = {
name: task,
promise
};
promise.then(() => {
if (added) {
removeBehaviorSubjectAryValue(this._subTasks, sub);
}
else {
remove.push(sub);
}
});
return sub;
});
pushBehaviorSubjectAryMany(this._subTasks, subs);
added = true;
if (remove.length) {
removeBehaviorSubjectAryValueMany(this._subTasks, remove);
}
}
getReversedMappedRole(role) {
if (!role) {
return 'user';
}
for (const e in this.roleMap) {
if (this.roleMap[e] === role) {
return e;
}
}
return role;
}
getMappedRole(role) {
if (!role) {
return 'user';
}
return this.roleMap[role] ?? role;
}
createConvoExecutionContext(append = []) {
const flatExe = new ConvoExecutionContext({
conversation: this,
convoPipeSink: (value) => {
if (!(typeof value === 'string')) {
value = value?.toString();
if (!value?.trim()) {
return value;
}
}
append.push(value);
return value;
}
});
flatExe.print = this.print;
for (const e in this.defaultVars) {
flatExe.setVar(true, this.defaultVars[e], e);
}
return flatExe;
}
flattenMsg(msg, setContent) {
const flat = {
role: this.getMappedRole(msg.role),
tags: msg.tags ? convoTagsToMap(msg.tags) : undefined,
};
if (setContent) {
flat.content = msg.content;
}
if (msg.component !== undefined) {
flat.component = msg.component;
}
if (msg.sourceId !== undefined) {
flat.sourceId = msg.sourceId;
}
if (msg.sourceUrl !== undefined) {
flat.sourceUrl = msg.sourceUrl;
}
if (msg.sourceName !== undefined) {
flat.sourceName = msg.sourceName;
}
if (msg.isSuggestion !== undefined) {
flat.isSuggestion = msg.isSuggestion;
}
if (msg.renderTarget) {
flat.renderTarget = msg.renderTarget;
}
if (msg.renderOnly) {
flat.renderOnly = true;
}
if (msg.markdown) {
flat.markdown = msg.markdown;
}
if (this.userRoles.includes(flat.role)) {
flat.isUser = true;
}
if (msg.tid) {
flat.tid = msg.tid;
}
return flat;
}
async loadImportsAsync(msg) {
if (this.importMessages.includes(msg) || !msg.tags) {
return;
}
const handler = this.defaultOptions.importHandler;
if (!handler) {
throw new Error('No conversation import handler defined');
}
this.importMessages.push(msg);
const index = Math.max(0, this._messages.indexOf(msg));
for (const t of msg.tags) {
if (t.name !== convoTags.import || !t.value) {
continue;
}
await this.importAsync(t.value, index);
}
}
async importAsync(name, index) {
const handler = this.defaultOptions.importHandler;
if (!handler) {
throw new Error('No conversation import handler defined');
}
const result = await handler({ name });
if (!result) {
throw new Error(`Convo import (${name}) not found`);
}
let convo = result.convo ?? '';
if (result.type) {
convo = ('> define\n' +
asArray(result.type).map(t => `${t.name} = ${schemeToConvoTypeString(t.type)}`).join('\n') +
'\n\n' +
convo);
}
if (!convo) {
return [];
}
const r = this.parseCode(convo);
if (r.error) {
throw r.error;
}
if (r.result) {
this._messages.splice(index ?? this._messages.length, 0, ...r.result);
}
return r.result ?? [];
}
async flattenAsync(exe = this.createConvoExecutionContext(), { task = defaultConvoTask, setCurrent = task === defaultConvoTask, discardTemplates, threadFilter, } = {}) {
const isDefaultTask = task === defaultConvoTask;
const messages = [];
const edgePairs = [];
const mdVarCtx = {
indexMap: {},
vars: {},
varCount: 0,
};
exe.setVar(true, mdVarCtx.vars, convoVars.__md);
exe.loadFunctions(this._messages, this.externFunctions);
let hasNonDefaultTasks = false;
let maxTaskMsgCount = -1;
let taskTriggers;
let templates;
for (let i = 0; i < this._messages.length; i++) {
const msg = this._messages[i];
if (!msg) {
continue;
}
if (containsConvoTag(msg.tags, convoTags.import) && !this.importMessages.includes(msg)) {
await this.loadImportsAsync(msg);
i--;
continue;
}
if (msg.role === 'user' && !msg.content && !msg.statement) {
continue;
}
const template = getConvoTag(msg.tags, convoTags.template)?.value;
if (template) {
if (discardTemplates) {
continue;
}
const tmpl = parseConvoMessageTemplate(msg, template);
if (!templates) {
templates = [];
}
templates.push(tmpl);
continue;
}
threadFilter = this.getThreadFilter(exe, threadFilter);
if (threadFilter && !isConvoThreadFilterMatch(threadFilter, msg.tid)) {
continue;
}
const flat = this.flattenMsg(msg, false);
const setMdVars = (this.defaultOptions.setMarkdownVars ||
containsConvoTag(msg.tags, convoTags.markdownVars));
const shouldParseMd = (setMdVars ||
this.defaultOptions.parseMarkdown ||
containsConvoTag(msg.tags, convoTags.markdown));
const msgTask = getConvoTag(msg.tags, convoTags.task)?.value ?? defaultConvoTask;
if (msgTask !== defaultConvoTask) {
hasNonDefaultTasks = true;
flat.task = msgTask;
if (isDefaultTask) {
const trigger = getConvoTag(msg.tags, convoTags.taskTrigger)?.value;
if (trigger) {
if (!taskTriggers) {
taskTriggers = {};
}
const ary = taskTriggers[trigger] ?? (taskTriggers[trigger] = []);
if (!ary.includes(msgTask)) {
ary.push(msgTask);
}
}
}
}
if (msgTask === task) {
const maxTasks = getConvoTag(msg.tags, convoTags.maxTaskMessageCount)?.value;
if (maxTasks) {
maxTaskMsgCount = safeParseNumber(maxTasks, maxTaskMsgCount);
}
}
if (msg.fn) {
if (msg.fn.local || msg.fn.call) {
continue;
}
else if (msg.fn.topLevel) {
exe.clearSharedSetters();
const r = exe.executeFunction(msg.fn);
if (r.valuePromise) {
await r.valuePromise;
}
if (exe.sharedSetters.length) {
const varSetter = {
role: msg.role ?? 'define',
};
varSetter.setVars = {};
for (const v of exe.sharedSetters) {
varSetter.setVars[v] = exe.getVar(v);
}
messages.push(varSetter);
}
const prev = this._messages[i - 1];
if (msg.role === 'result' && prev?.fn?.call) {
flat.role = 'function';
flat.called = prev.fn;
flat.calledReturn = exe.getVarEx(convoResultReturnName, undefined, undefined, false);
flat.calledParams = exe.getConvoFunctionArgsValue(prev.fn);
if (prev.tags) {
flat.tags = flat.tags ? { ...convoTagsToMap(prev.tags), ...flat.tags } : convoTagsToMap(prev.tags);
}
}
else {
continue;
}
}
else {
flat.role = 'function';
flat.fn = msg.fn;
flat.fnParams = exe.getConvoFunctionArgsScheme(msg.fn);
}
}
else if (msg.statement) {
if (containsConvoTag(msg.tags, convoTags.edge)) {
flat.edge = true;
edgePairs.push({ flat, msg: msg, shouldParseMd, setMdVars });
}
else {
await flattenMsgAsync(exe, msg.statement, flat, shouldParseMd);
}
}
else if (msg.content !== undefined) {
if (containsConvoTag(msg.tags, convoTags.concat)) {
const prev = messages[messages.length - 1];
if (prev?.content !== undefined) {
const tag = getConvoTag(msg.tags, convoTags.condition);
if (tag?.value && !this.isTagConditionTrue(exe, tag.value)) {
continue;
}
prev.content += '\n\n' + msg.content;
continue;
}
}
flat.content = msg.content;
}
else {
continue;
}
messages.push(flat);
if (!flat.edge) {
this.applyTagsAndState(msg, flat, messages, exe, setMdVars, mdVarCtx);
}
}
for (const pair of edgePairs) {
if (pair.msg.statement) {
await flattenMsgAsync(exe, pair.msg.statement, pair.flat, pair.shouldParseMd);
}
this.applyTagsAndState(pair.msg, pair.flat, messages, exe, pair.setMdVars, mdVarCtx);
}
const ragStr = exe.getVar(convoVars.__rag);
const ragMode = isConvoRagMode(ragStr) ? ragStr : undefined;
this.applyRagMode(messages, ragMode);
let capabilities = [...this.serviceCapabilities, ...this.getMessageListCapabilities(messages)];
if (!isDefaultTask) {
capabilities = capabilities.filter(c => c !== 'vision');
}
if (capabilities.includes('vision') && isDefaultTask) {
const systemMessage = messages.find(m => m.role === 'system');
const content = exe.getVar(convoVars.__visionServiceSystemMessage, null, defaultConvoVisionSystemMessage);
if (systemMessage) {
systemMessage.content = ((systemMessage.content ? systemMessage.content + '\n\n' : '') +
content);
}
else {
messages.unshift({
role: 'system',
content
});
}
}
const shouldDebug = this.shouldDebug(exe);
const debug = shouldDebug ? (this.debug ?? this.debugToConversation) : undefined;
if (shouldDebug) {
exe.print = (...args) => {
debug?.(...args);
return defaultConvoPrintFunction(...args);
};
}
if (!isDefaultTask || hasNonDefaultTasks) {
if (isDefaultTask) {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if ((msg?.task ?? defaultConvoTask) !== defaultConvoTask) {
messages.splice(i, 1);
i--;
}
}
}
else {
const taskMsgs = [];
let taskHasSystem = false;
let otherMsgCount = 0;
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (!msg) {
continue;
}
if (msg.task === task) {
if (msg.role === 'system') {
taskHasSystem = true;
}
taskMsgs.push(msg);
messages.splice(i, 1);
i--;
}
else if (msg.fn || msg.called) {
messages.splice(i, 1);
i--;
}
else if (msg.role !== 'system') {
otherMsgCount++;
}
}
if (taskHasSystem) {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (msg?.role === 'system') {
messages.splice(i, 1);
i--;
}
}
}
if (maxTaskMsgCount !== -1 && otherMsgCount > maxTaskMsgCount) {
let index = 0;
while (otherMsgCount > maxTaskMsgCount && index < messages.length) {
const msg = messages[index];
if (!msg || msg.role === 'system') {
index++;
continue;
}
messages.splice(index, 1);
otherMsgCount--;
}
}
for (let i = 0; i < taskMsgs.length; i++) {
const msg = taskMsgs[i];
if (msg) {
messages.push(msg);
}
}
}
}
if (templates) {
for (let i = 0; i < templates.length; i++) {
const tmpl = templates[i];
if (!tmpl || !tmpl.watchPath) {
continue;
}
tmpl.startValue = exe.getVar(tmpl.watchPath);
}
}
const flat = {
exe,
vars: exe.getUserSharedVars(),
messages,
conversation: this,
task,
taskTriggers,
templates,
debug,
capabilities,
markdownVars: mdVarCtx.vars,
ragMode
};
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (!msg) {
continue;
}
switch (msg.ro