1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct Character {
22 pub name: String,
24
25 #[serde(default)]
27 pub username: Option<String>,
28
29 #[serde(default)]
31 pub bio: Vec<String>,
32
33 #[serde(default)]
35 pub lore: Vec<String>,
36
37 #[serde(default)]
39 pub topics: Vec<String>,
40
41 #[serde(default)]
43 pub style: CharacterStyle,
44
45 #[serde(default)]
47 pub knowledge: Vec<String>,
48
49 #[serde(default)]
51 pub message_examples: Vec<MessageExample>,
52
53 #[serde(default)]
55 pub adjectives: Vec<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62pub struct CharacterStyle {
63 #[serde(default)]
65 pub general: Vec<String>,
66
67 #[serde(default)]
69 pub chat: Vec<String>,
70
71 #[serde(default)]
73 pub post: Vec<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct MessageExample {
81 pub user: String,
83 pub assistant: String,
85}
86
87impl Character {
90 #[must_use]
95 pub fn to_system_prompt(&self) -> String {
96 let mut parts = Vec::new();
97
98 if !self.name.is_empty() {
100 parts.push(format!("You are {}.", self.name));
101 }
102
103 if !self.bio.is_empty() {
105 parts.push(String::new());
106 parts.push("About you:".to_string());
107 for line in &self.bio {
108 parts.push(format!("- {line}"));
109 }
110 }
111
112 if !self.lore.is_empty() {
114 parts.push(String::new());
115 parts.push("Background:".to_string());
116 for line in &self.lore {
117 parts.push(format!("- {line}"));
118 }
119 }
120
121 if !self.topics.is_empty() {
123 parts.push(String::new());
124 parts.push(format!("Your areas of expertise: {}", self.topics.join(", ")));
125 }
126
127 let has_style = !self.style.general.is_empty() ||
129 !self.style.chat.is_empty() ||
130 !self.style.post.is_empty();
131 if has_style {
132 parts.push(String::new());
133 parts.push("Style guidelines:".to_string());
134 for rule in &self.style.general {
135 parts.push(format!("- {rule}"));
136 }
137 for rule in &self.style.chat {
138 parts.push(format!("- [chat] {rule}"));
139 }
140 for rule in &self.style.post {
141 parts.push(format!("- [post] {rule}"));
142 }
143 }
144
145 if !self.adjectives.is_empty() {
147 parts.push(String::new());
148 parts.push(format!("You are: {}", self.adjectives.join(", ")));
149 }
150
151 if !self.knowledge.is_empty() {
153 parts.push(String::new());
154 parts.push("Reference knowledge:".to_string());
155 for snippet in &self.knowledge {
156 parts.push(format!("- {snippet}"));
157 }
158 }
159
160 parts.join("\n")
161 }
162
163 #[must_use]
165 pub fn is_empty(&self) -> bool {
166 self.name.is_empty() &&
167 self.bio.is_empty() &&
168 self.lore.is_empty() &&
169 self.topics.is_empty() &&
170 self.style.general.is_empty() &&
171 self.style.chat.is_empty() &&
172 self.style.post.is_empty() &&
173 self.knowledge.is_empty() &&
174 self.adjectives.is_empty()
175 }
176}
177
178#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn sample_character() -> Character {
185 Character {
186 name: "CodeBot".to_string(),
187 username: Some("codebot".to_string()),
188 bio: vec![
189 "An AI assistant specialized in code review.".to_string(),
190 "Focuses on Rust best practices.".to_string(),
191 ],
192 topics: vec!["rust".to_string(), "code-review".to_string()],
193 style: CharacterStyle {
194 general: vec!["Be concise and technical.".to_string()],
195 chat: vec!["Use code blocks for examples.".to_string()],
196 post: vec![],
197 },
198 adjectives: vec!["helpful".to_string(), "precise".to_string()],
199 knowledge: vec!["Always check for unwrap() in library code.".to_string()],
200 ..Default::default()
201 }
202 }
203
204 #[test]
205 fn renders_system_prompt() {
206 let character = sample_character();
207 let prompt = character.to_system_prompt();
208
209 assert!(prompt.contains("You are CodeBot."));
210 assert!(prompt.contains("About you:"));
211 assert!(prompt.contains("An AI assistant specialized in code review."));
212 assert!(prompt.contains("Your areas of expertise: rust, code-review"));
213 assert!(prompt.contains("Style guidelines:"));
214 assert!(prompt.contains("Be concise and technical."));
215 assert!(prompt.contains("[chat] Use code blocks for examples."));
216 assert!(prompt.contains("You are: helpful, precise"));
217 assert!(prompt.contains("Reference knowledge:"));
218 assert!(prompt.contains("Always check for unwrap() in library code."));
219 }
220
221 #[test]
222 fn empty_character_is_detected() {
223 let empty = Character::default();
224 assert!(empty.is_empty());
225
226 let with_name = Character { name: "Bot".to_string(), ..Default::default() };
227 assert!(!with_name.is_empty());
228 }
229
230 #[test]
231 fn serializes_to_json() {
232 let character = sample_character();
233 let json = serde_json::to_string(&character).expect("should serialize");
234 let roundtrip: Character = serde_json::from_str(&json).expect("should deserialize");
235 assert_eq!(roundtrip.name, "CodeBot");
236 assert_eq!(roundtrip.topics.len(), 2);
237 }
238
239 #[test]
240 fn partial_fields_default() {
241 let json = r#"{"name": "Test"}"#;
242 let character: Character = serde_json::from_str(json).expect("should deserialize");
243 assert!(character.bio.is_empty());
244 assert!(character.topics.is_empty());
245 assert!(character.style.general.is_empty());
246 }
247}