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:
2025-10-16 17:33:50 -07:00
parent b9230772ed
commit d8cb4a768b
3 changed files with 189 additions and 2 deletions

View File

@@ -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,