UNPKG

@convo-lang/convo-lang

Version:
1,259 lines (1,253 loc) 148 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Conversation = void 0; const common_1 = require("@iyio/common"); const json5_1 = require("@iyio/json5"); const rxjs_1 = require("rxjs"); const ConvoError_1 = require("./ConvoError"); const ConvoExecutionContext_1 = require("./ConvoExecutionContext"); const ConvoRoom_1 = require("./ConvoRoom"); const convo_component_lib_1 = require("./convo-component-lib"); const convo_eval_1 = require("./convo-eval"); const convo_lang_lock_1 = require("./convo-lang-lock"); const convo_lib_1 = require("./convo-lib"); const convo_parser_1 = require("./convo-parser"); const convo_template_1 = require("./convo-template"); const convo_types_1 = require("./convo-types"); const convo_zod_1 = require("./convo-zod"); const convo_deps_1 = require("./convo.deps"); const createConvoVisionFunction_1 = require("./createConvoVisionFunction"); const convoScopeFunctionEvalJavascript_1 = require("./scope-functions/convoScopeFunctionEvalJavascript"); class Conversation { _convo = []; get convo() { return this._convo.join('\n\n'); } getConvoStrings() { return [...this._convo]; } name; room; isAgent; _messages = []; get messages() { return this._messages; } _onAppend = new rxjs_1.Subject(); get onAppend() { return this._onAppend; } _openTasks = new rxjs_1.BehaviorSubject([]); get openTasksSubject() { return this._openTasks; } get openTasks() { return this._openTasks.value; } _activeTaskCount = new rxjs_1.BehaviorSubject(0); get activeTaskCountSubject() { return this._activeTaskCount; } get activeTaskCount() { return this._activeTaskCount.value; } _trackTime; get trackTimeSubject() { return this._trackTime; } get trackTime() { return this._trackTime.value; } set trackTime(value) { if (value == this._trackTime.value) { return; } this._trackTime.next(value); } _trackTokens; get trackTokensSubject() { return this._trackTokens; } get trackTokens() { return this._trackTokens.value; } set trackTokens(value) { if (value == this._trackTokens.value) { return; } this._trackTokens.next(value); } _onTokenUsage; /** * Tracks the token usages of the Conversation */ usage; _trackModel; get trackModelSubject() { return this._trackModel; } get trackModel() { return this._trackModel.value; } set trackModel(value) { if (value == this._trackModel.value) { return; } this._trackModel.next(value); } _debugMode = new rxjs_1.BehaviorSubject(false); get debugModeSubject() { return this._debugMode; } get debugMode() { return this._debugMode.value; } set debugMode(value) { if (value == this._debugMode.value) { return; } this._debugMode.next(value); } _flat = new rxjs_1.BehaviorSubject(null); get flatSubject() { return this._flat; } /** * A reference to the last flattening of the conversation */ get flat() { return this._flat.value; } _subTasks = new rxjs_1.BehaviorSubject([]); get subTasksSubject() { return this._subTasks; } get subTasks() { return this._subTasks.value; } _beforeAppend = new rxjs_1.Subject(); get beforeAppend() { return this._beforeAppend; } /** * 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. */ unregisteredVars = {}; userRoles; assistantRoles; systemRoles; roleMap; completionService; converters; maxAutoCompleteDepth; beforeCreateExeCtx; externFunctions = {}; components; /** * Array of modules that can be imported. The modules are not automatically imported, they have * to be imported to take effect. */ modules; /** * The default capabilities of the conversation. Additional capabilities can be enabled by * the first and last message of the conversation as long a disableMessageCapabilities is not * true. */ capabilities; /** * If true capabilities enabled by message in the conversation will be ignored. */ disableMessageCapabilities; /** * Capabilities that should be enabled by the underlying completion service. */ serviceCapabilities; disableAutoFlatten; autoFlattenDelayMs; dynamicFunctionCallback; componentCompletionCallback; /** * A callback used to implement rag retrieval. */ ragCallback; getStartOfConversation; /** * A function that will be used to output debug values. By default debug information is written * to the output of the conversation as comments */ debug; print = convo_lib_1.defaultConvoPrintFunction; defaultOptions; defaultVars; cache; /** * If true the flattened version of the conversation will be logged before sending to LLMs. * @note Cached responses are not logged. Use `logFlatCached` to log both cached and non-cached * flattened conversations. */ logFlat; /** * If true both cached and non-cached versions of the conversation are logged before being set * to an LLM */ logFlatCached; /** * Sub conversations */ subs = {}; constructor(options = {}) { const { name = convo_lib_1.defaultConversationName, isAgent = false, room = new ConvoRoom_1.ConvoRoom(), userRoles = ['user'], assistantRoles = ['assistant'], systemRoles = ['system'], roleMap = {}, completionService = convo_deps_1.convoCompletionService.all(), converters = convo_deps_1.convoConversationConverterProvider.all(), capabilities = [], serviceCapabilities = [], maxAutoCompleteDepth = 100, trackTime = false, trackTokens = false, trackModel = false, onTokenUsage, disableAutoFlatten = false, autoFlattenDelayMs = 30, ragCallback, debug, debugMode, disableMessageCapabilities = false, initConvo, defaultVars, onConstructed, define, onComponentMessages, componentCompletionCallback, externFunctions, components, externScopeFunctions, getStartOfConversation, cache, logFlat = false, logFlatCached = false, usage = (0, convo_lib_1.createEmptyConvoTokenUsage)(), beforeCreateExeCtx, modules, } = options; this.name = name; this.usage = usage; this.isAgent = isAgent; this.defaultOptions = options; this.beforeCreateExeCtx = beforeCreateExeCtx; this.getStartOfConversation = getStartOfConversation; this.cache = typeof cache === 'boolean' ? (cache ? [(0, convo_deps_1.convoCacheService)()] : []) : (0, common_1.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.disableMessageCapabilities = disableMessageCapabilities; this.serviceCapabilities = serviceCapabilities; this.maxAutoCompleteDepth = maxAutoCompleteDepth; this._trackTime = new rxjs_1.BehaviorSubject(trackTime); this._trackTokens = new rxjs_1.BehaviorSubject(trackTokens); this._trackModel = new rxjs_1.BehaviorSubject(trackModel); this._onTokenUsage = onTokenUsage; this.disableAutoFlatten = disableAutoFlatten; 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 === convo_lib_1.convoRoles.user && (0, convo_lib_1.containsConvoTag)(last.tags, convo_lib_1.convoTags.init) ? true : false; } addTask(task) { this._activeTaskCount.next(this._activeTaskCount.value + 1); (0, common_1.pushBehaviorSubjectAry)(this._openTasks, task); let removed = false; return () => { if (removed) { return; } removed = true; this._activeTaskCount.next(this._activeTaskCount.value - 1); (0, common_1.removeBehaviorSubjectAryValue)(this._openTasks, task); }; } 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 = (0, convo_lib_1.mapToConvoTags)(firstMsg.tags); tags.push(...t); } if (lastMsg?.tags) { const t = (0, convo_lib_1.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 convo_lib_1.convoTags.capability: if (tag.value) { const caps = tag.value.split(','); for (const c of caps) { const cap = c.trim(); if ((0, convo_types_1.isConvoCapability)(cap) && !capList?.includes(cap)) { if (!capList) { capList = []; } capList.push(cap); } } } break; case convo_lib_1.convoTags.enableVision: if (!capList?.includes('vision')) { if (!capList) { capList = []; } capList.push('vision'); } break; case convo_lib_1.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; } _isDisposed = false; _disposeToken = new common_1.CancelToken(); 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); } _defaultApiKey = null; setDefaultApiKey(key) { this._defaultApiKey = key; } getDefaultApiKey() { if (typeof this._defaultApiKey === 'function') { return this._defaultApiKey(); } else { return this._defaultApiKey; } } parseCode(code) { return (0, convo_parser_1.parseConvoCode)(code, { parseMarkdown: this.defaultOptions.parseMarkdown }); } enabledCapabilities = []; enableCapability(cap) { if (this.enabledCapabilities.includes(cap)) { return; } this.enabledCapabilities.push(cap); switch (cap) { case 'visionFunction': this.define({ hidden: true, fn: (0, createConvoVisionFunction_1.createConvoVisionFunction)() }, true); break; case 'vision': if (!this.serviceCapabilities.includes("vision")) { this.serviceCapabilities.push('vision'); } break; } } autoUpdateCompletionService() { if (!this.completionService) { this.completionService = convo_deps_1.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, 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(options, convoOptions) { const conversation = new Conversation(this.getCloneOptions(convoOptions)); if (this._defaultApiKey) { conversation.setDefaultApiKey(this._defaultApiKey); } 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 === convo_lib_1.convoFunctions.getState); } if (options?.removeAgents) { messages = messages.filter(m => m.tags?.some(t => t.name === convo_lib_1.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]; } conversation.appendMessageObject(messages, { disableAutoFlatten: true, appendCode: options?.cloneConvoString }); 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 }); } 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 convo_lib_1.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 => convo_lib_1.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 === convo_lib_1.convoTags.agentSystem)); if (!hasAgentSystem) { this.append(`@${convo_lib_1.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 === convo_lib_1.convoTags.cap && tag.value) { agent.capabilities?.push(tag.value); } } } headMsg.fn = { ...headMsg.fn }; headMsg.fn.modifiers = [...headMsg.fn.modifiers]; headMsg.fn.local = true; (0, common_1.aryRemoveItem)(headMsg.fn.modifiers, convo_lib_1.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, (0, convo_lib_1.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((0, convo_template_1.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); } appendAfterCall = []; agents = []; /** * Appends new messages to the conversation and by default does not add code to the conversation. */ appendMessageObject(message, { disableAutoFlatten, appendCode } = {}) { const messages = (0, common_1.asArray)(message); this.appendMsgsAry(messages); if (appendCode) { for (const msg of messages) { let messages = (0, convo_lib_1.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; } appendDefineVars(vars) { const convo = [`> define`]; for (const name in vars) { const value = vars[name]; if (!(0, convo_lib_1.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, } = options; 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) { this.appendMsgsAry(r.result); } this._onAppend.next({ text: messages, messages: r.result ?? [] }); if (!this.disableAutoFlatten && !disableAutoFlatten) { this.autoFlattenAsync(false); } return r; } autoFlattenId = 0; async autoFlattenAsync(skipDelay) { const id = ++this.autoFlattenId; if (this.autoFlattenDelayMs > 0 && !skipDelay) { await (0, common_1.delayAsync)(this.autoFlattenDelayMs); if (this.isDisposed || id !== this.autoFlattenId) { return undefined; } } return await this.getAutoFlattenPromise(id); } autoFlatPromiseRef = null; 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]; } appendUserMessage(message, options) { this.append((0, convo_lib_1.formatConvoMessage)('user', message, this.getPrefixTags(options))); } appendAssistantMessage(message, options) { this.append((0, convo_lib_1.formatConvoMessage)('assistant', message, this.getPrefixTags(options))); } appendMessage(role, message, options) { this.append((0, convo_lib_1.formatConvoMessage)(role, message, this.getPrefixTags(options))); } appendDefine(defineCode, description) { return this.append((description ? (0, convo_lib_1.convoDescriptionToComment)(description) + '\n' : '') + '> define\n' + defineCode); } appendTopLevel(defineCode, description) { return this.append((description ? (0, convo_lib_1.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(convo_lib_1.convoVars.__trackTime)) { tags += `@${convo_lib_1.convoTags.time} ${(0, convo_lib_1.getConvoDateString)()}\n`; } if (!msg) { return tags; } if (options?.includeTokenUsage && (this.trackTokens || this.getVar(convo_lib_1.convoVars.__trackTokenUsage))) { tags += `@${convo_lib_1.convoTags.tokenUsage} ${(0, convo_lib_1.convoUsageTokensToString)(msg)}\n`; } if (msg.model && (this.trackModel || this.getVar(convo_lib_1.convoVars.__trackModel))) { tags += `@${convo_lib_1.convoTags.model} ${msg.model}\n`; } if (msg.endpoint) { tags += `@${convo_lib_1.convoTags.endpoint} ${msg.endpoint}\n`; } if (msg.format) { tags += `@${convo_lib_1.convoTags.format} ${msg.format}\n`; } if (msg.assignTo) { tags += `@${convo_lib_1.convoTags.assignTo} ${msg.assignTo}\n`; } return tags; } modelServiceMap = {}; async getCompletionServicesForModelAsync(model = convo_lib_1.convoAnyModelName) { const services = this.completionService ? (0, common_1.asArray)(this.completionService) : []; const cached = this.modelServiceMap[model]; if (cached) { return cached; } const matches = []; for (const s of services) { const models = await s.getModelsAsync?.(); if (!models) { matches.push({ priority: Number.MIN_SAFE_INTEGER, service: { service: s } }); continue; } let hasMatch = false; for (const m of models) { if (m.name === model) { matches.push({ priority: m.priority ?? 0, service: { service: s, model: m } }); hasMatch = true; } if (m.aliases) { for (const a of m.aliases) { if ((0, convo_lib_1.isConvoModelAliasMatch)(model, a)) { matches.push({ priority: a.priority ?? 0, service: { service: s, model: m } }); hasMatch = true; } } } } if (!hasMatch) { const m = models.find(m => m.isServiceDefault); if (m) { matches.push({ priority: Number.MIN_SAFE_INTEGER, service: { service: s, model: m } }); } } } matches.sort((a, b) => b.priority - a.priority); return this.modelServiceMap[model] = matches.map(m => m.service); } async getCompletionServiceAsync(flat, updateTargetModel = false) { const services = await this.getCompletionServicesForModelAsync(flat.responseModel); for (const s of services) { if (s.service?.canComplete(s.model?.name ?? flat.responseModel ?? convo_lib_1.convoAnyModelName, flat)) { if (updateTargetModel && s.model) { flat.responseModel = s.model.name; flat.model = s.model; } return s; } } return undefined; } 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(`@${convo_lib_1.convoTags.toolId} call_${(0, common_1.shortUuid)()}\n> call ${functionName}(${args === undefined ? '' : (0, convo_lib_1.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); } 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, await this.getCompletionServiceAsync(flat, true)); }); if (appendOrOptions?.debug) { console.info('Conversation.completeAsync Result:\n', result.messages ? (result.messages.length === 1 ? result.messages[0] : result.messages) : result); } return result; } async completeWithServiceAsync(flat, serviceAndModel) { const lastMsg = flat.messages[flat.messages.length - 1]; let cacheType = ((lastMsg?.tags && (convo_lib_1.convoTags.cache in lastMsg.tags) && (lastMsg.tags[convo_lib_1.convoTags.cache] ?? convo_lib_1.defaultConvoCacheType)) ?? flat.exe.getVar(convo_lib_1.convoVars.__cache)); if (this.logFlatCached) { console.info((0, convo_lib_1.getFlattenConversationDisplayString)(flat, true)); } let cache = cacheType ? this.cache?.find(c => c.cacheType === cacheType) : this.cache?.[0]; if (!cache && (cacheType === true || cacheType === convo_lib_1.defaultConvoCacheType)) { cache = (0, convo_deps_1.convoCacheService)(); } if (cache?.getCachedResponse) { const cached = await cache.getCachedResponse(flat); if (cached) { return cached; } } if (this.logFlat) { console.info((0, convo_lib_1.getFlattenConversationDisplayString)(flat, true)); } if (!serviceAndModel) { return []; } this.debug?.('To be completed', flat.messages); let configInputResult; if (serviceAndModel.model) { configInputResult = await this.applyModelConfigurationToInputAsync(serviceAndModel.model, flat); } let messages; const lock = (0, convo_lang_lock_1.getGlobalConversationLock)(); const release = await lock?.waitOrCancelAsync(this._disposeToken); if (lock && !release) { return []; } try { messages = await (0, convo_lib_1.completeConvoUsingCompletionServiceAsync)(flat, serviceAndModel.service, this.converters); } finally { release?.(); } this.debug?.('Completion message', messages); if (serviceAndModel.model && configInputResult) { this.applyModelConfigurationToOutput(serviceAndModel.model, flat, messages, configInputResult); } if (cache?.cachedResponse) { await cache.cachedResponse(flat, messages); } return messages; } async applyModelConfigurationToInputAsync(model, flat) { const lastMsg = (0, convo_lib_1.getLastConvoMessageWithRole)(flat.messages, 'user'); const jsonMode = lastMsg?.responseFormat === 'json'; let hasFunctions = false; let fnSystem; for (let i = 0; i < flat.messages.length; i++) { const msg = flat.messages[i]; if (!msg) { continue; } if (jsonMode && (model.jsonModeDisableFunctions || model.jsonModeImplementAsFunction) && msg?.fn && !msg.called) { flat.messages.splice(i, 1); i--; continue; } if (msg.responseFormat === 'json') { this.applyJsonModeToMessage(msg, model, flat); } if (msg.fn && !msg.called) { hasFunctions = true; } if (!model.supportsFunctionCalling) { if (msg.fn && !msg.called) { if (!fnSystem) { fnSystem = []; } fnSystem.push(`<function>\nName: ${msg.fn.name}\nDescription: ${msg.fn.description ?? ''}\nParameters JSON Scheme: ${JSON.stringify((msg._fnParams ?? (msg.fnParams ? ((0, common_1.zodTypeToJsonScheme)(msg.fnParams) ?? {}) : {})))}\n</function>\n`); flat.messages.splice(i, 1); i--; } else if (msg.called) { const updated = { ...msg }; flat.messages[i] = updated; delete updated.called; updated.role = 'assistant'; const fnCall = { functionName: msg.called.name, parameters: msg.calledParams ?? {} }; updated.content = JSON.stringify(fnCall, null, 4); const resultMsg = { role: 'user', content: (`The return value of calling ${msg.called.name} is:\`\`\` json\n${msg.calledReturn === undefined ? 'undefined' : JSON.stringify(msg.calledReturn, null, 4)}\n\`\`\``) }; flat.messages.splice(i + 1, 0, resultMsg); } } } if (fnSystem) { fnSystem.unshift('## Function Calling\nYou can call functions when responding to the user if any of the ' + 'functions relate to the user\'s message.\n\n<callable-functions>\n'); fnSystem.push(`</callable-functions> To call a function respond with a JSON object with 2 properties, "functionName" and "parameters". The value of parameters property should conform to the function parameter JSON scheme. For example to call a function named "openFolder" with a user asks to open the folder named "My Documents" you would respond with the following JSON object. <call-function> { "functionName":"openFolder", "parameters":{ "folderName":"My Documents" } } </call-function> `); (0, convo_lib_1.insertSystemMessageIntoFlatConvo)(fnSystem.join(''), flat); } if (!jsonMode && model.enableRespondWithTextFunction && flat.messages.some(m => m.fn)) { await this.flattenSourceAsync({ appendTo: flat, passExe: true, cacheName: 'responseWithTextFunction', convo: model.respondWithTextFunctionSource ?? /*convo*/ ` # You can call this function if no other functions match the user's message > respondWithText( # Message to response with text: string ) ` }); } if (jsonMode && model.jsonModeImplementAsFunction) { const isAry = lastMsg?.responseFormatIsArray; const convoType = `${isAry ? 'array(' : ''}${lastMsg?.responseFormatTypeName ?? 'any'}${isAry ? ')' : ''}`; await this.flattenSourceAsync({ appendTo: flat, passExe: true, cacheName: 'respondWithJSONFunction_' + convoType, convo: model.respondWithJSONFunctionSource?.replace('__TYPE__', convoType) ?? /*convo*/ ` # You can call this function to return JSON values to the user > respondWithJSON( # JSON object. Do not serialize the value. value: ${convoType} ) ` }); } return { lastMsg, jsonMode, hasFunctions }; } applyModelConfigurationToOutput(model, flat, output, { lastMsg, jsonMode, hasFunctions, }) { for (let i = 0; i < output.length; i++) { const msg = output[i]; if (!msg) { continue; } if (hasFunctions && !model.supportsFunctionCalling && msg.content && msg.content.includes('"functionName"')) { try { let content = msg.content; if (content.includes('<call')) { content = content.replace(/<\/?call-?function\/?>/g, ''); } const call = (0, convo_lib_1.parseConvoJsonMessage)(content); if (call.functionName && call.parameters) { output[i] = (0, convo_lib_1.createFunctionCallConvoCompletionMessage)({ flat, callFn: call.functionName, callParams: call.parameters, toolId: (0, common_1.uuid)(), model: model.name, inputTokens: msg?.inputTokens, outputTokens: msg?.outputTokens, tokenPrice: msg.tokenPrice, }); } } catch { } } if (model.enableRespondWithTextFunction && msg.callFn === 'respondWithText') { output[i] = (0, convo_lib_1.createTextConvoCompletionMessage)({ flat, role: msg.role ?? 'assistant', content: msg.callParams?.text, model: msg.model ?? convo_lib_1.convoAnyModelName, inputTokens: msg.inputTokens, outputTokens: msg.outputTokens, tokenPrice: msg.tokenPrice, }); } if (model.jsonModeImplementAsFunction && jsonMode) { if (msg.callFn === 'respondWithJSON') { let paramValue = msg.callParams?.value ?? null; if (lastMsg?.responseFormatTypeName && lastMsg?.responseFormatTypeName !== 'string' && (typeof paramValue === 'string')) { paramValue = (0, json5_1.parseJson5)(paramValue); } output[i] = (0, convo_lib_1.createTextConvoCompletionMessage)({ flat, role: msg.role ?? 'assistant', content: JSON.stringify(paramValue), model: msg.model ?? convo_lib_1.convoAnyModelName, inputTokens: msg.inputTokens, outputTokens: msg.outputTokens, tokenPrice: msg.tokenPrice, defaults: { format: 'json', formatTypeName: lastMsg?.responseFormatTypeName, formatIsArray: lastMsg?.responseFormatIsArray, } }); } else { output.splice(i, 1); i--; } } } } applyJsonModeToMessage(msg, model, flat) { if (msg.responseFormat !== 'json' || model.jsonModeInstructions) { return; } if (!msg.suffix) { msg.suffix = '\n\n'; } if (model.jsonModeInstructions) { msg.suffix = model.jsonModeInstructions; } else if (model.jsonModeImplementAsFunction) { msg.suffix = 'Call the respondWithJSON function'; } else if (msg.responseFormatTypeName) { const type = flat.exe.getVar(msg.responseFormatTypeName); let scheme = (0, convo_zod_1.convoTypeToJsonScheme)(type); if (!scheme) { throw new ConvoError_1.ConvoError('invalid-message-response-scheme', {}, `${msg.responseFormatTypeName} does not point to a convo type object `); } if (msg.responseFormatIsArray) { scheme = { type: 'object', required: ['values'], properties: { values: { type: 'array', items: scheme } } }; } msg.suffix += `\n\nReturn a well formatted JSON ${msg.responseFormatIsArray ? 'array' : 'object'} that conforms to the following JSON Schema:\n${JSON.stringify(scheme)}`; } else { msg.suffix += `\n\nReturn a well formatted JSON ${msg.responseFormatIsArray ? 'array' : 'object'}.`; } if (model.jsonModeInstructWrapInCodeBlock) { msg.suffix += '\n\nWrap the generated JSON in a markdown json code fence and do not include any pre or post-amble.'; } if (model.jsonModeInstructionsPrefix) { msg.suffix = `${model.jsonModeInstructionsPrefix}\n\n${msg.suffix}`; } if (model.jsonModeInstructionsSuffix) { msg.suffix = `${msg.suffix}\n\n${model.jsonModeInstructionsSuffix}`; } } /** * 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 (0, convo_lib_1.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=${(0, convo_zod_1.zodSchemeToConvoTypeString)(params)} @json JsonScheme > user ${(0, convo_lib_1.escapeConvoMessageContent)(userMessage)} `); if (r.message?.content === undefined) { return undefined; } try { return (0, convo_lib_1.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 =