@convo-lang/convo-lang
Version:
The language of AI
1,188 lines (1,187 loc) • 173 kB
JavaScript
import { CancelToken, aryRemoveItem, asArray, asArrayItem, createJsonRefReplacer, deepClone, delayAsync, deleteUndefined, getDirectoryName, getErrorMessage, getObjKeyCount, getValueByPath, isClassInstanceObject, isRooted, joinPaths, log, normalizePath, parseBoolean, parseMarkdown, pushBehaviorSubjectAry, pushBehaviorSubjectAryMany, removeBehaviorSubjectAryValue, removeBehaviorSubjectAryValueMany, safeParseNumber, safeParseNumberOrUndefined, shortUuid, starStringToRegex } from "@iyio/common";
import { parseJson5 } from "@iyio/json5";
import { BehaviorSubject, Subject } from "rxjs";
import { ConvoError } from "./ConvoError.js";
import { ConvoExecutionContext } from "./ConvoExecutionContext.js";
import { ConvoRoom } from "./ConvoRoom.js";
import { HttpConvoCompletionService } from "./HttpConvoCompletionService.js";
import { requireParseConvoCached } from "./convo-cached-parsing.js";
import { applyConvoModelConfigurationToInputAsync, applyConvoModelConfigurationToOutput, completeConvoUsingCompletionServiceAsync, convertConvoInput, getConvoCompletionServiceAsync, requireConvertConvoOutput } from "./convo-completion-lib.js";
import { getConvoMessageComponent } from "./convo-component-lib.js";
import { evalConvoMessageAsCodeAsync } from "./convo-eval.js";
import { getGlobalConversationLock } from "./convo-lang-lock.js";
import { addConvoUsageTokens, appendFlatConvoMessageSuffix, containsConvoTag, contentHasConvoRole, convertFlatConvoMessageToCompletionMessage, convoDescriptionToComment, convoDisableAutoCompleteName, convoFunctions, convoImportModifiers, convoLabeledScopeParamsToObj, convoMessageToString, convoMsgModifiers, convoPartialUsageTokensToUsage, convoRagDocRefToMessage, convoResultReturnName, convoRoles, convoScopedModifiers, convoStdImportPrefix, convoStringToComment, convoTagMapToCode, convoTags, convoTagsToMap, convoTaskTriggers, convoUsageTokensToString, convoVars, createEmptyConvoTokenUsage, defaultConversationName, defaultConvoCacheType, defaultConvoImportServicePriority, defaultConvoPrintFunction, defaultConvoRagTol, defaultConvoTask, defaultConvoTransformGroup, defaultConvoVisionSystemMessage, escapeConvo, escapeConvoMessageContent, evalConvoTransformCondition, findConvoMessage, formatConvoContentSpace, formatConvoMessage, getAssumedConvoCompletionValue, getConvoCompletionServiceModelsAsync, getConvoDateString, getConvoDebugLabelComment, getConvoStructPropertyCount, getConvoTag, getFlatConvoMessageCachedJsonValue, getFlatConvoMessageCondition, getFlatConvoTagBoolean, getFlatConvoTagValues, getFlattenConversationDisplayString, getFullFlatConvoMessageContent, getLastCalledConvoMessage, getLastCompletionMessage, insertConvoContentIntoSlot, isConvoThreadFilterMatch, isValidConvoIdentifier, mapToConvoTags, parseConvoJsonMessage, parseConvoMessageTemplate, parseConvoTransformTag, setFlatConvoMessageCachedJsonValue, setFlatConvoMessageCondition, spreadConvoArgs, validateConvoFunctionName, validateConvoTypeName, validateConvoVarName } from "./convo-lib.js";
import { parseConvoCode } from "./convo-parser.js";
import { defaultConvoRagServiceCallback } from "./convo-rag-lib.js";
import { convoStdImportHandler } from "./convo-std-imports.js";
import { convoScript } from "./convo-template.js";
import { allConvoMessageModificationAction, baseConvoToolChoice, convoImportMatchRegKey, convoMessageSourcePathKey, convoObjFlag, isConvoCapability, isConvoMessageModification, isConvoMessageModificationAction, isConvoRagMode, isConvoReasoningEffort, isConvoResponseVerbosity } from "./convo-types.js";
import { schemeToConvoTypeString, zodSchemeToConvoTypeString } from "./convo-zod.js";
import { convoCacheService, convoCompletionService, convoConversationConverterProvider, convoDefaultModelParam, convoImportService } from "./convo.deps.js";
import { isConvoObject } from "./convoAsync.js";
import { createConvoVisionFunction } from "./createConvoVisionFunction.js";
import { convoScopeFunctionEvalJavascript } from "./scope-functions/convoScopeFunctionEvalJavascript.js";
let nextInstanceId = 1;
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 onImportSource() { return this._onImportSource; }
get openTasksSubject() { return this._openTasks; }
get openTasks() { return this._openTasks.value; }
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; }
get sandboxMode() { return this.defaultOptions.sandboxMode ?? false; }
constructor(options = {}) {
this._convo = [];
this._messages = [];
this._onAppend = new Subject();
this._onImportSource = new Subject();
this._openTasks = new BehaviorSubject([]);
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.
*
* @note shares the same functionality as defaultVars. Maybe remove
*/
this.unregisteredVars = {};
this.externFunctions = {};
this.print = defaultConvoPrintFunction;
/**
* Sub conversations
*/
this.subs = {};
this._isDisposed = false;
this._disposeToken = new CancelToken();
this._defaultApiKey = null;
this.enabledCapabilities = [];
this.appendAfterCall = [];
this.agents = [];
this.autoFlattenId = 0;
this.autoFlatPromiseRef = null;
this.modelServiceMap = {};
this.endpointModelServiceMap = {};
this.httpEndpointServices = {};
this._onCompletionStart = new Subject();
this._isCompleting = new BehaviorSubject(false);
this.importMessages = [];
this.importedModules = {};
this.debugToConversation = (...args) => {
this.appendArgsAsComment('debug', args);
};
this.definitionItems = [];
this.preAssignMessages = [];
this.onComponentSubmission = new Subject();
const { name = defaultConversationName, isAgent = false, room = new ConvoRoom(), userRoles = ['user'], assistantRoles = ['assistant'], systemRoles = ['system'], roleMap = {}, completionService = convoCompletionService.all(), converters = convoConversationConverterProvider.all(), defaultModel = convoDefaultModelParam.get(), capabilities = [], serviceCapabilities = [], maxAutoCompleteDepth = 100, trackTime = false, trackTokens = false, trackModel = false, onTokenUsage, disableAutoFlatten = false, disableTransforms = false, autoFlattenDelayMs = 30, ragCallback, debug, debugMode, disableMessageCapabilities = false, initConvo, defaultVars, onConstructed, define, onComponentMessages, componentCompletionCallback, externFunctions, components, externScopeFunctions, getStartOfConversation, cache, logFlat = false, logFlatCached = false, usage = createEmptyConvoTokenUsage(), beforeCreateExeCtx, modules, childDepth = 0, disableTriggers = false, inlineHost, inlinePrompt, } = options;
this.instanceId = nextInstanceId++;
this.name = name;
this.usage = usage;
this.isAgent = isAgent;
this.defaultOptions = options;
this.childDepth = childDepth;
this.disableTriggers = disableTriggers;
this.beforeCreateExeCtx = beforeCreateExeCtx;
this.getStartOfConversation = getStartOfConversation;
this.inlineHost = inlineHost;
this.inlinePrompt = inlinePrompt;
this.cache = typeof cache === 'boolean' ? (cache ? [convoCacheService()] : []) : asArray(cache);
this.logFlat = logFlat;
this.logFlatCached = logFlatCached;
this.defaultVars = defaultVars ? defaultVars : {};
this.userRoles = [...userRoles];
this.assistantRoles = [...assistantRoles];
this.systemRoles = [...systemRoles];
this.roleMap = roleMap;
if (Array.isArray(completionService) && completionService.length === 1) {
this.completionService = completionService[0];
}
else {
this.completionService = completionService;
}
this.converters = converters;
this.capabilities = [...capabilities];
this.defaultModel = defaultModel ?? undefined;
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._onTokenUsage = onTokenUsage;
this.disableAutoFlatten = disableAutoFlatten;
this.disableTransforms = disableTransforms;
this.autoFlattenDelayMs = autoFlattenDelayMs;
this.componentCompletionCallback = componentCompletionCallback;
this.ragCallback = ragCallback;
this.debug = debug;
if (debugMode) {
this.debugMode = true;
}
this.room = room;
this.room.addConversation(this);
if (initConvo) {
this.append(initConvo, true);
}
if (define) {
this.define(define);
}
if (onComponentMessages) {
if (Array.isArray(onComponentMessages)) {
for (const cb of onComponentMessages) {
this.watchComponentMessages(cb);
}
}
else {
this.watchComponentMessages(onComponentMessages);
}
}
this.components = { ...components };
this.modules = modules ? [...modules] : [];
if (externFunctions) {
for (const e in externFunctions) {
const f = externFunctions[e];
if (f) {
this.implementExternFunction(e, f);
}
}
}
if (externScopeFunctions) {
for (const e in externScopeFunctions) {
const f = externScopeFunctions[e];
if (f) {
this.externFunctions[e] = f;
}
}
}
onConstructed?.(this);
}
initMessageReady() {
const last = this.getLastMessage();
return last?.role === convoRoles.user && containsConvoTag(last.tags, convoTags.init) ? true : false;
}
addTask(task) {
this._activeTaskCount.next(this._activeTaskCount.value + 1);
pushBehaviorSubjectAry(this._openTasks, task);
const removeFromHost = this.inlineHost?.addTask(task);
let removed = false;
return () => {
if (removed) {
return;
}
removed = true;
this._activeTaskCount.next(this._activeTaskCount.value - 1);
removeBehaviorSubjectAryValue(this._openTasks, task);
removeFromHost?.();
};
}
popTask() {
const task = this._openTasks.value[this._openTasks.value.length - 1];
if (task) {
removeBehaviorSubjectAryValue(this._openTasks, task);
this._activeTaskCount.next(this._activeTaskCount.value - 1);
if (this.inlineHost?._openTasks.value.includes(task)) {
removeBehaviorSubjectAryValue(this.inlineHost._openTasks, task);
this.inlineHost._activeTaskCount.next(this.inlineHost._activeTaskCount.value - 1);
}
}
}
watchComponentMessages(callback) {
return this._flat.subscribe(flat => {
if (!flat) {
return;
}
const all = flat.messages.filter(m => m.component);
const state = {
last: all[all.length - 1],
all,
flat,
convo: this
};
if (typeof callback === 'function') {
callback(state);
}
else {
callback.next(state);
}
});
}
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');
}
break;
case convoTags.enabledVisionFunction:
if (!capList?.includes('visionFunction')) {
if (!capList) {
capList = [];
}
capList.push('visionFunction');
}
break;
}
}
if (!capList) {
return undefined;
}
for (const cap of capList) {
this.enableCapability(cap);
}
return capList;
}
get disposeToken() { return this._disposeToken; }
get isDisposed() { return this._isDisposed; }
dispose() {
if (this._isDisposed) {
return;
}
this.autoFlattenId++;
this._isDisposed = true;
this._disposeToken.cancelNow();
this.room.removeConversation(this);
}
setDefaultApiKey(key) {
this._defaultApiKey = key;
}
getDefaultApiKey() {
if (typeof this._defaultApiKey === 'function') {
return this._defaultApiKey();
}
else {
return this._defaultApiKey;
}
}
parseCode(code) {
return parseConvoCode(code, { parseMarkdown: this.defaultOptions.parseMarkdown, logErrors: true });
}
enableCapability(cap) {
if (this.enabledCapabilities.includes(cap)) {
return;
}
this.enabledCapabilities.push(cap);
switch (cap) {
case 'visionFunction':
this.define({
hidden: true,
fn: createConvoVisionFunction()
}, true);
break;
case 'vision':
if (!this.serviceCapabilities.includes("vision")) {
this.serviceCapabilities.push('vision');
}
break;
}
}
autoUpdateCompletionService() {
if (!this.completionService) {
this.completionService = convoCompletionService.get();
}
}
createChild(options) {
const convo = new Conversation(this.getCloneOptions(options));
if (this._defaultApiKey) {
convo.setDefaultApiKey(this._defaultApiKey);
}
return convo;
}
getCloneOptions(options) {
return {
...this.defaultOptions,
childDepth: this.childDepth + 1,
debug: this.debugToConversation,
debugMode: this.shouldDebug(),
beforeCreateExeCtx: this.beforeCreateExeCtx,
...options,
defaultVars: { ...this.defaultVars, ...options?.defaultVars },
externScopeFunctions: { ...this.externFunctions },
components: { ...this.components },
modules: [...this.modules],
};
}
/**
* Creates a new Conversation and appends the messages of this conversation to the newly
* created conversation.
*/
clone({ inlinePrompt, triggerName, empty = (inlinePrompt && !inlinePrompt.extend && !inlinePrompt.continue), noFunctions, systemOnly, removeAgents, dropLast = inlinePrompt?.dropLast, dropUntilContent = inlinePrompt ? true : false, last = inlinePrompt?.last, cloneConvoString, } = {}, convoOptions) {
const cloneOptions = this.getCloneOptions(convoOptions);
if (inlinePrompt) {
delete cloneOptions.debug;
cloneOptions.inlinePrompt = inlinePrompt;
cloneOptions.inlineHost = this;
cloneOptions.disableTriggers = true;
cloneOptions.disableAutoFlatten = true;
}
const conversation = new Conversation(cloneOptions);
if (this._defaultApiKey) {
conversation.setDefaultApiKey(this._defaultApiKey);
}
let messages = empty ? [] : [...this.messages];
if (inlinePrompt && triggerName) {
this.filterConvoMessagesForTrigger(inlinePrompt, triggerName, messages);
}
if (noFunctions) {
messages = messages.filter(m => !m.fn || m.fn.topLevel);
}
if (systemOnly) {
messages = messages.filter(m => m.role === 'system' || m.fn?.topLevel || m.fn?.name === convoFunctions.getState);
}
if (removeAgents) {
messages = messages.filter(m => m.tags?.some(t => t.name === convoTags.agentSystem) || (m.fn && this.agents.some(a => a.name === m.fn?.name)));
for (const agent of this.agents) {
delete conversation.defaultVars[agent.name];
}
}
else {
for (const agent of this.agents) {
conversation.agents.push(agent);
}
}
for (const name in this.importedModules) {
conversation.importedModules[name] = this.importedModules[name];
}
if (dropUntilContent) {
while (messages.length && !this.isContentMessage(messages[messages.length - 1])) {
messages.pop();
}
}
if (dropLast !== undefined) {
messages.splice(messages.length - dropLast, dropLast);
}
if (last !== undefined) {
messages.splice(0, messages.length - last);
}
if (triggerName) {
messages.push(...requireParseConvoCached(`> define\n${convoFunctions.clearRag}()`));
}
conversation.appendMessageObject(messages, { disableAutoFlatten: true, appendCode: cloneConvoString });
return conversation;
}
filterConvoMessagesForTrigger(prompt, triggerName, messages) {
const { system, functions, } = prompt;
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (!msg) {
messages.splice(i, 1);
i--;
continue;
}
const exclude = getConvoTag(msg.tags, convoTags.excludeFromTriggers);
const include = getConvoTag(msg.tags, convoTags.includeInTriggers);
if (((!system && this.isSystemMessage(msg)) ||
(!functions && msg.fn && !msg.fn.call && !msg.fn.topLevel) ||
(exclude && (exclude.value === undefined || exclude.value === triggerName))) && !((include && (include.value === undefined || include.value === triggerName)))) {
messages.splice(i, 1);
i--;
continue;
}
}
}
/**
* 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 });
}
appendMsgsAry(messages, index = this._messages.length) {
let depth = 0;
let subName;
let subs;
let endRole;
let subType;
let head;
let imp;
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (!msg) {
continue;
}
if (msg.role === endRole) {
depth--;
if (!depth) {
const sub = this.room.state.lookup[subName ?? ''] ?? this.createChild({ room: this.room });
for (const agent of this.agents) {
delete sub.externFunctions[agent.name];
}
if (head) {
if (!imp) {
imp = [];
}
switch (subType) {
case convoMsgModifiers.agent:
imp.push({ subs: subs ?? [], subType: subType, head, convo: sub });
break;
}
}
endRole = undefined;
subName = undefined;
subs = undefined;
subType = undefined;
head = undefined;
continue;
}
}
else if (msg.fn) {
const mod = msg.fn.modifiers?.find(m => convoScopedModifiers.includes(m));
if (mod) {
depth++;
if (depth === 1) {
subType = mod;
endRole = mod + 'End';
subName = msg.fn.name;
subs = [];
head = msg;
continue;
}
}
}
if (subs) {
subs.push(msg);
}
else {
this._messages.splice(index, 0, msg);
index++;
}
}
if (depth) {
throw new Error(`Sub-conversation not ended - name=${subName}`);
}
if (imp) {
for (const i of imp) {
switch (i.subType) {
case 'agent':
this.defineAgent(i.convo, i.head, i.subs);
}
}
}
}
defineAgent(conversation, headMsg, messages) {
headMsg = { ...headMsg };
if (!headMsg.fn) {
return;
}
const hasAgentSystem = this._messages.some(m => m.tags?.some(t => t.name === convoTags.agentSystem));
if (!hasAgentSystem) {
this.append(`@${convoTags.agentSystem}\n> system\nYou can use the following agents to assistant the user.\n` +
'{{getAgentList()}}\n\n' +
'To send a request to an agent either call the function with the same name as the agent or ' +
'call a more specialized function that starts with the agents name, but DO NOT call both.');
}
const agent = {
name: headMsg.fn.name === this.name ? this.name + '_2' : headMsg.fn.name,
description: headMsg.description,
main: headMsg,
capabilities: [],
functions: []
};
if (headMsg.tags) {
for (const tag of headMsg.tags) {
if (tag.name === convoTags.cap && tag.value) {
agent.capabilities?.push(tag.value);
}
}
}
headMsg.fn = { ...headMsg.fn };
headMsg.fn.modifiers = [...headMsg.fn.modifiers];
headMsg.fn.local = true;
aryRemoveItem(headMsg.fn.modifiers, convoMsgModifiers.agent);
messages.push(headMsg);
const proxyFn = { ...headMsg };
if (proxyFn.fn) {
proxyFn.fn = { ...proxyFn.fn };
delete proxyFn.fn.body;
proxyFn.fn.extern = true;
proxyFn.fn.local = false;
this.appendMessageObject(proxyFn);
this.externFunctions[agent.name] = async (scope) => {
if (!headMsg.fn) {
return null;
}
const clone = conversation.clone();
let r = await clone.callFunctionAsync(headMsg.fn.name, convoLabeledScopeParamsToObj(scope), { returnOnCalled: true });
if (!r) {
return r;
}
if (typeof r === 'object') {
const keys = Object.keys(r);
if (keys.length === 1) {
r = r[keys[0] ?? ''];
}
if (!r) {
return r;
}
}
const sub = clone.onAppend.subscribe(a => {
this.appendAfterCall.push(a.text.replace(/(^|\n)\s*>/g, text => `\n@cid ${agent.name}\n${text}`));
// todo - append
});
try {
clone.append(convoScript `> user\n${r}`);
const completion = await clone.completeAsync();
const returnValue = completion.message?.callParams ?? completion.message?.content;
if (typeof returnValue === 'string') {
return `${agent.name}'s response:\n<agent-response>\n${returnValue}\n</agent-response>`;
}
else {
return returnValue;
}
}
finally {
sub.unsubscribe();
}
};
}
conversation.appendMessageObject(messages);
for (const msg of messages) {
if (!msg.fn || msg === headMsg || !msg.fn.modifiers.includes('public')) {
continue;
}
// add to description - When call the agent "Max" will handle the execution of the function.
// create proxy
}
// create proxy for any public functions
this.agents.push(agent);
}
/**
* Appends new messages to the conversation and by default does not add code to the conversation.
*/
appendMessageObject(message, { disableAutoFlatten, appendCode, source, } = {}) {
const messages = asArray(message);
this.appendMsgsAry(messages);
if (source) {
if (this._beforeAppend.observed) {
source = this.transformMessageBeforeAppend(source);
}
this._convo.push(source);
}
else if (appendCode) {
for (const msg of messages) {
source = convoMessageToString(msg);
if (this._beforeAppend.observed) {
source = this.transformMessageBeforeAppend(source);
}
this._convo.push(source);
}
}
this._onAppend.next({
text: source ?? '',
messages,
});
if (!this.disableAutoFlatten && !disableAutoFlatten) {
this.autoFlattenAsync(false);
}
}
transformMessageBeforeAppend(messages) {
const append = {
text: messages,
messages: []
};
this._beforeAppend.next(append);
return append.text;
}
appendDefineVars(vars) {
const convo = [`> define`];
for (const name in vars) {
const value = vars[name];
if (!isValidConvoIdentifier(name)) {
throw new Error(`Invalid var name - ${name}`);
}
convo.push(`${name} = ${value === undefined ? undefined : JSON.stringify(value, null, 4)}`);
}
this.append(convo.join('\n'));
}
appendDefineVar(name, value) {
return this.appendDefineVars({ [name]: value });
}
append(messages, mergeWithPrevOrOptions = false, _throwOnError = true) {
const options = (typeof mergeWithPrevOrOptions === 'object') ? mergeWithPrevOrOptions : { mergeWithPrev: mergeWithPrevOrOptions };
const { mergeWithPrev = false, throwOnError = _throwOnError, disableAutoFlatten, addTags, } = options;
if (isConvoObject(messages)) {
const outputOptions = messages.getOutputOptions();
for (const e in outputOptions.defaultVars) {
const v = outputOptions.defaultVars[e];
if (v === undefined) {
continue;
}
this.defaultVars[e] = v;
}
if (outputOptions.externFunctions) {
for (const name in outputOptions.externFunctions) {
const fn = outputOptions.externFunctions[name];
if (!fn) {
continue;
}
this.implementExternFunction(name, fn);
}
}
if (outputOptions.externScopeFunctions) {
for (const name in outputOptions.externScopeFunctions) {
const fn = outputOptions.externScopeFunctions[name];
if (!fn) {
continue;
}
this.externFunctions[name] = fn;
}
}
messages = messages.getInput();
}
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 (options.filePath && r.result) {
for (const m of r.result) {
m[convoMessageSourcePathKey] = options.filePath;
}
}
if (addTags?.length && r.result) {
for (const m of r.result) {
if (m.tags) {
m.tags.push(...addTags);
}
else {
m.tags = [...addTags];
}
}
}
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) {
this.appendMsgsAry(r.result);
}
this._onAppend.next({
text: messages,
messages: r.result ?? []
});
if (!this.disableAutoFlatten && !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);
}
getLastMessage() {
return this.messages[this.messages.length - 1];
}
getLastUserMessage(messages) {
if (!messages) {
return undefined;
}
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg && this.isUserMessage(msg)) {
return msg;
}
}
return undefined;
}
getLastUserOrThinkingMessage(messages) {
if (!messages) {
return undefined;
}
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg && this.isUserOrThinkingMessage(msg)) {
return msg;
}
}
return 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.assignTo} ${msg.assignTo}\n`;
}
return tags;
}
async getCompletionServiceAsync(flat) {
const convoEndpoint = flat.exe.getVar(convoVars.__convoEndpoint);
const serviceAndModel = await getConvoCompletionServiceAsync(flat, (convoEndpoint ?
[this.getHttpService(convoEndpoint)]
: this.completionService ?
asArray(this.completionService)
:
[]), false, convoEndpoint ?
(this.endpointModelServiceMap[convoEndpoint] ?? (this.endpointModelServiceMap[convoEndpoint] = {})) :
this.modelServiceMap);
return serviceAndModel;
}
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, returnOnCalled: true }, 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();
}
async completeAsync(appendOrOptions, optionsForAwaitable) {
if (isConvoObject(appendOrOptions)) {
this.append(appendOrOptions);
const completion = await this.completeAsync(optionsForAwaitable);
return getAssumedConvoCompletionValue(completion);
}
if (typeof appendOrOptions === 'string') {
this.append(appendOrOptions);
appendOrOptions = undefined;
}
const modelInputOutput = appendOrOptions?.modelInputOutput;
if (appendOrOptions?.append) {
this.append(appendOrOptions.append);
}
if (appendOrOptions?.debug) {
console.info('Conversation.completeAsync:\n', appendOrOptions.append);
}
const result = await this.tryCompleteAsync(appendOrOptions?.task, appendOrOptions, async (flat) => {
return await this.completeWithServiceAsync(flat, modelInputOutput);
});
if (appendOrOptions?.debug) {
console.info('Conversation.completeAsync Result:\n', result.messages ? (result.messages.length === 1 ? result.messages[0] : result.messages) : result);
}
return result;
}
getHttpService(endpoint) {
return this.httpEndpointServices[endpoint] ?? (this.httpEndpointServices[endpoint] =
new HttpConvoCompletionService({ endpoint }));
}
async completeWithServiceAsync(flat, modelInputOutput) {
//@@with-service
const convoEndpoint = flat.exe.getVar(convoVars.__convoEndpoint);
const serviceAndModel = await getConvoCompletionServiceAsync(flat, (convoEndpoint ?
[this.getHttpService(convoEndpoint)]
: this.completionService ?
asArray(this.completionService)
:
[]), true, convoEndpoint ?
(this.endpointModelServiceMap[convoEndpoint] ?? (this.endpointModelServiceMap[convoEndpoint] = {})) :
this.modelServiceMap);
const lastMsg = flat.messages[flat.messages.length - 1];
const cacheType = ((lastMsg?.tags && (convoTags.cache in lastMsg.tags) && (lastMsg.tags[convoTags.cache] ?? defaultConvoCacheType)) ||
flat.exe.getVar(convoVars.__cache));
if (this.logFlatCached) {
console.info(getFlattenConversationDisplayString(flat, true));
}
let cache = cacheType ? this.cache?.find(c => c.cacheType === cacheType) : this.cache?.[0];
if (!cache && (cacheType === true || cacheType === defaultConvoCacheType)) {
cache = convoCacheService();
}
if (cache?.getCachedResponse) {
const cached = await cache.getCachedResponse(flat);
if (cached) {
return cached;
}
}
if (this.logFlat) {
console.info(getFlattenConversationDisplayString(flat, true));
}
if (!serviceAndModel) {
return [];
}
this.debug?.('To be completed', flat.messages);
const triggerName = flat.exe.getVar(convoVars.__trigger);
if (this.inlineHost) {
const last = this.getLastUserOrThinkingMessage(flat.messages);
if (last) {
this.inlineHost.append(`> ${convoRoles.thinking}${triggerName ? ' ' + triggerName : ''} ${last.role} (${this.inlinePrompt?.header})\n${escapeConvo(getFullFlatConvoMessageContent(last))}`, { disableAutoFlatten: true });
}
if (flat.exe.getVar(convoVars.__debugInline)) {
this.inlineHost.appendArgsAsComment('debug thinking', flat.messages, true);
}
}
let configInputResult;
if (serviceAndModel.model) {
configInputResult = await applyConvoModelConfigurationToInputAsync(serviceAndModel.model, flat, this);
}
let messages;
const lock = getGlobalConversationLock();
const release = await lock?.waitOrCancelAsync(this._disposeToken);
if (lock && !release) {
return [];
}
try {
if (modelInputOutput !== undefined) {
messages = requireConvertConvoOutput(modelInputOutput.output, serviceAndModel.service.outputType, modelInputOutput.input, serviceAndModel.service.inputType, this.converters, flat);
}
else {
messages = await completeConvoUsingCompletionServiceAsync(flat, serviceAndModel.service, this.converters);
}
}
finally {
release?.();
}
this.debug?.('Completion message', messages);
if (serviceAndModel.model && configInputResult) {
applyConvoModelConfigurationToOutput(serviceAndModel.model, flat, messages, configInputResult);
}
if (this.inlineHost) {
this.inlineHost.append(messages.map(m => `> ${convoRoles.thinking}${triggerName ? ' ' + triggerName : ''} ${m.role}\n${escapeConvo(m.content)}`), { disableAutoFlatten: true });
if (flat.exe.getVar(convoVars.__debugInline)) {
this.inlineHost.appendArgsAsComment('debug thinking response', messages, true);
}
}
if (cache?.cachedResponse) {
await cache.cachedResponse(flat, messages);
}
return messages;
}
/**
* 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;
}
}
/**
* Occurs at the start of a public completion.
*/
get onCompletionStart() { return this._onCompletionStart; }
/**
* 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 {
const completionPromise = this._completeAsync(undefined, true, additionalOptions?.usage, task, additionalOptions, getCompletion, autoCompleteDepth, prevCompletion, preReturnValues);
this._onCompletionStart.next({ convo: this, completionPromise, options: additionalOptions, task });
return await completionPromise;
}
finally {
this._isCompleting.next(false);
}
}
}
async completeParallelAsync(flat, options) {
const messages = flat.parallelMessages;
if (!messages || messages.length < 2) {
return undefined;
}
const startIndex = this.messages.indexOf(messages[0]);
if (startIndex === -1) {
return undefined;
}
const c = await this.completeParallelMessagesAsync(messages, flat.messages.slice(flat.messages.length - messages.length).map(m => m.label), startIndex, options, flat.queueRef ? true : false);
return c;
}
async getModelsAsync(serviceOrId) {
const service = (typeof serviceOrId === 'string') ? asArray(this.completionService)?.find(s => s.serviceId === serviceOrId) : serviceOrId;
if (!service) {
return [];
}
return await getConvoCompletionServiceModelsAsync(service);
}
async getAllModelsAsync() {
if (!this.completionService) {
return [];
}
const models = [];
const ary = asArray(this.completionService);
for (const s of ary) {
const m = await getConvoCompletionServiceModelsAsync(s);
models.push(...m);
}
return models;
}
async completeParallelMessagesAsync(messages, labels, startIndex, options, inQueue) {
const all = await Promise.all(messag