use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CharacterCard {
pub spec: String,
pub spec_version: String,
pub data: CharacterData,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CharacterData {
pub name: String,
pub description: String,
pub personality: String,
pub scenario: String,
pub first_mes: String,
pub mes_example: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator_notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_history_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alternate_greetings: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_version: Option<String>,
}
impl CharacterCard {
pub fn build_system_prompt_with_substitutions(
&self,
user_name: Option<&str>,
char_name: Option<&str>,
) -> String {
let mut prompt = String::new();
if let Some(system_prompt) = &self.data.system_prompt {
let substituted = self.apply_substitutions(system_prompt, user_name, char_name);
prompt.push_str(&substituted);
prompt.push_str("\n\n");
}
let char_display_name = char_name.unwrap_or(&self.data.name);
prompt.push_str(&format!("Character: {}\n", char_display_name));
prompt.push_str(&format!("Description: {}\n", self.data.description));
prompt.push_str(&format!("Personality: {}\n", self.data.personality));
prompt.push_str(&format!("Scenario: {}\n", self.data.scenario));
if !self.data.mes_example.is_empty() {
let substituted_example =
self.apply_substitutions(&self.data.mes_example, user_name, char_name);
prompt.push_str(&format!("\nExample dialogue:\n{}\n", substituted_example));
}
prompt
}
pub fn get_greeting_with_substitutions(
&self,
user_name: Option<&str>,
char_name: Option<&str>,
) -> String {
self.apply_substitutions(&self.data.first_mes, user_name, char_name)
}
pub fn get_post_history_instructions_with_substitutions(
&self,
user_name: Option<&str>,
char_name: Option<&str>,
) -> Option<String> {
self.data
.post_history_instructions
.as_ref()
.map(|instructions| self.apply_substitutions(instructions, user_name, char_name))
}
fn apply_substitutions(
&self,
text: &str,
user_name: Option<&str>,
char_name: Option<&str>,
) -> String {
let char_replacement = char_name.unwrap_or(&self.data.name);
let user_replacement = user_name.unwrap_or("Anon");
text.replace("{{char}}", char_replacement)
.replace("{{user}}", user_replacement)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_card() -> CharacterCard {
CharacterCard {
spec: "chara_card_v2".to_string(),
spec_version: "2.0".to_string(),
data: CharacterData {
name: "Alice".to_string(),
description: "A helpful AI assistant".to_string(),
personality: "Friendly and knowledgeable".to_string(),
scenario: "Helping users with their questions".to_string(),
first_mes: "Hello! How can I help you today?".to_string(),
mes_example: "{{user}}: Hi\n{{char}}: Hello there!".to_string(),
creator_notes: None,
system_prompt: None,
post_history_instructions: None,
alternate_greetings: None,
tags: None,
creator: None,
character_version: None,
},
}
}
#[test]
fn test_character_card_structure() {
let card = create_test_card();
assert_eq!(card.spec, "chara_card_v2");
assert_eq!(card.spec_version, "2.0");
assert_eq!(card.data.name, "Alice");
}
#[test]
fn test_optional_fields() {
let mut card = create_test_card();
card.data.creator_notes = Some("Test notes".to_string());
card.data.alternate_greetings =
Some(vec!["Hi there!".to_string(), "Greetings!".to_string()]);
card.data.tags = Some(vec!["helpful".to_string(), "friendly".to_string()]);
card.data.creator = Some("Test Creator".to_string());
card.data.character_version = Some("1.0".to_string());
assert_eq!(card.data.creator_notes, Some("Test notes".to_string()));
assert_eq!(card.data.alternate_greetings.as_ref().unwrap().len(), 2);
assert_eq!(card.data.tags.as_ref().unwrap().len(), 2);
assert_eq!(card.data.creator, Some("Test Creator".to_string()));
assert_eq!(card.data.character_version, Some("1.0".to_string()));
}
#[test]
fn test_serialization_deserialization() {
let card = create_test_card();
let json = serde_json::to_string(&card).unwrap();
let deserialized: CharacterCard = serde_json::from_str(&json).unwrap();
assert_eq!(card, deserialized);
}
#[test]
fn test_optional_fields_not_serialized_when_none() {
let card = create_test_card();
let json = serde_json::to_string(&card).unwrap();
assert!(!json.contains("creator_notes"));
assert!(!json.contains("system_prompt"));
assert!(!json.contains("post_history_instructions"));
assert!(!json.contains("alternate_greetings"));
assert!(!json.contains("tags"));
assert!(!json.contains("creator"));
assert!(!json.contains("character_version"));
}
#[test]
fn test_greeting_with_substitutions() {
let mut card = create_test_card();
card.data.first_mes = "Hello {{user}}! I'm {{char}}, nice to meet you!".to_string();
let greeting_default = card.get_greeting_with_substitutions(None, None);
assert_eq!(greeting_default, "Hello Anon! I'm Alice, nice to meet you!");
let greeting_custom = card.get_greeting_with_substitutions(Some("Bob"), Some("Assistant"));
assert_eq!(
greeting_custom,
"Hello Bob! I'm Assistant, nice to meet you!"
);
}
#[test]
fn test_system_prompt_with_substitutions() {
let mut card = create_test_card();
card.data.system_prompt = Some("You are {{char}} talking to {{user}}.".to_string());
card.data.mes_example = "{{user}}: Hi\n{{char}}: Hello {{user}}!".to_string();
let prompt = card.build_system_prompt_with_substitutions(Some("Alice"), Some("Bot"));
assert!(prompt.contains("You are Bot talking to Alice."));
assert!(prompt.contains("Character: Bot"));
assert!(prompt.contains("Alice: Hi\nBot: Hello Alice!"));
}
#[test]
fn test_post_history_instructions_with_substitutions() {
let mut card = create_test_card();
card.data.post_history_instructions =
Some("Remember that {{user}} is talking to {{char}}.".to_string());
let instructions =
card.get_post_history_instructions_with_substitutions(Some("John"), Some("AI"));
assert_eq!(
instructions,
Some("Remember that John is talking to AI.".to_string())
);
card.data.post_history_instructions = None;
let instructions_none =
card.get_post_history_instructions_with_substitutions(Some("John"), Some("AI"));
assert_eq!(instructions_none, None);
}
}