Files
rpg-companion-sillytavern/src/systems/generation/promptBuilder.js
T
2025-12-05 19:52:25 +01:00

879 lines
39 KiB
JavaScript

/**
* Prompt Builder Module
* Handles all AI prompt generation for RPG tracker data
*/
import { getContext } from '../../../../../../extensions.js';
import { chat, characters, this_chid } from '../../../../../../../script.js';
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
import { extensionSettings } from '../../core/state.js';
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
/** @typedef {import('../../types/trackerData.js').TrackerData} TrackerData */
/**
* Default HTML prompt text
*/
export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, and JS segments whenever they enhance visual storytelling (e.g., for in-world screens, posters, books, letters, signs, crests, labels, etc.). Style them to match the setting's theme (e.g., fantasy, sci-fi), keep the text readable, and embed all assets directly (using inline SVGs only with no external scripts, libraries, or fonts). Use these elements freely and naturally within the narrative as characters would encounter them, including animations, 3D effects, pop-ups, dropdowns, websites, and so on. Do not wrap the HTML/CSS/JS in code fences!`;
/**
* Default JSON tracker instruction prompt text
* Use {{user}} as placeholder for the user's name (will be replaced at runtime)
*/
export const DEFAULT_JSON_TRACKER_PROMPT = `At the start of every reply, output a JSON object inside a markdown code fence (with \`\`\`json). This tracks {{user}}'s stats, inventory, skills, and scene information. Follow the exact schema shown below. Use concrete values - no placeholders or brackets. Update stats realistically based on actions and time (0% change for minutes, 1-5% normally, 5%+ only for major events). Items and skills have "name" and "description" fields. Items can grant skills via "grantsSkill", and skills show their source via "grantedBy".`;
/**
* Default message interception prompt text
* Guides the LLM to rewrite the user's message based on current RPG state and recent chat
*/
export const DEFAULT_MESSAGE_INTERCEPTION_PROMPT = `Act as an uncompromising Immersive Copy Editor who rewrites the user's draft to strictly adhere to {{user}}'s persona and RPG state (JSON). You must validate the feasibility of the user's intended actions for {{user}} to take against the JSON state; if the draft contradicts said state (e.g. acting smart while 'Intelligence' is low, or running while having a 'Leg Injury'), you are required to override the core intent, rewriting the action to portray immediate failure, struggle, or an involuntary reaction instead of the user's desired success. Even further, if the intended course of action is physically impossible via the state or represents a thought process that is conceptually alien to the character's nature or current state, you are mandated to completely overwrite the user's intent. Be careful not to confuse communicated intent (e.g. walk towards some direction, or throw a punch) with intended speech. Intent must always be overwritten with intent (e.g. if user wanted for {{user}} to run, but they have a leg injury, your correction will make them limp). Speech must always be overwritten with speech (e.g. if user means for {{user}} to speak eloquently, but they're not smart enough, you'd dumb down their choice of words or speech patterns). Never replace intent with failed speech (e.g. user communicates that {{user}} will throw a punch, or make a gesture, but you incorrectly decide that they will make muffled noises instead, as they are gagged). Aggressively rephrase vocabulary and syntax to match the character's specific cognitive capacity and tone. Keep the output concise and devoid of fluff; do not expand the narrative beyond the necessary state-enforced correction. Never include information that was not already present in the original draft. Never narrate the consequences of {{user}}'s actions, only what they are. Return ONLY the modified message text.`;
/**
* Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information
*/
export async function getCharacterCardsInfo() {
let characterInfo = '';
// Check if in group chat
if (selected_group) {
const group = await getGroupChat(selected_group);
const groupMembers = getGroupMembers(selected_group);
if (groupMembers && groupMembers.length > 0) {
characterInfo += 'Characters in this roleplay:\n\n';
// Filter out disabled (muted) members
const disabledMembers = group?.disabled_members || [];
let characterIndex = 0;
groupMembers.forEach((member) => {
if (!member || !member.name) return;
// Skip muted characters
if (member.avatar && disabledMembers.includes(member.avatar)) {
return;
}
characterIndex++;
characterInfo += `<character${characterIndex}="${member.name}">\n`;
if (member.description) {
characterInfo += `${member.description}\n`;
}
if (member.personality) {
characterInfo += `${member.personality}\n`;
}
characterInfo += `</character${characterIndex}>\n\n`;
});
}
} else if (this_chid !== undefined && characters && characters[this_chid]) {
// Single character chat
const character = characters[this_chid];
characterInfo += 'Character in this roleplay:\n\n';
characterInfo += `<character="${character.name}">\n`;
if (character.description) {
characterInfo += `${character.description}\n`;
}
if (character.personality) {
characterInfo += `${character.personality}\n`;
}
characterInfo += `</character>\n\n`;
}
return characterInfo;
}
/**
* Builds a formatted inventory summary for AI context injection.
* Converts v2 inventory structure to multi-line plaintext format.
*
* @param {InventoryV2|string} inventory - Current inventory (v2 or legacy string)
* @returns {string} Formatted inventory summary for prompt injection
* @example
* // v2 input: { onPerson: "Sword", stored: { Home: "Gold" }, assets: "Horse", version: 2 }
* // Returns: "On Person: Sword\nStored - Home: Gold\nAssets: Horse"
*/
export function buildInventorySummary(inventory) {
// Handle legacy v1 string format
if (typeof inventory === 'string') {
return `Inventory: ${inventory}`;
}
// Handle v2 object format
if (inventory && typeof inventory === 'object' && inventory.version === 2) {
// Check for simplified inventory mode
if (inventory.simplified || extensionSettings.useSimplifiedInventory) {
const items = inventory.items || inventory.onPerson || 'None';
return `Inventory: ${items}`;
}
// Full categorized format
let summary = '';
// Add On Person section
if (inventory.onPerson && inventory.onPerson !== 'None') {
summary += `On Person: ${inventory.onPerson}\n`;
}
// Add Stored sections for each location
if (inventory.stored && Object.keys(inventory.stored).length > 0) {
for (const [location, items] of Object.entries(inventory.stored)) {
if (items && items !== 'None') {
summary += `Stored - ${location}: ${items}\n`;
}
}
}
// Add Assets section
if (inventory.assets && inventory.assets !== 'None') {
summary += `Assets: ${inventory.assets}`;
}
return summary.trim();
}
// Fallback for unknown format
return 'None';
}
/**
* Builds a dynamic attributes string based on configured RPG attributes.
* Uses custom attribute names and values from classicStats.
*
* @returns {string} Formatted attributes string (e.g., "STR 10, DEX 12, INT 15, LVL 5")
*/
function buildAttributesString() {
const trackerConfig = extensionSettings.trackerConfig;
const classicStats = extensionSettings.classicStats;
const userStatsConfig = trackerConfig?.userStats;
// Get enabled attributes from config
const rpgAttributes = userStatsConfig?.rpgAttributes || [
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
{ id: 'con', name: 'CON', description: '', enabled: true },
{ id: 'int', name: 'INT', description: '', enabled: true },
{ id: 'wis', name: 'WIS', description: '', enabled: true },
{ id: 'cha', name: 'CHA', description: '', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
// Build attributes string dynamically
const attributeParts = enabledAttributes.map(attr => {
const value = classicStats[attr.id] !== undefined ? classicStats[attr.id] : 10;
return `${attr.name} ${value}`;
});
// Add level at the end
attributeParts.push(`LVL ${extensionSettings.level}`);
return attributeParts.join(', ');
}
/**
* Generates JSON-based tracker instructions.
* Creates a prompt asking the LLM to output structured JSON data.
*
* @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt
* @param {boolean} includeContinuation - Whether to include continuation instruction
* @param {boolean} includeAttributes - Whether to include RPG attributes
* @returns {string} Formatted JSON instruction text for the AI
*/
export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true, includeAttributes = true) {
const userName = getContext().name1;
const trackerConfig = extensionSettings.trackerConfig;
let instructions = '';
// Check which sections are enabled
const showStats = extensionSettings.showUserStats;
const showInfoBox = extensionSettings.showInfoBox;
const showCharacters = extensionSettings.showCharacterThoughts;
const showInventory = extensionSettings.showInventory;
const showSkills = extensionSettings.showSkills;
const showQuests = extensionSettings.showQuests;
const enableItemSkillLinks = extensionSettings.enableItemSkillLinks;
const deleteSkillWithItem = extensionSettings.deleteSkillWithItem;
const hasAnyTrackers = showStats || showInfoBox || showCharacters || showInventory || showSkills || showQuests;
if (!hasAnyTrackers) {
return instructions;
}
// JSON instruction header
const jsonPrompt = (extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT).replace(/\{\{user\}\}/g, userName);
instructions += `\n${jsonPrompt}\n\n`;
// Build the JSON schema example based on enabled sections
instructions += '```json\n';
instructions += '{\n';
let sections = [];
// Stats section
if (showStats) {
const enabledStats = trackerConfig?.userStats?.customStats?.filter(s => s?.enabled && s?.name) || [];
if (enabledStats.length > 0) {
let statsJson = ' "stats": {\n';
statsJson += enabledStats.map(s => ` "${s.name}": 75`).join(',\n');
statsJson += '\n }';
sections.push(statsJson);
}
// Status section
const statusConfig = trackerConfig?.userStats?.statusSection;
if (statusConfig?.enabled) {
let statusJson = ' "status": {\n';
const statusParts = [];
if (statusConfig.showMoodEmoji) {
statusParts.push(' "mood": "😊"');
}
const customFields = statusConfig.customFields || [];
if (customFields.length > 0) {
const fieldsJson = customFields.map(f => ` "${f}": "[${f} description]"`).join(',\n');
statusParts.push(` "fields": {\n${fieldsJson}\n }`);
}
statusJson += statusParts.join(',\n');
statusJson += '\n }';
sections.push(statusJson);
}
// Skills section
const skillsSectionEnabled = trackerConfig?.userStats?.skillsSection?.enabled || false;
if (skillsSectionEnabled && !extensionSettings.showSkills) {
sections.push(` "skills": "Skill1, Skill2, Skill3"`);
}
}
// Attributes section (if RPG attributes are enabled and should be included)
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes;
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
if (showRPGAttributes && shouldSendAttributes) {
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
{ id: 'con', name: 'CON', description: '', enabled: true },
{ id: 'int', name: 'INT', description: '', enabled: true },
{ id: 'wis', name: 'WIS', description: '', enabled: true },
{ id: 'cha', name: 'CHA', description: '', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
if (enabledAttributes.length > 0) {
let attrsJson = ' "attributes": {\n';
const attrParts = enabledAttributes.map(attr => {
const value = extensionSettings.classicStats?.[attr.id] ?? 10;
return ` "${attr.name}": ${value}`;
});
attrsJson += attrParts.join(',\n');
attrsJson += '\n }';
sections.push(attrsJson);
// Add level
const currentLevel = extensionSettings.level ?? 1;
sections.push(` "level": ${currentLevel}`);
}
}
// Info Box section
if (showInfoBox) {
const widgets = trackerConfig?.infoBox?.widgets || {};
const infoParts = [];
if (widgets.date?.enabled) infoParts.push(' "date": "Monday, March 15, 1242"');
if (widgets.time?.enabled) infoParts.push(' "time": "14:00 → 15:30"');
if (widgets.weather?.enabled) infoParts.push(' "weather": "☀️ Sunny"');
if (widgets.temperature?.enabled) {
const unit = widgets.temperature.unit === 'F' ? '°F' : '°C';
infoParts.push(` "temperature": "22${unit}"`);
}
if (widgets.location?.enabled) infoParts.push(' "location": "Forest Clearing"');
if (widgets.recentEvents?.enabled) infoParts.push(' "recentEvents": ["Event 1", "Event 2"]');
if (infoParts.length > 0) {
sections.push(' "infoBox": {\n' + infoParts.join(',\n') + '\n }');
}
}
// Characters section
if (showCharacters) {
const charConfig = trackerConfig?.presentCharacters || {};
let charExample = ' {\n "name": "Character Name",\n "emoji": "🧑"';
if (charConfig.relationshipFields?.length > 0) {
// Show allowed relationship values as explanation
const allowedRelationships = charConfig.relationshipFields.join(' | ');
charExample += `,\n "relationship": "(${allowedRelationships})"`;
}
const enabledFields = charConfig.customFields?.filter(f => f.enabled) || [];
if (enabledFields.length > 0) {
const fieldsJson = enabledFields.map(f => ` "${f.name}": "[${f.description || f.name}]"`).join(',\n');
charExample += `,\n "fields": {\n${fieldsJson}\n }`;
}
// Character stats (Health, Arousal, etc.)
const charStatsConfig = charConfig.characterStats;
const enabledCharStats = charStatsConfig?.enabled && charStatsConfig?.customStats?.filter(s => s?.enabled && s?.name) || [];
if (enabledCharStats.length > 0) {
const statsJson = enabledCharStats.map(s => ` "${s.name}": 75`).join(',\n');
charExample += `,\n "stats": {\n${statsJson}\n }`;
}
if (charConfig.thoughts?.enabled) {
charExample += ',\n "thoughts": "Character\'s inner thoughts in first person..."';
}
charExample += '\n }';
sections.push(' "characters": [\n' + charExample + '\n ]');
}
// Inventory section
if (showInventory) {
let invSection = ' "inventory": {\n';
if (extensionSettings.useSimplifiedInventory) {
// Simplified: single list
let itemExample = '{ "name": "Item Name", "description": "What it is" }';
if (enableItemSkillLinks) {
itemExample = '{ "name": "Iron Sword", "description": "A sturdy blade", "grantsSkill": "Sword Fighting" }';
}
invSection += ` "items": [${itemExample}]\n`;
} else {
// Full categorized inventory
let itemExample = '{ "name": "Item", "description": "Description" }';
if (enableItemSkillLinks) {
itemExample = '{ "name": "Iron Sword", "description": "A sturdy blade", "grantsSkill": "Sword Fighting" }';
}
invSection += ` "onPerson": [${itemExample}],\n`;
invSection += ' "stored": { "Location Name": [{ "name": "Stored Item", "description": "Description" }] },\n';
invSection += ' "assets": [{ "name": "Property/Vehicle", "description": "Description" }]\n';
}
invSection += ' }';
sections.push(invSection);
}
// Skills section
if (showSkills) {
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
// Migration function handles string array → object array conversion on load
const enabledCategories = skillCategories.filter(cat => cat.enabled !== false);
if (enabledCategories.length > 0) {
let skillsSection = ' "skills": {\n';
const categoryExamples = enabledCategories.map(cat => {
const catName = cat.name;
let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }';
if (enableItemSkillLinks) {
skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }';
}
return ` "${catName}": [${skillExample}]`;
});
skillsSection += categoryExamples.join(',\n');
skillsSection += '\n }';
sections.push(skillsSection);
}
}
// Quests section
if (showQuests) {
let questsSection = ' "quests": {\n';
questsSection += ' "main": { "name": "Main Quest Title", "description": "Primary objective" },\n';
questsSection += ' "optional": [{ "name": "Side Quest", "description": "Optional objective" }]\n';
questsSection += ' }';
sections.push(questsSection);
}
instructions += sections.join(',\n');
instructions += '\n}\n```\n\n';
// Add notes about the format
instructions += 'Important:\n';
instructions += '- Output ONLY valid JSON inside the code fence\n';
instructions += '- Use actual values, not placeholders like [Location]\n';
instructions += '- Stats are percentages (0-100)\n';
if (showRPGAttributes && shouldSendAttributes) {
instructions += '- Attributes are numeric values (typically 1-20, but can be higher)\n';
instructions += '- Level is a numeric value (typically 1+, represents character progression)\n';
}
if (showQuests) {
instructions += '- A main quest can be created when the current main objective changes\n';
instructions += '- Optional quests can be created for smaller matters that need to be resolved\n';
}
instructions += '- Items should be placeed in the inventory section, not the skills section\n';
instructions += '- Characters should be removed as soon as they leave the scene\n';
instructions += '- Your list of characters must never include {{user}}\n';
instructions += '- Empty arrays [] for sections with no items\n';
instructions += '- null for main quest if none active\n';
// Add stat descriptions if any have descriptions
if (showStats) {
const customStats = trackerConfig?.userStats?.customStats || [];
const statsWithDesc = customStats.filter(s => s?.enabled && s?.description);
if (statsWithDesc.length > 0) {
instructions += '- Stat meanings:\n';
statsWithDesc.forEach(stat => {
instructions += ` • "${stat.name}": ${stat.description}\n`;
});
}
}
if (showSkills) {
const skillsLabel = trackerConfig?.userStats?.skillsSection?.label || 'Skills';
if (skillsLabel !== 'Skills') {
instructions += `- The "skills" section represents "${skillsLabel}" in this context\n`;
}
// Add skill category descriptions if any have descriptions
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
const categoriesWithDesc = skillCategories.filter(cat =>
typeof cat === 'object' && cat.description && cat.enabled !== false
);
if (categoriesWithDesc.length > 0) {
instructions += `- ${skillsLabel} categories:\n`;
categoriesWithDesc.forEach(cat => {
instructions += ` • "${cat.name}": ${cat.description}\n`;
});
}
}
if (enableItemSkillLinks) {
instructions += '- Items can grant skills: add {"grantsSkill": "Skill Name"} to the item object\n';
instructions += '- When a skill comes from an item, add {"grantedBy": "Item Name"} to that skill object\n';
if (deleteSkillWithItem) {
instructions += '- If an item is removed/lost, also remove any skill it granted\n';
}
}
instructions += '\n';
// Continuation instruction
if (includeContinuation) {
instructions += `After the JSON block, continue the story naturally from where the last message left off. The tracker data should reflect and influence the narrative - fatigue affects performance, mood colors dialogue, etc.\n\n`;
}
// Attributes
if (includeAttributes) {
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
if (shouldSendAttributes) {
const attributesString = buildAttributesString();
instructions += `${userName}'s attributes: ${attributesString}\n`;
// Add attribute descriptions if any have descriptions
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [];
const attrsWithDesc = rpgAttributes.filter(a => a?.enabled && a?.description);
if (attrsWithDesc.length > 0) {
instructions += 'Attribute meanings:\n';
attrsWithDesc.forEach(attr => {
instructions += ` • ${attr.name}: ${attr.description}\n`;
});
}
if (extensionSettings.lastDiceRoll) {
const roll = extensionSettings.lastDiceRoll;
instructions += `${userName} rolled ${roll.total} on ${roll.formula}. Determine success/failure based on attributes.\n\n`;
} else {
instructions += '\n';
}
}
}
// HTML prompt
if (extensionSettings.enableHtmlPrompt && includeHtmlPrompt) {
const htmlPrompt = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
instructions += htmlPrompt;
}
return instructions;
}
/**
* Generates the current tracker state as a JSON string for SEPARATE mode injection.
* Uses COMMITTED data (not displayed data) for generation context.
* Similar to how <previous> is formatted, but for the current state.
*
* @returns {string} JSON string of current state, or empty string if no data
*/
export function generateContextualSummary() {
// Build current state as JSON (similar to previousState in generateRPGPromptText)
const currentState = {};
const trackerConfig = extensionSettings.trackerConfig;
const descriptions = {};
// Stats
if (extensionSettings.showUserStats) {
const customStats = trackerConfig?.userStats?.customStats?.filter(s => s?.enabled) || [];
if (customStats.length > 0) {
currentState.stats = {};
descriptions.stats = {};
for (const stat of customStats) {
currentState.stats[stat.name] = extensionSettings.userStats[stat.id] ?? 100;
if (stat.description) {
descriptions.stats[stat.name] = stat.description;
}
}
}
// Status
const statusConfig = trackerConfig?.userStats?.statusSection;
if (statusConfig?.enabled) {
currentState.status = {
mood: extensionSettings.userStats.mood || '😐',
fields: {}
};
const customFields = statusConfig.customFields || [];
for (const field of customFields) {
currentState.status.fields[field] = extensionSettings.userStats.conditions || 'None';
}
}
// Skills section
const skillsSectionEnabled = trackerConfig?.userStats?.skillsSection?.enabled || false;
if (skillsSectionEnabled && !extensionSettings.showSkills) {
currentState.skills = extensionSettings.userStats?.skills || 'None';
}
}
// InfoBox
if (extensionSettings.showInfoBox && extensionSettings.infoBoxData) {
currentState.infoBox = extensionSettings.infoBoxData;
}
// Characters - format to match schema
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
// Ensure characters match the expected schema format
currentState.characters = extensionSettings.charactersData.map(char => {
const formatted = { name: char.name };
if (char.relationship) formatted.relationship = char.relationship;
if (char.emoji) formatted.emoji = char.emoji;
if (char.fields && Object.keys(char.fields).length > 0) formatted.fields = char.fields;
if (char.stats && Object.keys(char.stats).length > 0) formatted.stats = char.stats;
if (char.thoughts) formatted.thoughts = char.thoughts;
return formatted;
});
// Add character field descriptions
const charConfig = trackerConfig?.presentCharacters;
if (charConfig?.customFields?.length > 0) {
descriptions.characterFields = {};
for (const field of charConfig.customFields) {
if (field.enabled && field.description) {
descriptions.characterFields[field.name] = field.description;
}
}
}
// Add character stats descriptions
const charStatsConfig = charConfig?.characterStats;
if (charStatsConfig?.enabled && charStatsConfig?.customStats?.length > 0) {
if (!descriptions.characterStats) {
descriptions.characterStats = {};
}
for (const stat of charStatsConfig.customStats) {
if (stat.enabled && stat.description) {
descriptions.characterStats[stat.name] = stat.description;
}
}
}
}
// Inventory - format to match schema (use "items" for simplified mode)
if (extensionSettings.showInventory && extensionSettings.inventoryV3) {
const inv = extensionSettings.inventoryV3;
if (extensionSettings.useSimplifiedInventory) {
// Simplified mode uses "items" key
const items = inv.simplified || inv.onPerson || [];
if (items.length > 0) {
currentState.inventory = { items };
}
} else {
// Full categorized mode
if (inv.onPerson?.length > 0 || Object.keys(inv.stored || {}).length > 0 || inv.assets?.length > 0) {
currentState.inventory = {
onPerson: inv.onPerson || [],
stored: inv.stored || {},
assets: inv.assets || []
};
}
}
}
// Skills
if (extensionSettings.showSkills && extensionSettings.skillsV2) {
currentState.skills = extensionSettings.skillsV2;
// Add skill category descriptions
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
const categoriesWithDesc = skillCategories.filter(cat =>
typeof cat === 'object' && cat.enabled !== false && cat.description
);
if (categoriesWithDesc.length > 0) {
descriptions.skillCategories = {};
for (const cat of categoriesWithDesc) {
descriptions.skillCategories[cat.name] = cat.description;
}
}
}
// Quests
if (extensionSettings.showQuests && extensionSettings.questsV2) {
currentState.quests = extensionSettings.questsV2;
}
// Attributes and level (if RPG attributes are enabled and should be included)
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes;
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
if (showRPGAttributes && shouldSendAttributes) {
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
{ id: 'con', name: 'CON', description: '', enabled: true },
{ id: 'int', name: 'INT', description: '', enabled: true },
{ id: 'wis', name: 'WIS', description: '', enabled: true },
{ id: 'cha', name: 'CHA', description: '', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
if (enabledAttributes.length > 0) {
currentState.attributes = {};
descriptions.attributes = {};
for (const attr of enabledAttributes) {
const value = extensionSettings.classicStats?.[attr.id] ?? 10;
currentState.attributes[attr.name] = value;
if (attr.description) {
descriptions.attributes[attr.name] = attr.description;
}
}
// Add level
currentState.level = extensionSettings.level ?? 1;
}
}
// Add descriptions metadata if any exist
if (Object.keys(descriptions).length > 0) {
currentState._descriptions = descriptions;
}
// Return JSON string if we have any data, otherwise empty string
if (Object.keys(currentState).length > 0) {
return JSON.stringify(currentState, null, 2);
}
return '';
}
/**
* Generates the RPG tracking prompt text for separate mode.
* Shows previous data in JSON format and requests JSON response.
*
* @returns {string} Full prompt text for separate tracker generation
*/
export function generateRPGPromptText() {
const trackerConfig = extensionSettings.trackerConfig;
let promptText = '';
promptText += `Here are the previous trackers in JSON format that you should consider when responding:\n`;
promptText += `<previous>\n`;
// Build previous state as JSON
const previousState = {};
// Stats
if (extensionSettings.showUserStats) {
const customStats = extensionSettings.trackerConfig?.userStats?.customStats?.filter(s => s?.enabled) || [];
if (customStats.length > 0) {
previousState.stats = {};
for (const stat of customStats) {
previousState.stats[stat.name] = extensionSettings.userStats[stat.id] ?? 100;
}
}
// Status
const statusConfig = extensionSettings.trackerConfig?.userStats?.statusSection;
if (statusConfig?.enabled) {
previousState.status = {
mood: extensionSettings.userStats.mood || '😐',
fields: {}
};
const customFields = statusConfig.customFields || [];
for (const field of customFields) {
previousState.status.fields[field] = extensionSettings.userStats.conditions || 'None';
}
}
// Skills
const skillsSectionEnabled = trackerConfig?.userStats?.skillsSection?.enabled || false;
if (skillsSectionEnabled && !extensionSettings.showSkills) {
previousState.skills = extensionSettings.userStats.skills;
}
}
// InfoBox
if (extensionSettings.showInfoBox && extensionSettings.infoBoxData) {
previousState.infoBox = extensionSettings.infoBoxData;
}
// Characters - format to match schema
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
// Ensure characters match the expected schema format
previousState.characters = extensionSettings.charactersData.map(char => {
const formatted = { name: char.name };
if (char.relationship) formatted.relationship = char.relationship;
if (char.emoji) formatted.emoji = char.emoji;
if (char.fields && Object.keys(char.fields).length > 0) formatted.fields = char.fields;
if (char.stats && Object.keys(char.stats).length > 0) formatted.stats = char.stats;
if (char.thoughts) formatted.thoughts = char.thoughts;
return formatted;
});
}
// Inventory - format to match schema (use "items" for simplified mode)
if (extensionSettings.showInventory && extensionSettings.inventoryV3) {
const inv = extensionSettings.inventoryV3;
if (extensionSettings.useSimplifiedInventory) {
// Simplified mode uses "items" key
const items = inv.simplified || inv.onPerson || [];
if (items.length > 0) {
previousState.inventory = { items };
}
} else {
// Full categorized mode
if (inv.onPerson?.length > 0 || Object.keys(inv.stored || {}).length > 0 || inv.assets?.length > 0) {
previousState.inventory = {
onPerson: inv.onPerson || [],
stored: inv.stored || {},
assets: inv.assets || []
};
}
}
}
// Skills
if (extensionSettings.showSkills && extensionSettings.skillsV2) {
previousState.skills = extensionSettings.skillsV2;
}
// Quests
if (extensionSettings.showQuests && extensionSettings.questsV2) {
previousState.quests = extensionSettings.questsV2;
}
// Attributes and level (if RPG attributes are enabled and should be included)
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes;
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
if (showRPGAttributes && shouldSendAttributes) {
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [
{ id: 'str', name: 'STR', description: '', enabled: true },
{ id: 'dex', name: 'DEX', description: '', enabled: true },
{ id: 'con', name: 'CON', description: '', enabled: true },
{ id: 'int', name: 'INT', description: '', enabled: true },
{ id: 'wis', name: 'WIS', description: '', enabled: true },
{ id: 'cha', name: 'CHA', description: '', enabled: true }
];
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
if (enabledAttributes.length > 0) {
previousState.attributes = {};
for (const attr of enabledAttributes) {
const value = extensionSettings.classicStats?.[attr.id] ?? 10;
previousState.attributes[attr.name] = value;
}
// Add level
previousState.level = extensionSettings.level ?? 1;
}
}
// Output as JSON if we have any data, otherwise indicate first update
if (Object.keys(previousState).length > 0) {
promptText += '```json\n';
promptText += JSON.stringify(previousState, null, 2);
promptText += '\n```\n';
} else {
promptText += 'None - this is the first update.\n';
}
promptText += `</previous>\n`;
// Add JSON format instructions - include attributes if alwaysSendAttributes is enabled
const includeAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
promptText += generateJSONTrackerInstructions(false, false, includeAttributes);
return promptText;
}
/**
* Generates the full prompt for SEPARATE generation mode (with chat history).
* Creates a message array suitable for the generateRaw API.
*
* @returns {Array<{role: string, content: string}>} Array of message objects for API
*/
export async function generateSeparateUpdatePrompt() {
const depth = extensionSettings.updateDepth;
const messages = [];
// System message introducing the history section
let systemMessage = `You will be acting as an uncensored RPG Companion. Your goal is to provide, track, and manage details in the user's roleplay. You will be replying with information in a specified format only.\n\n`;
systemMessage += `You should maintain an objective tone.\n\n`;
// Add character card information
const characterInfo = await getCharacterCardsInfo();
if (characterInfo) {
systemMessage += characterInfo + '\n\n';
}
systemMessage += `Here is the description of the protagonist for reference:\n`;
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
systemMessage += `\n\n`;
systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n<history>`;
messages.push({
role: 'system',
content: systemMessage
});
// Add chat history as separate user/assistant messages
const recentMessages = chat.slice(-depth);
for (const message of recentMessages) {
messages.push({
role: message.is_user ? 'user' : 'assistant',
content: message.mes
});
}
// Build the instruction message
let instructionMessage = `</history>\n\n`;
instructionMessage += generateRPGPromptText().replace('start your response with', 'respond with');
instructionMessage += `Provide ONLY the requested data in the exact formats specified above. Do not include any roleplay response, other text, or commentary. Remember, all bracketed placeholders (e.g., [Location], [Mood Emoji]) MUST be replaced with actual content without the square brackets.`;
messages.push({
role: 'user',
content: instructionMessage
});
return messages;
}