use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Character {
pub name: String,
#[serde(default)]
pub username: Option<String>,
#[serde(default)]
pub bio: Vec<String>,
#[serde(default)]
pub lore: Vec<String>,
#[serde(default)]
pub topics: Vec<String>,
#[serde(default)]
pub style: CharacterStyle,
#[serde(default)]
pub knowledge: Vec<String>,
#[serde(default)]
pub message_examples: Vec<MessageExample>,
#[serde(default)]
pub adjectives: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CharacterStyle {
#[serde(default)]
pub general: Vec<String>,
#[serde(default)]
pub chat: Vec<String>,
#[serde(default)]
pub post: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageExample {
pub user: String,
pub assistant: String,
}
impl Character {
#[must_use]
pub fn to_system_prompt(&self) -> String {
let mut parts = Vec::new();
if !self.name.is_empty() {
parts.push(format!("You are {}.", self.name));
}
if !self.bio.is_empty() {
parts.push(String::new());
parts.push("About you:".to_string());
for line in &self.bio {
parts.push(format!("- {line}"));
}
}
if !self.lore.is_empty() {
parts.push(String::new());
parts.push("Background:".to_string());
for line in &self.lore {
parts.push(format!("- {line}"));
}
}
if !self.topics.is_empty() {
parts.push(String::new());
parts.push(format!("Your areas of expertise: {}", self.topics.join(", ")));
}
let has_style = !self.style.general.is_empty() ||
!self.style.chat.is_empty() ||
!self.style.post.is_empty();
if has_style {
parts.push(String::new());
parts.push("Style guidelines:".to_string());
for rule in &self.style.general {
parts.push(format!("- {rule}"));
}
for rule in &self.style.chat {
parts.push(format!("- [chat] {rule}"));
}
for rule in &self.style.post {
parts.push(format!("- [post] {rule}"));
}
}
if !self.adjectives.is_empty() {
parts.push(String::new());
parts.push(format!("You are: {}", self.adjectives.join(", ")));
}
if !self.knowledge.is_empty() {
parts.push(String::new());
parts.push("Reference knowledge:".to_string());
for snippet in &self.knowledge {
parts.push(format!("- {snippet}"));
}
}
parts.join("\n")
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.name.is_empty() &&
self.bio.is_empty() &&
self.lore.is_empty() &&
self.topics.is_empty() &&
self.style.general.is_empty() &&
self.style.chat.is_empty() &&
self.style.post.is_empty() &&
self.knowledge.is_empty() &&
self.adjectives.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_character() -> Character {
Character {
name: "CodeBot".to_string(),
username: Some("codebot".to_string()),
bio: vec![
"An AI assistant specialized in code review.".to_string(),
"Focuses on Rust best practices.".to_string(),
],
topics: vec!["rust".to_string(), "code-review".to_string()],
style: CharacterStyle {
general: vec!["Be concise and technical.".to_string()],
chat: vec!["Use code blocks for examples.".to_string()],
post: vec![],
},
adjectives: vec!["helpful".to_string(), "precise".to_string()],
knowledge: vec!["Always check for unwrap() in library code.".to_string()],
..Default::default()
}
}
#[test]
fn renders_system_prompt() {
let character = sample_character();
let prompt = character.to_system_prompt();
assert!(prompt.contains("You are CodeBot."));
assert!(prompt.contains("About you:"));
assert!(prompt.contains("An AI assistant specialized in code review."));
assert!(prompt.contains("Your areas of expertise: rust, code-review"));
assert!(prompt.contains("Style guidelines:"));
assert!(prompt.contains("Be concise and technical."));
assert!(prompt.contains("[chat] Use code blocks for examples."));
assert!(prompt.contains("You are: helpful, precise"));
assert!(prompt.contains("Reference knowledge:"));
assert!(prompt.contains("Always check for unwrap() in library code."));
}
#[test]
fn empty_character_is_detected() {
let empty = Character::default();
assert!(empty.is_empty());
let with_name = Character { name: "Bot".to_string(), ..Default::default() };
assert!(!with_name.is_empty());
}
#[test]
fn serializes_to_json() {
let character = sample_character();
let json = serde_json::to_string(&character).expect("should serialize");
let roundtrip: Character = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(roundtrip.name, "CodeBot");
assert_eq!(roundtrip.topics.len(), 2);
}
#[test]
fn partial_fields_default() {
let json = r#"{"name": "Test"}"#;
let character: Character = serde_json::from_str(json).expect("should deserialize");
assert!(character.bio.is_empty());
assert!(character.topics.is_empty());
assert!(character.style.general.is_empty());
}
}