| 1 | import { name1, name2, characters, getCharacterCardFieldsLazy, getGeneratingModel } from '../../../script.js'; |
| 2 | import { groups, selected_group } from '../../../scripts/group-chats.js'; |
| 3 | import { logMacroGeneralError } from './MacroDiagnostics.js'; |
| 4 | import { getStringHash } from '/scripts/utils.js'; |
| 5 | /** |
| 6 | * MacroEnvBuilder is responsible for constructing the MacroEnv object |
| 7 | * that is passed to macro handlers. |
| 8 | * |
| 9 | * It does **not** depend on the legacy regex macro system. Instead, it |
| 10 | * works from the same raw inputs that substituteParams receives plus a |
| 11 | * small bundle of global helpers, so it can eventually replace the |
| 12 | * environment-building block in substituteParams. |
| 13 | */ |
| 14 | |
| 15 | /** @typedef {import('./MacroEnv.types.js').MacroEnv} MacroEnv */ |
| 16 | |
| 17 | /** |
| 18 | * @typedef {Object} MacroEnvRawContext |
| 19 | * @property {string} content |
| 20 | * @property {string|null} [name1Override] |
| 21 | * @property {string|null} [name2Override] |
| 22 | * @property {string|null} [original] |
| 23 | * @property {string|null} [groupOverride] |
| 24 | * @property {boolean} [replaceCharacterCard] |
| 25 | * @property {Record<string, import('./MacroEnv.types.js').DynamicMacroValue>|null} [dynamicMacros] |
| 26 | * @property {(value: string) => string} [postProcessFn] |
| 27 | */ |
| 28 | |
| 29 | /** |
| 30 | * @typedef {(env: MacroEnv, ctx: MacroEnvRawContext) => void} MacroEnvProvider |
| 31 | */ |
| 32 | |
| 33 | /** |
| 34 | * @enum {number} Exposed ordering buckets for providers. Callers can use envBuilder.providerOrder.* when registering providers. |
| 35 | */ |
| 36 | export const env_provider_order = { |
| 37 | EARLIEST: 0, |
| 38 | EARLY: 10, |
| 39 | NORMAL: 50, |
| 40 | LATE: 90, |
| 41 | LATEST: 100, |
| 42 | }; |
| 43 | |
| 44 | /** @type {MacroEnvBuilder} */ |
| 45 | let instance; |
| 46 | export { instance as MacroEnvBuilder }; |
| 47 | |
| 48 | class MacroEnvBuilder { |
| 49 | /** @type {MacroEnvBuilder} */ static #instance; |
| 50 | /** @type {MacroEnvBuilder} */ static get instance() { return MacroEnvBuilder.#instance ?? (MacroEnvBuilder.#instance = new MacroEnvBuilder()); } |
| 51 | |
| 52 | /** @type {{ fn: MacroEnvProvider, order: env_provider_order }[]} */ |
| 53 | #providers; |
| 54 | |
| 55 | constructor() { |
| 56 | this.#providers = []; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Registers a provider that can augment the MacroEnv with additional |
| 61 | * data (for extensions, extra context, etc.). |
| 62 | * |
| 63 | * Should be called once during initialization. |
| 64 | * |
| 65 | * @param {MacroEnvProvider} provider |
| 66 | * @param {env_provider_order} [order=env_provider_order.NORMAL] |
| 67 | * @returns {void} |
| 68 | */ |
| 69 | registerProvider(provider, order = env_provider_order.NORMAL) { |
| 70 | if (typeof provider !== 'function') throw new Error('Provider must be a function'); |
| 71 | this.#providers.push({ fn: provider, order }); |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Builds a MacroEnv from the raw arguments that are conceptually the |
| 76 | * same as substituteParams receives, plus a bundle of global helpers. |
| 77 | * |
| 78 | * @param {MacroEnvRawContext} ctx |
| 79 | * @returns {MacroEnv} |
| 80 | */ |
| 81 | buildFromRawEnv(ctx) { |
| 82 | // Create the env first, we will populate it step by step. |
| 83 | // Some fields are marked as required, so we have to fill them with dummy fields here |
| 84 | /** @type {MacroEnv} */ |
| 85 | const env = { |
| 86 | content: ctx.content, |
| 87 | contentHash: getStringHash(ctx.content), |
| 88 | names: { user: '', char: '', group: '', groupNotMuted: '', notChar: '' }, |
| 89 | character: {}, |
| 90 | system: { model: '' }, |
| 91 | functions: { postProcess: (x) => x }, |
| 92 | dynamicMacros: {}, |
| 93 | extra: {}, |
| 94 | }; |
| 95 | |
| 96 | if (ctx.replaceCharacterCard) { |
| 97 | // Use lazy fields - each property is only resolved when accessed |
| 98 | const fields = getCharacterCardFieldsLazy(); |
| 99 | if (fields) { |
| 100 | // Define lazy getters on env.character that delegate to fields |
| 101 | const fieldMappings = /** @type {const} */ ([ |
| 102 | ['charPrompt', 'system'], |
| 103 | ['charInstruction', 'jailbreak'], |
| 104 | ['description', 'description'], |
| 105 | ['personality', 'personality'], |
| 106 | ['scenario', 'scenario'], |
| 107 | ['persona', 'persona'], |
| 108 | ['mesExamplesRaw', 'mesExamples'], |
| 109 | ['version', 'version'], |
| 110 | ['charDepthPrompt', 'charDepthPrompt'], |
| 111 | ['creatorNotes', 'creatorNotes'], |
| 112 | ['firstMessage', 'firstMessage'], |
| 113 | ['alternateGreetings', 'alternateGreetings'], |
| 114 | ]); |
| 115 | for (const [envKey, fieldKey] of fieldMappings) { |
| 116 | Object.defineProperty(env.character, envKey, { |
| 117 | get() { |
| 118 | const value = fields[fieldKey]; |
| 119 | // alternateGreetings should default to [] instead of '' |
| 120 | if (envKey === 'alternateGreetings') { |
| 121 | return Array.isArray(value) ? value : []; |
| 122 | } |
| 123 | return value || ''; |
| 124 | }, |
| 125 | enumerable: true, |
| 126 | configurable: true, |
| 127 | }); |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // Names |
| 133 | env.names.user = ctx.name1Override ?? name1 ?? ''; |
| 134 | env.names.char = ctx.name2Override ?? name2 ?? ''; |
| 135 | env.names.group = getGroupValue(ctx, { currentChar: env.names.char, includeMuted: true }); |
| 136 | env.names.groupNotMuted = getGroupValue(ctx, { currentChar: env.names.char, includeMuted: false }); |
| 137 | env.names.notChar = getGroupValue(ctx, { currentChar: env.names.char, filterOutChar: true, includeUser: env.names.user }); |
| 138 | |
| 139 | // System |
| 140 | env.system.model = getGeneratingModel(); |
| 141 | |
| 142 | // Functions |
| 143 | // original (one-shot) and arbitrary additional values |
| 144 | if (typeof ctx.original === 'string') { |
| 145 | let originalSubstituted = false; |
| 146 | env.functions.original = () => { |
| 147 | if (originalSubstituted) return ''; |
| 148 | originalSubstituted = true; |
| 149 | return ctx.original; |
| 150 | }; |
| 151 | } |
| 152 | env.functions.postProcess = typeof ctx.postProcessFn === 'function' ? ctx.postProcessFn : (x) => x; |
| 153 | |
| 154 | // Dynamic, per-call macros that should be visible only for this evaluation run. |
| 155 | // Keys are normalized to lowercase for case-insensitive matching. |
| 156 | if (ctx.dynamicMacros && typeof ctx.dynamicMacros === 'object') { |
| 157 | for (const [key, value] of Object.entries(ctx.dynamicMacros)) { |
| 158 | env.dynamicMacros[key.toLowerCase()] = value; |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | // Let providers augment the env, if any are registered. Apply them in order, |
| 163 | // so callers can influence when their provider runs relative to others. |
| 164 | const orderedProviders = this.#providers.slice().sort((a, b) => a.order - b.order); |
| 165 | for (const { fn } of orderedProviders) { |
| 166 | try { |
| 167 | fn(env, ctx); |
| 168 | } catch (e) { |
| 169 | // Provider errors should not break macro evaluation |
| 170 | logMacroGeneralError({ message: 'MacroEnvBuilder: Provider error', error: e }); |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | return env; |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | instance = MacroEnvBuilder.instance; |
| 179 | |
| 180 | /** |
| 181 | * @param {MacroEnvRawContext} ctx |
| 182 | * @param {Object} options |
| 183 | * @param {string} [options.currentChar=null] |
| 184 | * @param {boolean} [options.includeMuted=false] |
| 185 | * @param {boolean} [options.filterOutChar=false] |
| 186 | * @param {string|null} [options.includeUser=null] |
| 187 | * @returns {string} |
| 188 | */ |
| 189 | function getGroupValue(ctx, { currentChar = null, includeMuted = false, filterOutChar = false, includeUser = null }) { |
| 190 | if (typeof ctx.groupOverride === 'string') { |
| 191 | return ctx.groupOverride; |
| 192 | } |
| 193 | |
| 194 | if (!selected_group) return filterOutChar ? (includeUser || '') : (currentChar ?? ''); |
| 195 | |
| 196 | const groupEntry = Array.isArray(groups) ? groups.find(x => x && x.id === selected_group) : null; |
| 197 | const members = /** @type {string[]} */ (groupEntry?.members ?? []); |
| 198 | const disabledMembers = /** @type {string[]} */ (groupEntry?.disabled_members ?? []); |
| 199 | |
| 200 | const names = Array.isArray(members) |
| 201 | ? members |
| 202 | .filter(((id) => includeMuted ? true : !disabledMembers.includes(id))) |
| 203 | .map(m => Array.isArray(characters) ? characters.find(c => c && c.avatar === m) : null) |
| 204 | .filter(c => !!c && typeof c.name === 'string') |
| 205 | .filter(c => !filterOutChar || c.name !== currentChar) |
| 206 | .map(c => c.name) |
| 207 | .join(', ') |
| 208 | : ''; |
| 209 | |
| 210 | return names; |
| 211 | } |