Blame Raw
Cohee · e3f41666 · · 211 lines (8.3 KB)
1 contributor
1import { name1, name2, characters, getCharacterCardFieldsLazy, getGeneratingModel } from '../../../script.js';
2import { groups, selected_group } from '../../../scripts/group-chats.js';
3import { logMacroGeneralError } from './MacroDiagnostics.js';
4import { 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 */
36export const env_provider_order = {
37 EARLIEST: 0,
38 EARLY: 10,
39 NORMAL: 50,
40 LATE: 90,
41 LATEST: 100,
42};
43
44/** @type {MacroEnvBuilder} */
45let instance;
46export { instance as MacroEnvBuilder };
47
48class 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
178instance = 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 */
189function 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}