From 407a45a25c9ff332112e0424fd939f453e97303c Mon Sep 17 00:00:00 2001 From: Andy Mauragis Date: Thu, 13 Nov 2025 13:45:27 -0500 Subject: [PATCH] feat: Add core suppression logic and integrate into prompt injector --- src/systems/generation/injector.js | 41 ++++++++++++-- src/systems/generation/suppression.js | 82 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/systems/generation/suppression.js diff --git a/src/systems/generation/injector.js b/src/systems/generation/injector.js index e4d8955..e6556b6 100644 --- a/src/systems/generation/injector.js +++ b/src/systems/generation/injector.js @@ -13,6 +13,7 @@ import { lastActionWasSwipe, setLastActionWasSwipe } from '../../core/state.js'; +import { evaluateSuppression } from './suppression.js'; import { parseUserStats } from './parser.js'; import { generateTrackerExample, @@ -44,7 +45,28 @@ export function onGenerationStarted(type, data) { return; } - const chat = getContext().chat; + const context = getContext(); + const chat = context.chat; + // Detect if a guided generation is active (GuidedGenerations and similar extensions + // inject an ephemeral 'instruct' injection into chatMetadata.script_injects). + // If present, we should avoid injecting RPG tracker instructions that ask + // the model to include stats/etc. This prevents conflicts when guided prompts + // are used (e.g., GuidedGenerations Extension). + // Evaluate suppression using the shared helper + const suppression = evaluateSuppression(extensionSettings, context, data); + const { shouldSuppress, skipMode, isGuidedGeneration, isImpersonationGeneration, hasQuietPrompt, instructContent, quietPromptRaw, matchedPattern } = suppression; + + if (shouldSuppress) { + // Debugging: indicate active suppression and which source triggered it + console.debug(`[RPG Companion] Suppression active (mode=${skipMode}). isGuided=${isGuidedGeneration}, isImpersonation=${isImpersonationGeneration}, hasQuietPrompt=${hasQuietPrompt} - skipping RPG tracker injections for this generation.`); + + // Also clear any existing RPG Companion prompts so they do not leak into this generation + // (e.g., previously set extension prompts should not be used alongside a guided prompt) + setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false); + setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false); + } const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; // For SEPARATE mode only: Check if we need to commit extension data @@ -145,7 +167,7 @@ export function onGenerationStarted(type, data) { } // If we have previous tracker data and found an assistant message, inject it as an assistant message - if (example && lastAssistantDepth > 0) { + if (!shouldSuppress && example && lastAssistantDepth > 0) { setExtensionPrompt('rpg-companion-example', example, extension_prompt_types.IN_CHAT, lastAssistantDepth, false, extension_prompt_roles.ASSISTANT); // console.log('[RPG Companion] Injected tracker example as assistant message at depth:', lastAssistantDepth); } else { @@ -153,11 +175,15 @@ export function onGenerationStarted(type, data) { } // Inject the instructions as a user message at depth 0 (right before generation) - setExtensionPrompt('rpg-companion-inject', instructions, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER); + // If this is a guided generation (user explicitly injected 'instruct'), skip adding + // our tracker instructions to avoid clobbering the guided prompt. + if (!shouldSuppress) { + setExtensionPrompt('rpg-companion-inject', instructions, extension_prompt_types.IN_CHAT, 0, false, extension_prompt_roles.USER); + } // console.log('[RPG Companion] Injected RPG tracking instructions at depth 0 (right before generation)'); // Inject HTML prompt separately at depth 0 if enabled (prevents duplication on swipes) - if (extensionSettings.enableHtmlPrompt) { + if (extensionSettings.enableHtmlPrompt && !shouldSuppress) { const htmlPrompt = `\nIf appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response: - Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on. - Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible. @@ -186,7 +212,10 @@ Ensure these details naturally reflect and influence the narrative. Character be `; // Inject context at depth 1 (before last user message) as SYSTEM - setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false); + // Skip when a guided generation injection is present to avoid conflicting instructions + if (!shouldSuppress) { + setExtensionPrompt('rpg-companion-context', wrappedContext, extension_prompt_types.IN_CHAT, 1, false); + } // console.log('[RPG Companion] Injected contextual summary for separate mode:', contextSummary); } else { // Clear if no data yet @@ -194,7 +223,7 @@ Ensure these details naturally reflect and influence the narrative. Character be } // Inject HTML prompt separately at depth 0 if enabled (same as together mode pattern) - if (extensionSettings.enableHtmlPrompt) { + if (extensionSettings.enableHtmlPrompt && !shouldSuppress) { const htmlPrompt = `\nIf appropriate, include inline HTML, CSS, and JS elements for creative, visual storytelling throughout your response: - Use them liberally to depict any in-world content that can be visualized (screens, posters, books, signs, letters, logos, crests, seals, medallions, labels, etc.), with creative license for animations, 3D effects, pop-ups, dropdowns, websites, and so on. - Style them thematically to match the theme (e.g., sleek for sci-fi, rustic for fantasy), ensuring text is visible. diff --git a/src/systems/generation/suppression.js b/src/systems/generation/suppression.js new file mode 100644 index 0000000..aa0313b --- /dev/null +++ b/src/systems/generation/suppression.js @@ -0,0 +1,82 @@ +/** + * Suppression helper for guided generation injection behavior. + * + * This module exports a pure function `evaluateSuppression` that computes + * whether RPG Companion should suppress tracker and HTML injections for a + * given generation request, based on runtime settings, extended context, and + * generation data (quiet prompt flags, etc.). + */ + +/** + * Determine if suppression should be applied for this generation. + * + * @param {any} extensionSettings - extension settings object (may contain skipInjectionsForGuided) + * @param {any} context - SillyTavern context object (used to find chatMetadata.script_injects.instruct) + * @param {any} data - Generation data (contains quiet_prompt/quietPrompt flags) + * @returns {Object} - An object describing the suppression decision. + */ +export function evaluateSuppression(extensionSettings, context, data) { + // Detect presence of any injected `instruct` script + const instructObj = context?.chatMetadata?.script_injects?.instruct; + const isGuidedGeneration = !!instructObj; + const quietPromptRaw = data?.quiet_prompt || data?.quietPrompt || ''; + const hasQuietPrompt = !!quietPromptRaw; + + // Normalize the injected instruction body (it may be an object with a 'value' field or a raw string) + let instructContent = ''; + if (instructObj) { + if (typeof instructObj === 'object') { + instructContent = String(instructObj.value || instructObj || ''); + } else { + instructContent = String(instructObj); + } + } + + const IMPERSONATION_PATTERNS = [ + { id: 'first-perspective', re: /write in first person perspective from/i }, + { id: 'second-perspective', re: /write in second person perspective from/i }, + { id: 'third-perspective', re: /write in third person perspective from/i }, + { id: 'you-yours', re: /using you\/yours for/i }, + { id: 'third-person-pronouns', re: /third-person pronouns for/i }, + { id: 'impersonate-word', re: /\bimpersonat(e|ion)?\b/i }, + { id: 'assume-role', re: /assume the role of/i }, + { id: 'play-role', re: /play the role of/i }, + { id: 'impersonate-command', re: /\/impersonate await=true/i }, + { id: 'generic-first', re: /\bfirst person\b/i }, + { id: 'generic-second', re: /\bsecond person\b/i }, + { id: 'generic-third', re: /\bthird person\b/i } + ]; + + // Include quietPrompt raw text in detection; guided impersonation flows may pass it directly here + const combinedTextForDetection = [instructContent, quietPromptRaw].filter(Boolean).join('\n'); + + let matchedPattern = ''; + let isImpersonationGeneration = false; + if (combinedTextForDetection.length) { + for (const pat of IMPERSONATION_PATTERNS) { + if (pat.re.test(combinedTextForDetection)) { + matchedPattern = pat.id; + isImpersonationGeneration = true; + break; + } + } + } + + const skipMode = (extensionSettings && extensionSettings.skipInjectionsForGuided) || 'none'; + + // Compute suppression according to mode + const shouldSuppress = skipMode === 'guided' + ? (isGuidedGeneration || hasQuietPrompt) + : (skipMode === 'impersonation' ? isImpersonationGeneration : false); + + return { + shouldSuppress, + skipMode, + isGuidedGeneration, + isImpersonationGeneration, + hasQuietPrompt, + instructContent, + quietPromptRaw, + matchedPattern + }; +}