feat: implement message examples usage from character cards
Add support for using mes_example field from character cards to teach the AI the character's voice and writing style. Examples are parsed, processed with template variable replacement, and injected into the context at a configurable position. Backend changes: - Extended RoleplaySettings with examples_enabled and examples_position fields - Implemented parse_message_examples() to parse <START>-delimited example blocks - Added example injection in build_api_messages() with position control - Integrated examples into token counter with accurate counting - Created update_examples_settings command for saving settings Frontend changes: - Added Message Examples UI controls in Author's Note tab - Checkbox to enable/disable examples - Dropdown to select injection position (after_system/before_history) - Save button with success/error feedback - Token breakdown now shows examples token count - Settings load/save integrated with roleplay panel Message examples help the AI understand character personality, speaking patterns, and response style by providing concrete examples of how the character should respond. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,10 @@ struct RoleplaySettings {
|
||||
recursion_depth: usize, // Max depth for recursive World Info activation (default 3)
|
||||
#[serde(default)]
|
||||
active_preset_id: Option<String>, // Selected prompt preset for this character
|
||||
#[serde(default)]
|
||||
examples_enabled: bool, // Whether to include message examples from character card
|
||||
#[serde(default = "default_examples_position")]
|
||||
examples_position: String, // Where to insert examples: "after_system" or "before_history"
|
||||
}
|
||||
|
||||
fn default_authors_note_depth() -> usize {
|
||||
@@ -301,6 +305,10 @@ fn default_scan_depth() -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
fn default_examples_position() -> String {
|
||||
"after_system".to_string() // Insert examples after system prompt, before history
|
||||
}
|
||||
|
||||
fn default_recursion_depth() -> usize {
|
||||
3
|
||||
}
|
||||
@@ -318,6 +326,8 @@ impl Default for RoleplaySettings {
|
||||
scan_depth: default_scan_depth(),
|
||||
recursion_depth: default_recursion_depth(),
|
||||
active_preset_id: None, // No preset selected by default
|
||||
examples_enabled: false, // Message examples disabled by default
|
||||
examples_position: default_examples_position(), // After system prompt by default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1310,6 +1320,74 @@ fn replace_template_variables(
|
||||
result
|
||||
}
|
||||
|
||||
// Parse mes_example field from character card into Message objects
|
||||
fn parse_message_examples(
|
||||
mes_example: &str,
|
||||
character: &Character,
|
||||
settings: &RoleplaySettings,
|
||||
) -> Vec<Message> {
|
||||
let mut examples = Vec::new();
|
||||
|
||||
// Split by <START> tag to get individual example blocks
|
||||
let blocks: Vec<&str> = mes_example
|
||||
.split("<START>")
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for block in blocks {
|
||||
// Process each line in the block
|
||||
for line in block.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace template variables
|
||||
let processed_line = replace_template_variables(line, character, settings);
|
||||
|
||||
// Determine role based on prefix ({{user}}: or {{char}}:)
|
||||
// After replacement, it will be the actual names
|
||||
let user_name = if settings.persona_enabled {
|
||||
settings.persona_name.as_deref().unwrap_or("User")
|
||||
} else {
|
||||
"User"
|
||||
};
|
||||
|
||||
if processed_line.starts_with(&format!("{}:", user_name)) {
|
||||
// User message
|
||||
let content = processed_line
|
||||
.trim_start_matches(&format!("{}:", user_name))
|
||||
.trim()
|
||||
.to_string();
|
||||
examples.push(Message::new_user(content));
|
||||
} else if processed_line.starts_with(&format!("{}:", character.name)) {
|
||||
// Assistant message
|
||||
let content = processed_line
|
||||
.trim_start_matches(&format!("{}:", character.name))
|
||||
.trim()
|
||||
.to_string();
|
||||
examples.push(Message::new_assistant(content));
|
||||
} else if processed_line.contains(':') {
|
||||
// Fallback: split on first colon
|
||||
let parts: Vec<&str> = processed_line.splitn(2, ':').collect();
|
||||
if parts.len() == 2 {
|
||||
let speaker = parts[0].trim();
|
||||
let content = parts[1].trim().to_string();
|
||||
|
||||
if speaker == user_name {
|
||||
examples.push(Message::new_user(content));
|
||||
} else {
|
||||
examples.push(Message::new_assistant(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
examples
|
||||
}
|
||||
|
||||
// Build injected context from roleplay settings
|
||||
fn build_roleplay_context(
|
||||
character: &Character,
|
||||
@@ -1398,6 +1476,29 @@ fn build_api_messages(
|
||||
let mut api_messages = vec![Message::new_user(enhanced_system_prompt)];
|
||||
api_messages[0].role = "system".to_string();
|
||||
|
||||
// Insert message examples if enabled
|
||||
if roleplay_settings.examples_enabled {
|
||||
if let Some(ref mes_example) = character.mes_example {
|
||||
if !mes_example.is_empty() {
|
||||
let examples = parse_message_examples(mes_example, character, roleplay_settings);
|
||||
|
||||
// Insert examples based on position setting
|
||||
match roleplay_settings.examples_position.as_str() {
|
||||
"after_system" => {
|
||||
// Insert right after system message (position 1)
|
||||
for (i, example) in examples.into_iter().enumerate() {
|
||||
api_messages.insert(1 + i, example);
|
||||
}
|
||||
}
|
||||
"before_history" | _ => {
|
||||
// Insert at end (before history gets added)
|
||||
api_messages.extend(examples);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add history messages with current swipe content
|
||||
for msg in &history.messages {
|
||||
let mut api_msg = Message::new_user(msg.get_content().to_string());
|
||||
@@ -2630,6 +2731,18 @@ fn update_persona(
|
||||
save_roleplay_settings(&character_id, &settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_examples_settings(
|
||||
character_id: String,
|
||||
enabled: bool,
|
||||
position: String,
|
||||
) -> Result<(), String> {
|
||||
let mut settings = load_roleplay_settings(&character_id);
|
||||
settings.examples_enabled = enabled;
|
||||
settings.examples_position = position;
|
||||
save_roleplay_settings(&character_id, &settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_recursion_depth(
|
||||
character_id: String,
|
||||
@@ -2797,6 +2910,7 @@ struct TokenBreakdown {
|
||||
persona: usize,
|
||||
world_info: usize,
|
||||
authors_note: usize,
|
||||
message_examples: usize,
|
||||
message_history: usize,
|
||||
current_input: usize,
|
||||
estimated_max_tokens: usize,
|
||||
@@ -2887,6 +3001,19 @@ fn get_token_count(character_id: Option<String>, current_input: String) -> Resul
|
||||
0
|
||||
};
|
||||
|
||||
// Count message examples
|
||||
let mut examples_tokens = 0;
|
||||
if roleplay_settings.examples_enabled {
|
||||
if let Some(ref mes_example) = character.mes_example {
|
||||
if !mes_example.is_empty() {
|
||||
let examples = parse_message_examples(mes_example, &character, &roleplay_settings);
|
||||
for example in examples {
|
||||
examples_tokens += count_tokens(example.get_content());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count message history
|
||||
let mut history_tokens = 0;
|
||||
for msg in &history.messages {
|
||||
@@ -2898,7 +3025,7 @@ fn get_token_count(character_id: Option<String>, current_input: String) -> Resul
|
||||
|
||||
// Calculate total
|
||||
let total = system_prompt_tokens + preset_tokens + persona_tokens + world_info_tokens +
|
||||
authors_note_tokens + history_tokens + input_tokens;
|
||||
authors_note_tokens + examples_tokens + history_tokens + input_tokens;
|
||||
|
||||
// Estimate remaining tokens for response (assuming 16k context with 4k max response)
|
||||
let estimated_max_tokens = if total < 12000 { 4096 } else { 16384 - total };
|
||||
@@ -2910,6 +3037,7 @@ fn get_token_count(character_id: Option<String>, current_input: String) -> Resul
|
||||
persona: persona_tokens,
|
||||
world_info: world_info_tokens,
|
||||
authors_note: authors_note_tokens,
|
||||
message_examples: examples_tokens,
|
||||
message_history: history_tokens,
|
||||
current_input: input_tokens,
|
||||
estimated_max_tokens,
|
||||
@@ -3211,6 +3339,7 @@ pub fn run() {
|
||||
update_roleplay_depths,
|
||||
update_authors_note,
|
||||
update_persona,
|
||||
update_examples_settings,
|
||||
update_recursion_depth,
|
||||
get_presets,
|
||||
get_preset,
|
||||
|
||||
@@ -153,9 +153,34 @@
|
||||
Enable Author's Note
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="save-authors-note-btn" class="btn-primary" style="width: 100%;">
|
||||
<button type="button" id="save-authors-note-btn" class="btn-primary" style="width: 100%; margin-bottom: 20px;">
|
||||
Save Author's Note
|
||||
</button>
|
||||
|
||||
<!-- Message Examples Section -->
|
||||
<div class="form-group" style="border-top: 1px solid var(--border); padding-top: 16px;">
|
||||
<label>Message Examples</label>
|
||||
<p style="color: var(--text-secondary); font-size: 12px; margin-bottom: 8px;">
|
||||
Use character card's message examples to teach the AI the character's voice and style.
|
||||
</p>
|
||||
<label>
|
||||
<input type="checkbox" id="examples-enabled" />
|
||||
Enable Message Examples
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="examples-position">Examples Position</label>
|
||||
<select id="examples-position" style="width: 100%;">
|
||||
<option value="after_system">After System Prompt (Recommended)</option>
|
||||
<option value="before_history">Before Message History</option>
|
||||
</select>
|
||||
<p style="color: var(--text-secondary); font-size: 11px; margin-top: 4px;">
|
||||
Where to inject examples in the context. After system prompt works best for most models.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" id="save-examples-btn" class="btn-primary" style="width: 100%;">
|
||||
Save Examples Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -671,6 +696,10 @@
|
||||
<span>Author's Note:</span>
|
||||
<span id="token-authorsnote">0</span>
|
||||
</div>
|
||||
<div class="token-breakdown-item">
|
||||
<span>Message Examples:</span>
|
||||
<span id="token-examples">0</span>
|
||||
</div>
|
||||
<div class="token-breakdown-item">
|
||||
<span>Message History:</span>
|
||||
<span id="token-history">0</span>
|
||||
|
||||
29
src/main.js
29
src/main.js
@@ -1561,6 +1561,7 @@ function setupAppControls() {
|
||||
document.getElementById('add-worldinfo-btn').addEventListener('click', handleAddWorldInfoEntry);
|
||||
document.getElementById('save-authors-note-btn').addEventListener('click', handleSaveAuthorsNote);
|
||||
document.getElementById('save-persona-btn').addEventListener('click', handleSavePersona);
|
||||
document.getElementById('save-examples-btn').addEventListener('click', handleSaveExamples);
|
||||
|
||||
// Setup recursion depth change handler
|
||||
document.getElementById('recursion-depth').addEventListener('change', handleRecursionDepthChange);
|
||||
@@ -1622,6 +1623,7 @@ async function updateTokenCount() {
|
||||
document.getElementById('token-persona').textContent = tokenData.persona;
|
||||
document.getElementById('token-worldinfo').textContent = tokenData.world_info;
|
||||
document.getElementById('token-authorsnote').textContent = tokenData.authors_note;
|
||||
document.getElementById('token-examples').textContent = tokenData.message_examples;
|
||||
document.getElementById('token-history').textContent = tokenData.message_history;
|
||||
document.getElementById('token-input').textContent = tokenData.current_input;
|
||||
document.getElementById('token-total-detail').textContent = tokenData.total;
|
||||
@@ -2012,6 +2014,10 @@ async function loadRoleplaySettings() {
|
||||
document.getElementById('persona-description').value = settings.persona_description || '';
|
||||
document.getElementById('persona-enabled').checked = settings.persona_enabled || false;
|
||||
|
||||
// Load Message Examples
|
||||
document.getElementById('examples-enabled').checked = settings.examples_enabled || false;
|
||||
document.getElementById('examples-position').value = settings.examples_position || 'after_system';
|
||||
|
||||
// Load Presets
|
||||
await loadPresets();
|
||||
} catch (error) {
|
||||
@@ -2243,6 +2249,29 @@ async function handleSavePersona() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save Message Examples Settings
|
||||
async function handleSaveExamples() {
|
||||
if (!currentCharacter) return;
|
||||
|
||||
const enabled = document.getElementById('examples-enabled').checked;
|
||||
const position = document.getElementById('examples-position').value;
|
||||
|
||||
try {
|
||||
await invoke('update_examples_settings', {
|
||||
characterId: currentCharacter.id,
|
||||
enabled,
|
||||
position
|
||||
});
|
||||
|
||||
// Show success message
|
||||
setStatus('Message Examples settings saved', 'success');
|
||||
setTimeout(() => setStatus('Ready'), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to save Message Examples settings:', error);
|
||||
setStatus('Failed to save Message Examples settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle recursion depth change
|
||||
async function handleRecursionDepthChange() {
|
||||
if (!currentCharacter) return;
|
||||
|
||||
Reference in New Issue
Block a user