mockforge_data/
persona_templates.rs

1//! Domain-specific persona templates for generating realistic trait patterns
2//!
3//! This module provides templates for different business domains that define
4//! how persona traits should be generated based on a seed. Each template
5//! creates believable trait combinations that reflect real-world patterns.
6
7use crate::domains::Domain;
8use crate::persona::PersonaProfile;
9use crate::persona_backstory::BackstoryGenerator;
10use crate::Result;
11use rand::rngs::StdRng;
12use rand::Rng;
13use rand::SeedableRng;
14use std::collections::HashMap;
15
16/// Trait for persona templates that generate traits based on a seed
17pub trait PersonaTemplate: Send + Sync {
18    /// Generate traits for a persona based on its seed
19    fn generate_traits(&self, seed: u64) -> HashMap<String, String>;
20
21    /// Get the domain this template applies to
22    fn domain(&self) -> Domain;
23
24    /// Generate a backstory for a persona based on its traits
25    ///
26    /// This is an optional method that can be overridden by implementations
27    /// to provide domain-specific backstory generation. The default implementation
28    /// uses the BackstoryGenerator.
29    fn generate_backstory(&self, persona: &PersonaProfile) -> Result<Option<String>> {
30        let backstory_generator = BackstoryGenerator::new();
31        match backstory_generator.generate_backstory(persona) {
32            Ok(backstory) => Ok(Some(backstory)),
33            Err(_) => Ok(None), // Return None if backstory generation fails
34        }
35    }
36}
37
38/// Finance persona template
39///
40/// Generates traits for financial personas including account types,
41/// spending patterns, transaction frequency, and currency preferences.
42pub struct FinancePersonaTemplate;
43
44impl FinancePersonaTemplate {
45    /// Create a new finance persona template
46    pub fn new() -> Self {
47        Self
48    }
49}
50
51impl PersonaTemplate for FinancePersonaTemplate {
52    fn domain(&self) -> Domain {
53        Domain::Finance
54    }
55
56    fn generate_traits(&self, seed: u64) -> HashMap<String, String> {
57        let mut rng = StdRng::seed_from_u64(seed);
58        let mut traits = HashMap::new();
59
60        // Account type: checking, savings, premium, business
61        let account_types = ["checking", "savings", "premium", "business"];
62        let account_type_idx = rng.random_range(0..account_types.len());
63        traits.insert("account_type".to_string(), account_types[account_type_idx].to_string());
64
65        // Spending level: conservative, moderate, high
66        let spending_levels = ["conservative", "moderate", "high"];
67        let spending_idx = rng.random_range(0..spending_levels.len());
68        traits.insert("spending_level".to_string(), spending_levels[spending_idx].to_string());
69
70        // Transaction frequency: low, medium, high
71        let frequencies = ["low", "medium", "high"];
72        let freq_idx = rng.random_range(0..frequencies.len());
73        traits.insert("transaction_frequency".to_string(), frequencies[freq_idx].to_string());
74
75        // Preferred currency: USD, EUR, GBP, JPY, CNY
76        let currencies = ["USD", "EUR", "GBP", "JPY", "CNY"];
77        let currency_idx = rng.random_range(0..currencies.len());
78        traits.insert("preferred_currency".to_string(), currencies[currency_idx].to_string());
79
80        // Account age: new, established, long_term
81        let account_ages = ["new", "established", "long_term"];
82        let age_idx = rng.random_range(0..account_ages.len());
83        traits.insert("account_age".to_string(), account_ages[age_idx].to_string());
84
85        traits
86    }
87}
88
89impl Default for FinancePersonaTemplate {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95/// E-commerce persona template
96///
97/// Generates traits for e-commerce personas including customer segments,
98/// purchase history patterns, product preferences, and shipping preferences.
99pub struct EcommercePersonaTemplate;
100
101impl EcommercePersonaTemplate {
102    /// Create a new e-commerce persona template
103    pub fn new() -> Self {
104        Self
105    }
106}
107
108impl PersonaTemplate for EcommercePersonaTemplate {
109    fn domain(&self) -> Domain {
110        Domain::Ecommerce
111    }
112
113    fn generate_traits(&self, seed: u64) -> HashMap<String, String> {
114        let mut rng = StdRng::seed_from_u64(seed);
115        let mut traits = HashMap::new();
116
117        // Customer segment: VIP, regular, new
118        let segments = ["VIP", "regular", "new"];
119        let segment_idx = rng.random_range(0..segments.len());
120        traits.insert("customer_segment".to_string(), segments[segment_idx].to_string());
121
122        // Purchase frequency: occasional, regular, frequent
123        let frequencies = ["occasional", "regular", "frequent"];
124        let freq_idx = rng.random_range(0..frequencies.len());
125        traits.insert("purchase_frequency".to_string(), frequencies[freq_idx].to_string());
126
127        // Product category preference
128        let categories = ["electronics", "clothing", "books", "home", "sports"];
129        let cat_idx = rng.random_range(0..categories.len());
130        traits.insert("preferred_category".to_string(), categories[cat_idx].to_string());
131
132        // Shipping preference: standard, express, overnight
133        let shipping = ["standard", "express", "overnight"];
134        let ship_idx = rng.random_range(0..shipping.len());
135        traits.insert("preferred_shipping".to_string(), shipping[ship_idx].to_string());
136
137        // Return frequency: low, medium, high
138        let return_freqs = ["low", "medium", "high"];
139        let ret_idx = rng.random_range(0..return_freqs.len());
140        traits.insert("return_frequency".to_string(), return_freqs[ret_idx].to_string());
141
142        traits
143    }
144}
145
146impl Default for EcommercePersonaTemplate {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152/// Healthcare persona template
153///
154/// Generates traits for healthcare personas including patient demographics,
155/// medical history patterns, insurance types, and condition patterns.
156pub struct HealthcarePersonaTemplate;
157
158impl HealthcarePersonaTemplate {
159    /// Create a new healthcare persona template
160    pub fn new() -> Self {
161        Self
162    }
163}
164
165impl PersonaTemplate for HealthcarePersonaTemplate {
166    fn domain(&self) -> Domain {
167        Domain::Healthcare
168    }
169
170    fn generate_traits(&self, seed: u64) -> HashMap<String, String> {
171        let mut rng = StdRng::seed_from_u64(seed);
172        let mut traits = HashMap::new();
173
174        // Insurance type: private, medicare, medicaid, uninsured
175        let insurance_types = ["private", "medicare", "medicaid", "uninsured"];
176        let ins_idx = rng.random_range(0..insurance_types.len());
177        traits.insert("insurance_type".to_string(), insurance_types[ins_idx].to_string());
178
179        // Blood type: A+, A-, B+, B-, AB+, AB-, O+, O-
180        let blood_types = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
181        let blood_idx = rng.random_range(0..blood_types.len());
182        traits.insert("blood_type".to_string(), blood_types[blood_idx].to_string());
183
184        // Age group: pediatric, adult, senior
185        let age_groups = ["pediatric", "adult", "senior"];
186        let age_idx = rng.random_range(0..age_groups.len());
187        traits.insert("age_group".to_string(), age_groups[age_idx].to_string());
188
189        // Visit frequency: rare, occasional, regular, frequent
190        let visit_freqs = ["rare", "occasional", "regular", "frequent"];
191        let visit_idx = rng.random_range(0..visit_freqs.len());
192        traits.insert("visit_frequency".to_string(), visit_freqs[visit_idx].to_string());
193
194        // Chronic conditions: none, single, multiple
195        let conditions = ["none", "single", "multiple"];
196        let cond_idx = rng.random_range(0..conditions.len());
197        traits.insert("chronic_conditions".to_string(), conditions[cond_idx].to_string());
198
199        traits
200    }
201}
202
203impl Default for HealthcarePersonaTemplate {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// Template registry for managing domain-specific templates
210pub struct PersonaTemplateRegistry {
211    templates: HashMap<Domain, Box<dyn PersonaTemplate + Send + Sync>>,
212}
213
214impl PersonaTemplateRegistry {
215    /// Create a new template registry with default templates
216    pub fn new() -> Self {
217        let mut registry = Self {
218            templates: HashMap::new(),
219        };
220
221        // Register default templates
222        registry.register_template(Domain::Finance, Box::new(FinancePersonaTemplate::new()));
223        registry.register_template(Domain::Ecommerce, Box::new(EcommercePersonaTemplate::new()));
224        registry.register_template(Domain::Healthcare, Box::new(HealthcarePersonaTemplate::new()));
225
226        registry
227    }
228
229    /// Register a template for a domain
230    pub fn register_template(
231        &mut self,
232        domain: Domain,
233        template: Box<dyn PersonaTemplate + Send + Sync>,
234    ) {
235        self.templates.insert(domain, template);
236    }
237
238    /// Get a template for a domain
239    pub fn get_template(&self, domain: Domain) -> Option<&(dyn PersonaTemplate + Send + Sync)> {
240        self.templates.get(&domain).map(|t| t.as_ref())
241    }
242
243    /// Generate traits for a persona using the appropriate template
244    pub fn generate_traits_for_persona(&self, persona: &PersonaProfile) -> HashMap<String, String> {
245        if let Some(template) = self.get_template(persona.domain) {
246            template.generate_traits(persona.seed)
247        } else {
248            HashMap::new()
249        }
250    }
251
252    /// Apply template traits to a persona
253    ///
254    /// Generates traits using the template and adds them to the persona.
255    pub fn apply_template_to_persona(&self, persona: &mut PersonaProfile) -> Result<()> {
256        let traits = self.generate_traits_for_persona(persona);
257        for (key, value) in traits {
258            persona.set_trait(key, value);
259        }
260        Ok(())
261    }
262
263    /// Apply template traits and optionally generate a backstory
264    ///
265    /// Generates traits using the template, adds them to the persona, and
266    /// optionally generates a backstory if `generate_backstory` is true.
267    pub fn apply_template_to_persona_with_backstory(
268        &self,
269        persona: &mut PersonaProfile,
270        generate_backstory: bool,
271    ) -> Result<()> {
272        // First apply traits
273        self.apply_template_to_persona(persona)?;
274
275        // Then generate backstory if requested
276        if generate_backstory {
277            if let Some(template) = self.get_template(persona.domain) {
278                if let Ok(Some(backstory)) = template.generate_backstory(persona) {
279                    persona.set_backstory(backstory);
280                }
281            }
282        }
283
284        Ok(())
285    }
286}
287
288impl Default for PersonaTemplateRegistry {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_finance_template_generate_traits() {
300        let template = FinancePersonaTemplate::new();
301        let traits = template.generate_traits(42);
302
303        assert!(traits.contains_key("account_type"));
304        assert!(traits.contains_key("spending_level"));
305        assert!(traits.contains_key("transaction_frequency"));
306        assert!(traits.contains_key("preferred_currency"));
307    }
308
309    #[test]
310    fn test_finance_template_deterministic() {
311        let template = FinancePersonaTemplate::new();
312        let traits1 = template.generate_traits(42);
313        let traits2 = template.generate_traits(42);
314
315        // Same seed should produce same traits
316        assert_eq!(traits1, traits2);
317    }
318
319    #[test]
320    fn test_ecommerce_template_generate_traits() {
321        let template = EcommercePersonaTemplate::new();
322        let traits = template.generate_traits(42);
323
324        assert!(traits.contains_key("customer_segment"));
325        assert!(traits.contains_key("purchase_frequency"));
326        assert!(traits.contains_key("preferred_category"));
327        assert!(traits.contains_key("preferred_shipping"));
328    }
329
330    #[test]
331    fn test_healthcare_template_generate_traits() {
332        let template = HealthcarePersonaTemplate::new();
333        let traits = template.generate_traits(42);
334
335        assert!(traits.contains_key("insurance_type"));
336        assert!(traits.contains_key("blood_type"));
337        assert!(traits.contains_key("age_group"));
338        assert!(traits.contains_key("visit_frequency"));
339    }
340
341    #[test]
342    fn test_template_registry() {
343        let registry = PersonaTemplateRegistry::new();
344
345        assert!(registry.get_template(Domain::Finance).is_some());
346        assert!(registry.get_template(Domain::Ecommerce).is_some());
347        assert!(registry.get_template(Domain::Healthcare).is_some());
348        assert!(registry.get_template(Domain::Iot).is_none());
349    }
350
351    #[test]
352    fn test_template_registry_apply_to_persona() {
353        let registry = PersonaTemplateRegistry::new();
354        let mut persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
355
356        registry.apply_template_to_persona(&mut persona).unwrap();
357
358        assert!(persona.get_trait("account_type").is_some());
359        assert!(persona.get_trait("spending_level").is_some());
360    }
361}