feat: json format, et al.

This commit is contained in:
Subarashimo
2025-12-03 14:55:30 +01:00
parent 56349f30e6
commit 0f7fdfcef1
28 changed files with 5692 additions and 237 deletions
+322 -53
View File
@@ -7,9 +7,11 @@ import { getContext } from '../../../../../../extensions.js';
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
import { generateSchemaExample } from '../../types/trackerData.js';
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
/** @typedef {import('../../types/trackerData.js').TrackerData} TrackerData */
/**
* Default HTML prompt text
@@ -17,11 +19,17 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co
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 tracker instruction prompt text
* Default tracker instruction prompt text (legacy text format)
* Use {{user}} as placeholder for the user's name (will be replaced at runtime)
*/
export const DEFAULT_TRACKER_PROMPT = `At the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that {{user}} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).`;
/**
* 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".`;
/**
* Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information
@@ -173,6 +181,9 @@ function buildAttributesString() {
}
/**
* @deprecated Use generateJSONTrackerInstructions instead. This legacy text format
* is kept for backwards compatibility with older LLM responses.
*
* Generates an example block showing current tracker states in markdown code blocks.
* Uses COMMITTED data (not displayed data) for generation context.
*
@@ -240,6 +251,9 @@ export function generateTrackerExample() {
}
/**
* @deprecated Use generateJSONTrackerInstructions instead. This legacy text format
* is kept for backwards compatibility with older LLM responses.
*
* Generates the instruction portion - format specifications and guidelines.
*
* @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt (true for main generation, false for separate tracker generation)
@@ -253,10 +267,10 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
const trackerConfig = extensionSettings.trackerConfig;
let instructions = '';
// Check if any trackers are enabled (including inventory and quests as independent sections)
// Check if any trackers are enabled (including inventory, skills and quests as independent sections)
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox ||
extensionSettings.showCharacterThoughts || extensionSettings.showInventory ||
extensionSettings.showQuests;
extensionSettings.showCharacterThoughts || extensionSettings.showSkills ||
extensionSettings.showInventory || extensionSettings.showQuests;
// Only add tracker instructions if at least one tracker is enabled
if (hasAnyTrackers) {
@@ -295,8 +309,9 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
}
}
// Add skills section if enabled
if (userStatsConfig?.skillsSection?.enabled) {
// Add skills section if enabled in config AND NOT shown as separate section
// When showSkills is true, skills are in their own tab and have their own code block
if (userStatsConfig?.skillsSection?.enabled && !extensionSettings.showSkills) {
const skillFields = userStatsConfig.skillsSection.customFields || [];
const skillFieldsText = skillFields.map(f => `[${f}]`).join(', ');
instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`;
@@ -329,6 +344,33 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
instructions += '```\n\n';
}
// Add separate skills section when showSkills is enabled
if (extensionSettings.showSkills) {
const skillsConfig = trackerConfig?.userStats?.skillsSection;
const skillFields = skillsConfig?.customFields || [];
if (skillFields.length > 0) {
instructions += '```\n';
instructions += 'Skills\n';
instructions += '---\n';
// Each skill category contains a list of abilities
for (const skillName of skillFields) {
if (extensionSettings.enableItemSkillLinks) {
instructions += `${skillName}: [Abilities in this category, e.g. "Sword Fighting (Iron Sword), Parry" or "None"]\n`;
} else {
instructions += `${skillName}: [Abilities in this category, e.g. "Lockpicking, Sneaking" or "None"]\n`;
}
}
if (extensionSettings.enableItemSkillLinks) {
instructions += '\n(Abilities from items use parentheses: "Skill (Item)". Remove if item is removed or unequipped.)\n';
}
instructions += '```\n\n';
}
}
if (extensionSettings.showInfoBox) {
const infoBoxConfig = trackerConfig?.infoBox;
const widgets = infoBoxConfig?.widgets || {};
@@ -459,6 +501,220 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
return instructions;
}
/**
* 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 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);
}
}
// 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": "Brief summary of recent events"');
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"';
if (charConfig.relationshipFields?.length > 0) {
charExample += `,\n "relationship": "${charConfig.relationshipFields[0]}"`;
}
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 }`;
}
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 || [];
if (skillCategories.length > 0) {
let skillsSection = ' "skills": {\n';
const categoryExamples = skillCategories.map(cat => {
let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }';
if (enableItemSkillLinks) {
skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }';
}
return ` "${cat}": [${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';
instructions += '- Empty arrays [] for sections with no items\n';
instructions += '- null for main quest if none active\n';
if (enableItemSkillLinks) {
instructions += '- Items can grant skills: add "grantsSkill": "Skill Name" to the item\n';
instructions += '- Skills from items: add "grantedBy": "Item Name" to the skill\n';
instructions += '- If an item is removed/lost, remove its linked skill too\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`;
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 a formatted contextual summary for SEPARATE mode injection.
* Includes the full tracker data in original format (without code fences and separators).
@@ -561,75 +817,88 @@ export function generateContextualSummary() {
}
/**
* Generates the RPG tracking prompt text (for backward compatibility with separate mode).
* Uses COMMITTED data (not displayed data) for generation context.
* 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() {
// Use COMMITTED data for generation context, not displayed data
const userName = getContext().name1;
let promptText = '';
promptText += `Here are the previous trackers in the roleplay that you should consider when responding:\n`;
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) {
if (committedTrackerData.userStats) {
promptText += `Last ${userName}'s Stats:\n${committedTrackerData.userStats}\n\n`;
} else {
promptText += `Last ${userName}'s Stats:\nNone - this is the first update.\n\n`;
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;
}
}
// Add current skills to the previous data context
const skillsSection = extensionSettings.trackerConfig?.userStats?.skillsSection;
if (skillsSection?.enabled && skillsSection.customFields && skillsSection.customFields.length > 0) {
const skillsList = skillsSection.customFields.join(', ');
promptText += `Skills: ${skillsList}\n\n`;
// 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';
}
}
}
// Add current inventory to the previous data context - independent of showUserStats
if (extensionSettings.showInventory && extensionSettings.userStats?.inventory) {
const inventorySummary = buildInventorySummary(extensionSettings.userStats.inventory);
if (inventorySummary && inventorySummary !== 'None') {
promptText += `Last Inventory:\n${inventorySummary}\n\n`;
// InfoBox
if (extensionSettings.showInfoBox && extensionSettings.infoBoxData) {
previousState.infoBox = extensionSettings.infoBoxData;
}
// Characters
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
previousState.characters = extensionSettings.charactersData;
}
// Inventory
if (extensionSettings.showInventory) {
if (extensionSettings.inventoryV3 && (extensionSettings.inventoryV3.onPerson?.length > 0 ||
Object.keys(extensionSettings.inventoryV3.stored || {}).length > 0 ||
extensionSettings.inventoryV3.assets?.length > 0)) {
previousState.inventory = extensionSettings.inventoryV3;
}
}
// Add current quests to the previous data context - independent of showUserStats
if (extensionSettings.showQuests && extensionSettings.quests) {
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
promptText += `Main Quests: ${extensionSettings.quests.main}\n`;
}
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
promptText += `Optional Quests: ${optionalQuests || 'None'}\n`;
}
promptText += `\n`;
// Skills
if (extensionSettings.showSkills && extensionSettings.skillsV2) {
previousState.skills = extensionSettings.skillsV2;
}
if (extensionSettings.showInfoBox) {
if (committedTrackerData.infoBox) {
promptText += `Last Info Box:\n${committedTrackerData.infoBox}\n\n`;
} else {
promptText += `Last Info Box:\nNone - this is the first update.\n\n`;
}
// Quests
if (extensionSettings.showQuests && extensionSettings.questsV2) {
previousState.quests = extensionSettings.questsV2;
}
if (extensionSettings.showCharacterThoughts) {
if (committedTrackerData.characterThoughts) {
promptText += `Last Present Characters:\n${committedTrackerData.characterThoughts}\n`;
} else {
promptText += `Last Present Characters:\nNone - this is the first update.\n`;
}
// 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`;
// Don't include HTML prompt, continuation instruction, or attributes for separate tracker generation
promptText += generateTrackerInstructions(false, false, false);
// Add JSON format instructions
promptText += generateJSONTrackerInstructions(false, false, false);
return promptText;
}