mockforge_data/
persona_backstory.rs

1//! Backstory generation for personas
2//!
3//! This module provides template-based backstory generation that creates
4//! coherent narrative contexts for personas based on their traits and domain.
5//! Backstories enable more realistic and logically consistent data generation.
6
7use crate::domains::Domain;
8use crate::persona::PersonaProfile;
9use crate::Result;
10use rand::rngs::StdRng;
11use rand::Rng;
12use rand::SeedableRng;
13use std::collections::HashMap;
14
15/// Generator for creating persona backstories
16///
17/// Uses domain-specific templates and persona traits to generate
18/// coherent narrative backstories that explain persona behavior.
19#[derive(Debug)]
20pub struct BackstoryGenerator {
21    /// Domain-specific backstory templates
22    templates: HashMap<Domain, Vec<BackstoryTemplate>>,
23}
24
25/// A backstory template with placeholders for trait values
26#[derive(Debug, Clone)]
27pub struct BackstoryTemplate {
28    /// Template string with placeholders like "{spending_level}" or "{account_type}"
29    template: String,
30    /// Required traits that must be present for this template to be used
31    required_traits: Vec<String>,
32}
33
34impl BackstoryGenerator {
35    /// Create a new backstory generator with default templates
36    pub fn new() -> Self {
37        let mut generator = Self {
38            templates: HashMap::new(),
39        };
40
41        // Initialize domain-specific templates
42        generator.initialize_finance_templates();
43        generator.initialize_ecommerce_templates();
44        generator.initialize_healthcare_templates();
45        generator.initialize_iot_templates();
46
47        generator
48    }
49
50    /// Generate a backstory for a persona based on its traits and domain
51    ///
52    /// Uses the persona's seed for deterministic generation, ensuring
53    /// the same persona always gets the same backstory.
54    pub fn generate_backstory(&self, persona: &PersonaProfile) -> Result<String> {
55        let templates = self.templates.get(&persona.domain).ok_or_else(|| {
56            crate::Error::generic(format!(
57                "No backstory templates available for domain: {:?}",
58                persona.domain
59            ))
60        })?;
61
62        // Filter templates that match the persona's traits
63        let matching_templates: Vec<&BackstoryTemplate> = templates
64            .iter()
65            .filter(|template| {
66                template
67                    .required_traits
68                    .iter()
69                    .all(|trait_name| persona.get_trait(trait_name).is_some())
70            })
71            .collect();
72
73        if matching_templates.is_empty() {
74            // Fallback to generic backstory if no templates match
75            return Ok(self.generate_generic_backstory(persona));
76        }
77
78        // Use persona seed for deterministic selection
79        let mut rng = StdRng::seed_from_u64(persona.seed);
80        let selected_template = &matching_templates[rng.random_range(0..matching_templates.len())];
81
82        // Fill in template placeholders with trait values
83        let mut backstory = selected_template.template.clone();
84        for (trait_name, trait_value) in &persona.traits {
85            let placeholder = format!("{{{}}}", trait_name);
86            backstory = backstory.replace(&placeholder, trait_value);
87        }
88
89        // Replace any remaining placeholders with generic values
90        backstory = self.fill_remaining_placeholders(&backstory, persona, &mut rng)?;
91
92        Ok(backstory)
93    }
94
95    /// Generate a generic backstory when no specific templates match
96    fn generate_generic_backstory(&self, persona: &PersonaProfile) -> String {
97        match persona.domain {
98            Domain::Finance => {
99                format!(
100                    "A {} user in the finance domain with {} traits.",
101                    persona.id,
102                    persona.traits.len()
103                )
104            }
105            Domain::Ecommerce => {
106                format!(
107                    "An e-commerce customer with ID {} and {} preferences.",
108                    persona.id,
109                    persona.traits.len()
110                )
111            }
112            Domain::Healthcare => {
113                format!(
114                    "A healthcare patient with ID {} and {} medical attributes.",
115                    persona.id,
116                    persona.traits.len()
117                )
118            }
119            Domain::Iot => {
120                format!(
121                    "An IoT device or user with ID {} and {} characteristics.",
122                    persona.id,
123                    persona.traits.len()
124                )
125            }
126            _ => format!("A user with ID {} in the {:?} domain.", persona.id, persona.domain),
127        }
128    }
129
130    /// Fill any remaining placeholders in the template
131    fn fill_remaining_placeholders(
132        &self,
133        template: &str,
134        _persona: &PersonaProfile,
135        rng: &mut StdRng,
136    ) -> Result<String> {
137        let mut result = template.to_string();
138
139        // Common placeholders that might not have corresponding traits
140        let common_replacements: HashMap<&str, Vec<&str>> = [
141            ("{age_group}", vec!["young", "middle-aged", "senior"]),
142            ("{location}", vec!["urban", "suburban", "rural"]),
143            ("{activity_level}", vec!["active", "moderate", "low"]),
144        ]
145        .into_iter()
146        .collect();
147
148        for (placeholder, options) in common_replacements {
149            if result.contains(placeholder) {
150                let value = options[rng.random_range(0..options.len())];
151                result = result.replace(placeholder, value);
152            }
153        }
154
155        // Remove any remaining unmatched placeholders
156        while let Some(start) = result.find('{') {
157            if let Some(end) = result[start..].find('}') {
158                result.replace_range(start..start + end + 1, "");
159            } else {
160                break;
161            }
162        }
163
164        Ok(result)
165    }
166
167    /// Initialize finance domain backstory templates
168    fn initialize_finance_templates(&mut self) {
169        let templates = vec![
170            BackstoryTemplate {
171                template: "A {spending_level} spender with a {account_type} account, preferring {preferred_currency} transactions. Account age: {account_age}.".to_string(),
172                required_traits: vec!["spending_level".to_string(), "account_type".to_string()],
173            },
174            BackstoryTemplate {
175                template: "Finance professional with {account_type} account and {transaction_frequency} transaction activity. Prefers {preferred_currency}.".to_string(),
176                required_traits: vec!["account_type".to_string(), "transaction_frequency".to_string()],
177            },
178            BackstoryTemplate {
179                template: "A {spending_level} spending customer with {account_type} {account_age} account history. Primary currency: {preferred_currency}.".to_string(),
180                required_traits: vec!["spending_level".to_string(), "account_age".to_string()],
181            },
182        ];
183
184        self.templates.insert(Domain::Finance, templates);
185    }
186
187    /// Initialize e-commerce domain backstory templates
188    fn initialize_ecommerce_templates(&mut self) {
189        let templates = vec![
190            BackstoryTemplate {
191                template: "A {customer_segment} customer who makes {purchase_frequency} purchases, primarily in {preferred_category}. Shipping preference: {preferred_shipping}.".to_string(),
192                required_traits: vec!["customer_segment".to_string(), "purchase_frequency".to_string()],
193            },
194            BackstoryTemplate {
195                template: "{customer_segment} shopper with {purchase_frequency} buying habits. Favorite category: {preferred_category}. Return frequency: {return_frequency}.".to_string(),
196                required_traits: vec!["customer_segment".to_string(), "preferred_category".to_string()],
197            },
198            BackstoryTemplate {
199                template: "E-commerce customer in the {preferred_category} category with {purchase_frequency} purchase patterns. Prefers {preferred_shipping} delivery.".to_string(),
200                required_traits: vec!["preferred_category".to_string(), "preferred_shipping".to_string()],
201            },
202        ];
203
204        self.templates.insert(Domain::Ecommerce, templates);
205    }
206
207    /// Initialize healthcare domain backstory templates
208    fn initialize_healthcare_templates(&mut self) {
209        let templates = vec![
210            BackstoryTemplate {
211                template: "A {age_group} patient with {insurance_type} insurance and blood type {blood_type}. Visit frequency: {visit_frequency}. Chronic conditions: {chronic_conditions}.".to_string(),
212                required_traits: vec!["insurance_type".to_string(), "blood_type".to_string()],
213            },
214            BackstoryTemplate {
215                template: "{age_group} patient covered by {insurance_type} insurance. {visit_frequency} medical visits with {chronic_conditions} chronic conditions.".to_string(),
216                required_traits: vec!["age_group".to_string(), "insurance_type".to_string(), "visit_frequency".to_string()],
217            },
218            BackstoryTemplate {
219                template: "Healthcare patient with {blood_type} blood type and {insurance_type} coverage. {visit_frequency} visits, {chronic_conditions} chronic conditions.".to_string(),
220                required_traits: vec!["blood_type".to_string(), "insurance_type".to_string()],
221            },
222        ];
223
224        self.templates.insert(Domain::Healthcare, templates);
225    }
226
227    /// Initialize IoT domain backstory templates
228    fn initialize_iot_templates(&mut self) {
229        let templates = vec![
230            BackstoryTemplate {
231                template: "IoT device or user in a {location} environment with {activity_level} activity patterns.".to_string(),
232                required_traits: vec![],
233            },
234            BackstoryTemplate {
235                template: "Connected device user with {activity_level} usage patterns in a {location} setting.".to_string(),
236                required_traits: vec![],
237            },
238        ];
239
240        self.templates.insert(Domain::Iot, templates);
241    }
242
243    /// Add a custom backstory template for a domain
244    pub fn add_template(&mut self, domain: Domain, template: BackstoryTemplate) {
245        self.templates.entry(domain).or_default().push(template);
246    }
247}
248
249impl Default for BackstoryGenerator {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::persona::PersonaProfile;
259    use std::collections::HashMap;
260
261    #[test]
262    fn test_backstory_generator_new() {
263        let generator = BackstoryGenerator::new();
264        assert!(generator.templates.contains_key(&Domain::Finance));
265        assert!(generator.templates.contains_key(&Domain::Ecommerce));
266        assert!(generator.templates.contains_key(&Domain::Healthcare));
267    }
268
269    #[test]
270    fn test_generate_finance_backstory() {
271        let generator = BackstoryGenerator::new();
272        let mut persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
273        persona.set_trait("spending_level".to_string(), "high".to_string());
274        persona.set_trait("account_type".to_string(), "premium".to_string());
275        persona.set_trait("preferred_currency".to_string(), "USD".to_string());
276        persona.set_trait("account_age".to_string(), "long_term".to_string());
277
278        let backstory = generator.generate_backstory(&persona).unwrap();
279        assert!(!backstory.is_empty());
280        assert!(backstory.contains("high"));
281        assert!(backstory.contains("premium"));
282    }
283
284    #[test]
285    fn test_generate_ecommerce_backstory() {
286        let generator = BackstoryGenerator::new();
287        let mut persona = PersonaProfile::new("customer456".to_string(), Domain::Ecommerce);
288        persona.set_trait("customer_segment".to_string(), "VIP".to_string());
289        persona.set_trait("purchase_frequency".to_string(), "frequent".to_string());
290        persona.set_trait("preferred_category".to_string(), "electronics".to_string());
291        persona.set_trait("preferred_shipping".to_string(), "express".to_string());
292
293        let backstory = generator.generate_backstory(&persona).unwrap();
294        assert!(!backstory.is_empty());
295        assert!(backstory.contains("VIP") || backstory.contains("electronics"));
296    }
297
298    #[test]
299    fn test_generate_healthcare_backstory() {
300        let generator = BackstoryGenerator::new();
301        let mut persona = PersonaProfile::new("patient789".to_string(), Domain::Healthcare);
302        persona.set_trait("insurance_type".to_string(), "private".to_string());
303        persona.set_trait("blood_type".to_string(), "O+".to_string());
304        persona.set_trait("age_group".to_string(), "adult".to_string());
305        persona.set_trait("visit_frequency".to_string(), "regular".to_string());
306        persona.set_trait("chronic_conditions".to_string(), "single".to_string());
307
308        let backstory = generator.generate_backstory(&persona).unwrap();
309        assert!(!backstory.is_empty());
310        assert!(backstory.contains("private") || backstory.contains("O+"));
311    }
312
313    #[test]
314    fn test_generate_generic_backstory() {
315        let generator = BackstoryGenerator::new();
316        let persona = PersonaProfile::new("user999".to_string(), Domain::General);
317
318        // Should fall back to generic backstory for unsupported domain
319        let backstory = generator.generate_backstory(&persona);
320        // This might fail for General domain, but that's okay - we test the fallback
321        if let Ok(backstory) = backstory {
322            assert!(!backstory.is_empty());
323        }
324    }
325
326    #[test]
327    fn test_deterministic_backstory() {
328        let generator = BackstoryGenerator::new();
329        let mut persona1 = PersonaProfile::new("user123".to_string(), Domain::Finance);
330        persona1.set_trait("spending_level".to_string(), "high".to_string());
331        persona1.set_trait("account_type".to_string(), "premium".to_string());
332
333        let mut persona2 = PersonaProfile::new("user123".to_string(), Domain::Finance);
334        persona2.set_trait("spending_level".to_string(), "high".to_string());
335        persona2.set_trait("account_type".to_string(), "premium".to_string());
336
337        // Same ID and domain should produce same seed and same backstory
338        assert_eq!(persona1.seed, persona2.seed);
339        let backstory1 = generator.generate_backstory(&persona1).unwrap();
340        let backstory2 = generator.generate_backstory(&persona2).unwrap();
341        assert_eq!(backstory1, backstory2);
342    }
343}