UNPKG

@convo-lang/convo-lang

Version:
1,188 lines (1,187 loc) 173 kB
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