Skip to main content

mockforge_data/
persona.rs

1//! Persona profile system for consistent, personality-driven data generation
2//!
3//! This module provides a system for generating mock data with specific "personalities"
4//! that remain consistent over time. Each persona has a unique ID, domain, traits,
5//! and a deterministic seed that ensures the same persona always generates the same
6//! data patterns.
7
8use crate::domains::{Domain, DomainGenerator};
9use crate::Result;
10use rand::rngs::StdRng;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::HashMap;
14use std::hash::{Hash, Hasher};
15use std::sync::{Arc, RwLock};
16
17/// Persona profile defining a consistent data personality
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PersonaProfile {
20    /// Unique identifier for this persona (e.g., user_id, device_id, transaction_id)
21    pub id: String,
22    /// Business domain this persona belongs to
23    pub domain: Domain,
24    /// Trait name to value mappings (e.g., "spending_level" → "high", "account_type" → "premium")
25    pub traits: HashMap<String, String>,
26    /// Deterministic seed derived from persona ID and domain for consistency
27    pub seed: u64,
28    /// Narrative backstory explaining persona behavior and characteristics
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub backstory: Option<String>,
31    /// Relationships to other personas
32    /// Keys: relationship types ("owns_devices", "belongs_to_org", "has_users")
33    /// Values: List of related persona IDs
34    #[serde(default)]
35    pub relationships: HashMap<String, Vec<String>>,
36    /// Additional persona-specific metadata
37    #[serde(default)]
38    pub metadata: HashMap<String, Value>,
39    /// Optional lifecycle state management
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub lifecycle: Option<crate::persona_lifecycle::PersonaLifecycle>,
42}
43
44impl PersonaProfile {
45    /// Create a new persona profile
46    ///
47    /// The seed is deterministically derived from the persona ID and domain,
48    /// ensuring the same ID and domain always produce the same seed.
49    pub fn new(id: String, domain: Domain) -> Self {
50        let seed = Self::derive_seed(&id, domain);
51        Self {
52            id,
53            domain,
54            traits: HashMap::new(),
55            seed,
56            backstory: None,
57            relationships: HashMap::new(),
58            metadata: HashMap::new(),
59            lifecycle: None,
60        }
61    }
62
63    /// Create a persona with initial traits
64    pub fn with_traits(id: String, domain: Domain, traits: HashMap<String, String>) -> Self {
65        let mut persona = Self::new(id, domain);
66        persona.traits = traits;
67        persona
68    }
69
70    /// Set the persona's lifecycle
71    pub fn set_lifecycle(&mut self, lifecycle: crate::persona_lifecycle::PersonaLifecycle) {
72        self.lifecycle = Some(lifecycle);
73    }
74
75    /// Get the persona's lifecycle
76    pub fn get_lifecycle(&self) -> Option<&crate::persona_lifecycle::PersonaLifecycle> {
77        self.lifecycle.as_ref()
78    }
79
80    /// Get mutable reference to lifecycle
81    pub fn get_lifecycle_mut(&mut self) -> Option<&mut crate::persona_lifecycle::PersonaLifecycle> {
82        self.lifecycle.as_mut()
83    }
84
85    /// Update lifecycle state based on virtual clock time
86    ///
87    /// Checks if any transitions should occur based on elapsed time and conditions.
88    pub fn update_lifecycle_state(&mut self, current_time: chrono::DateTime<chrono::Utc>) {
89        if let Some(ref mut lifecycle) = self.lifecycle {
90            if let Some((new_state, _rule)) = lifecycle.transition_if_elapsed(current_time) {
91                lifecycle.transition_to(new_state, current_time);
92
93                // Apply lifecycle effects to persona traits
94                let effects = lifecycle.apply_lifecycle_effects();
95                for (key, value) in effects {
96                    self.set_trait(key, value);
97                }
98            }
99        }
100    }
101
102    /// Derive a deterministic seed from persona ID and domain
103    ///
104    /// Uses a simple hash function to convert the ID and domain into a u64 seed.
105    /// This ensures the same ID and domain always produce the same seed.
106    fn derive_seed(id: &str, domain: Domain) -> u64 {
107        use std::collections::hash_map::DefaultHasher;
108        let mut hasher = DefaultHasher::new();
109        id.hash(&mut hasher);
110        domain.as_str().hash(&mut hasher);
111        hasher.finish()
112    }
113
114    /// Add or update a trait
115    pub fn set_trait(&mut self, name: String, value: String) {
116        self.traits.insert(name, value);
117    }
118
119    /// Get a trait value
120    pub fn get_trait(&self, name: &str) -> Option<&String> {
121        self.traits.get(name)
122    }
123
124    /// Add metadata
125    pub fn set_metadata(&mut self, key: String, value: Value) {
126        self.metadata.insert(key, value);
127    }
128
129    /// Get metadata
130    pub fn get_metadata(&self, key: &str) -> Option<&Value> {
131        self.metadata.get(key)
132    }
133
134    /// Set the persona's backstory
135    ///
136    /// The backstory provides narrative context that explains the persona's
137    /// behavior and characteristics, enabling coherent data generation.
138    pub fn set_backstory(&mut self, backstory: String) {
139        self.backstory = Some(backstory);
140    }
141
142    /// Get the persona's backstory
143    pub fn get_backstory(&self) -> Option<&String> {
144        self.backstory.as_ref()
145    }
146
147    /// Check if the persona has a backstory
148    pub fn has_backstory(&self) -> bool {
149        self.backstory.is_some()
150    }
151
152    /// Add a relationship to another persona
153    ///
154    /// # Arguments
155    /// * `relationship_type` - Type of relationship (e.g., "owns_devices", "belongs_to_org", "has_users")
156    /// * `related_persona_id` - ID of the related persona
157    pub fn add_relationship(&mut self, relationship_type: String, related_persona_id: String) {
158        self.relationships
159            .entry(relationship_type)
160            .or_default()
161            .push(related_persona_id);
162    }
163
164    /// Get all relationships of a specific type
165    ///
166    /// Returns a list of persona IDs that have the specified relationship type.
167    pub fn get_relationships(&self, relationship_type: &str) -> Option<&Vec<String>> {
168        self.relationships.get(relationship_type)
169    }
170
171    /// Get all related personas for a specific relationship type
172    ///
173    /// Returns a cloned vector of persona IDs, or an empty vector if no relationships exist.
174    pub fn get_related_personas(&self, relationship_type: &str) -> Vec<String> {
175        self.relationships.get(relationship_type).cloned().unwrap_or_default()
176    }
177
178    /// Get all relationship types for this persona
179    pub fn get_relationship_types(&self) -> Vec<String> {
180        self.relationships.keys().cloned().collect()
181    }
182
183    /// Remove a specific relationship
184    ///
185    /// Removes the specified persona ID from the relationship type's list.
186    /// Returns true if the relationship was found and removed.
187    pub fn remove_relationship(
188        &mut self,
189        relationship_type: &str,
190        related_persona_id: &str,
191    ) -> bool {
192        if let Some(related_ids) = self.relationships.get_mut(relationship_type) {
193            if let Some(pos) = related_ids.iter().position(|id| id == related_persona_id) {
194                related_ids.remove(pos);
195                // Clean up empty relationship lists
196                if related_ids.is_empty() {
197                    self.relationships.remove(relationship_type);
198                }
199                return true;
200            }
201        }
202        false
203    }
204}
205
206/// Registry for managing persona profiles
207///
208/// Provides thread-safe access to persona profiles with in-memory storage
209/// and optional persistence capabilities.
210#[derive(Debug, Clone)]
211pub struct PersonaRegistry {
212    /// In-memory storage of personas keyed by their ID
213    personas: Arc<RwLock<HashMap<String, PersonaProfile>>>,
214    /// Default traits to apply to new personas
215    default_traits: HashMap<String, String>,
216    /// Graph structure for relationship management
217    graph: Arc<crate::persona_graph::PersonaGraph>,
218}
219
220impl PersonaRegistry {
221    /// Create a new persona registry
222    pub fn new() -> Self {
223        Self {
224            personas: Arc::new(RwLock::new(HashMap::new())),
225            default_traits: HashMap::new(),
226            graph: Arc::new(crate::persona_graph::PersonaGraph::new()),
227        }
228    }
229
230    /// Create a registry with default traits for new personas
231    pub fn with_default_traits(default_traits: HashMap<String, String>) -> Self {
232        Self {
233            personas: Arc::new(RwLock::new(HashMap::new())),
234            default_traits,
235            graph: Arc::new(crate::persona_graph::PersonaGraph::new()),
236        }
237    }
238
239    /// Get the persona graph
240    pub fn graph(&self) -> Arc<crate::persona_graph::PersonaGraph> {
241        Arc::clone(&self.graph)
242    }
243
244    /// Get or create a persona profile
245    ///
246    /// If a persona with the given ID exists, returns it. Otherwise, creates
247    /// a new persona with the specified domain and applies default traits.
248    pub fn get_or_create_persona(&self, id: String, domain: Domain) -> PersonaProfile {
249        let personas = self.personas.read().expect("persona registry read lock poisoned");
250
251        // Check if persona already exists
252        if let Some(persona) = personas.get(&id) {
253            return persona.clone();
254        }
255        drop(personas);
256
257        // Create new persona with default traits
258        let mut persona = PersonaProfile::new(id.clone(), domain);
259        for (key, value) in &self.default_traits {
260            persona.set_trait(key.clone(), value.clone());
261        }
262
263        // Store the new persona
264        let mut personas = self.personas.write().expect("persona registry write lock poisoned");
265        personas.insert(id.clone(), persona.clone());
266
267        // Add to graph
268        let entity_type = persona.domain.as_str().to_string();
269        let graph_node = crate::persona_graph::PersonaNode::new(id.clone(), entity_type);
270        self.graph.add_node(graph_node);
271
272        persona
273    }
274
275    /// Get a persona by ID
276    pub fn get_persona(&self, id: &str) -> Option<PersonaProfile> {
277        let personas = self.personas.read().expect("persona registry read lock poisoned");
278        personas.get(id).cloned()
279    }
280
281    /// Update persona traits
282    pub fn update_persona(&self, id: &str, traits: HashMap<String, String>) -> Result<()> {
283        let mut personas = self.personas.write().map_err(|e| {
284            crate::Error::LockPoisoned(format!("persona registry write lock: {}", e))
285        })?;
286        if let Some(persona) = personas.get_mut(id) {
287            for (key, value) in traits {
288                persona.set_trait(key, value);
289            }
290            Ok(())
291        } else {
292            Err(crate::Error::generic(format!("Persona with ID '{}' not found", id)))
293        }
294    }
295
296    /// Update persona backstory
297    ///
298    /// Sets or updates the backstory for an existing persona.
299    pub fn update_persona_backstory(&self, id: &str, backstory: String) -> Result<()> {
300        let mut personas = self.personas.write().map_err(|e| {
301            crate::Error::LockPoisoned(format!("persona registry write lock: {}", e))
302        })?;
303        if let Some(persona) = personas.get_mut(id) {
304            persona.set_backstory(backstory);
305            Ok(())
306        } else {
307            Err(crate::Error::generic(format!("Persona with ID '{}' not found", id)))
308        }
309    }
310
311    /// Update persona with full profile data
312    ///
313    /// Updates traits, backstory, and relationships for an existing persona.
314    /// This is useful when you have a complete persona profile to apply.
315    pub fn update_persona_full(
316        &self,
317        id: &str,
318        traits: Option<HashMap<String, String>>,
319        backstory: Option<String>,
320        relationships: Option<HashMap<String, Vec<String>>>,
321    ) -> Result<()> {
322        let mut personas = self.personas.write().map_err(|e| {
323            crate::Error::LockPoisoned(format!("persona registry write lock: {}", e))
324        })?;
325        if let Some(persona) = personas.get_mut(id) {
326            if let Some(traits) = traits {
327                for (key, value) in traits {
328                    persona.set_trait(key, value);
329                }
330            }
331            if let Some(backstory) = backstory {
332                persona.set_backstory(backstory);
333            }
334            if let Some(relationships) = relationships {
335                for (rel_type, related_ids) in relationships {
336                    for related_id in related_ids {
337                        persona.add_relationship(rel_type.clone(), related_id);
338                    }
339                }
340            }
341            Ok(())
342        } else {
343            Err(crate::Error::generic(format!("Persona with ID '{}' not found", id)))
344        }
345    }
346
347    /// Remove a persona
348    pub fn remove_persona(&self, id: &str) -> bool {
349        let mut personas = self.personas.write().expect("persona registry write lock poisoned");
350        personas.remove(id).is_some()
351    }
352
353    /// Get all persona IDs
354    pub fn list_persona_ids(&self) -> Vec<String> {
355        let personas = self.personas.read().expect("persona registry read lock poisoned");
356        personas.keys().cloned().collect()
357    }
358
359    /// Clear all personas
360    pub fn clear(&self) {
361        let mut personas = self.personas.write().expect("persona registry write lock poisoned");
362        personas.clear();
363    }
364
365    /// Get the number of registered personas
366    pub fn count(&self) -> usize {
367        let personas = self.personas.read().expect("persona registry read lock poisoned");
368        personas.len()
369    }
370
371    /// Get all personas that have a relationship of the specified type with the given persona
372    ///
373    /// Returns a vector of persona profiles that are related to the specified persona.
374    pub fn get_related_personas(
375        &self,
376        persona_id: &str,
377        relationship_type: &str,
378    ) -> Result<Vec<PersonaProfile>> {
379        let personas = self.personas.read().map_err(|e| {
380            crate::Error::LockPoisoned(format!("persona registry read lock: {}", e))
381        })?;
382        if let Some(persona) = personas.get(persona_id) {
383            let related_ids = persona.get_related_personas(relationship_type);
384            let mut related_personas = Vec::new();
385            for related_id in related_ids {
386                if let Some(related_persona) = personas.get(&related_id) {
387                    related_personas.push(related_persona.clone());
388                }
389            }
390            Ok(related_personas)
391        } else {
392            Err(crate::Error::generic(format!("Persona with ID '{}' not found", persona_id)))
393        }
394    }
395
396    /// Find all personas that have a relationship pointing to the specified persona
397    ///
398    /// This performs a reverse lookup to find personas that reference the given persona.
399    pub fn find_personas_with_relationship_to(
400        &self,
401        target_persona_id: &str,
402        relationship_type: &str,
403    ) -> Vec<PersonaProfile> {
404        let personas = self.personas.read().expect("persona registry read lock poisoned");
405        let mut result = Vec::new();
406
407        for persona in personas.values() {
408            if let Some(related_ids) = persona.get_relationships(relationship_type) {
409                if related_ids.contains(&target_persona_id.to_string()) {
410                    result.push(persona.clone());
411                }
412            }
413        }
414
415        result
416    }
417
418    /// Add a relationship between two personas
419    ///
420    /// Creates a relationship from `from_persona_id` to `to_persona_id` of the specified type.
421    pub fn add_relationship(
422        &self,
423        from_persona_id: &str,
424        relationship_type: String,
425        to_persona_id: String,
426    ) -> Result<()> {
427        let mut personas = self.personas.write().map_err(|e| {
428            crate::Error::LockPoisoned(format!("persona registry write lock: {}", e))
429        })?;
430        if let Some(persona) = personas.get_mut(from_persona_id) {
431            persona.add_relationship(relationship_type.clone(), to_persona_id.clone());
432
433            // Also add to graph
434            self.graph
435                .add_edge(from_persona_id.to_string(), to_persona_id, relationship_type);
436
437            Ok(())
438        } else {
439            Err(crate::Error::generic(format!(
440                "Persona with ID '{}' not found",
441                from_persona_id
442            )))
443        }
444    }
445
446    /// Switch to a new persona and update all related personas in the graph
447    ///
448    /// This ensures coherent persona switching across related entities.
449    /// When switching to a new root persona, all related personas (orders, payments, etc.)
450    /// are also updated to maintain consistency.
451    ///
452    /// # Arguments
453    /// * `root_persona_id` - The root persona ID to switch to (e.g., user ID)
454    /// * `relationship_types` - Optional filter for relationship types to follow
455    /// * `update_callback` - Optional callback to update each related persona
456    ///
457    /// # Returns
458    /// Vector of persona IDs that were updated
459    pub fn coherent_persona_switch<F>(
460        &self,
461        root_persona_id: &str,
462        relationship_types: Option<&[String]>,
463        update_callback: Option<F>,
464    ) -> Result<Vec<String>>
465    where
466        F: Fn(&str, &mut PersonaProfile),
467    {
468        // Find all related personas using BFS traversal
469        let related_ids = self.graph.find_related_bfs(root_persona_id, relationship_types, None);
470
471        // Start with the root persona
472        let mut updated_ids = vec![root_persona_id.to_string()];
473        updated_ids.extend(related_ids);
474
475        // Update each persona in the graph
476        let mut personas = self.personas.write().map_err(|e| {
477            crate::Error::LockPoisoned(format!("persona registry write lock: {}", e))
478        })?;
479        for persona_id in &updated_ids {
480            if let Some(persona) = personas.get_mut(persona_id) {
481                // Apply update callback if provided
482                if let Some(ref callback) = update_callback {
483                    callback(persona_id, persona);
484                }
485            }
486        }
487
488        Ok(updated_ids)
489    }
490}
491
492impl Default for PersonaRegistry {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498/// Generator for creating data based on persona profiles
499///
500/// Uses the persona's seed and traits to generate consistent, domain-appropriate
501/// data that reflects the persona's personality.
502#[derive(Debug)]
503pub struct PersonaGenerator {
504    /// Domain generator for domain-specific data generation
505    domain_generator: DomainGenerator,
506}
507
508impl PersonaGenerator {
509    /// Create a new persona generator
510    pub fn new(domain: Domain) -> Self {
511        Self {
512            domain_generator: DomainGenerator::new(domain),
513        }
514    }
515
516    /// Generate data for a specific field type based on persona
517    ///
518    /// Uses the persona's seed to create a deterministic RNG, then generates
519    /// domain-specific data that may be influenced by the persona's traits.
520    pub fn generate_for_persona(
521        &self,
522        persona: &PersonaProfile,
523        field_type: &str,
524    ) -> Result<Value> {
525        // Generate with default reality ratio (0.0 = fully synthetic)
526        self.generate_for_persona_with_reality(persona, field_type, 0.0, None, None)
527    }
528
529    /// Generate data for a specific field type based on persona with reality awareness
530    ///
531    /// The reality ratio determines how much the generated data blends with recorded/real data:
532    /// - 0.0-0.3: Purely synthetic (persona-generated)
533    /// - 0.3-0.7: Blended with recorded snapshots
534    /// - 0.7-1.0: Blended with upstream/real data
535    ///
536    /// # Arguments
537    /// * `persona` - Persona profile to generate data for
538    /// * `field_type` - Type of field to generate (e.g., "name", "email", "amount")
539    /// * `reality_ratio` - Reality continuum ratio (0.0 = mock, 1.0 = real)
540    /// * `recorded_data` - Optional recorded/snapshot data to blend with
541    /// * `real_data` - Optional real/upstream data to blend with
542    pub fn generate_for_persona_with_reality(
543        &self,
544        persona: &PersonaProfile,
545        field_type: &str,
546        reality_ratio: f64,
547        recorded_data: Option<&Value>,
548        real_data: Option<&Value>,
549    ) -> Result<Value> {
550        // Create a deterministic RNG from the persona's seed
551        use rand::rngs::StdRng;
552        use rand::SeedableRng;
553        let mut rng = StdRng::seed_from_u64(persona.seed);
554
555        // Generate base synthetic value using domain generator
556        let mut synthetic_value = self.domain_generator.generate(field_type)?;
557
558        // Apply persona traits to influence the generated value
559        synthetic_value =
560            self.apply_persona_traits(persona, field_type, synthetic_value, &mut rng)?;
561
562        // Apply reality continuum blending based on ratio
563        let reality_ratio = reality_ratio.clamp(0.0, 1.0);
564
565        if reality_ratio < 0.3 {
566            // Low reality: Purely synthetic
567            Ok(synthetic_value)
568        } else if reality_ratio < 0.7 {
569            // Medium reality: Blend with recorded snapshots
570            if let Some(recorded) = recorded_data {
571                self.blend_values(&synthetic_value, recorded, reality_ratio)
572            } else {
573                // No recorded data available, use synthetic
574                Ok(synthetic_value)
575            }
576        } else {
577            // High reality: Blend with upstream/real data
578            if let Some(real) = real_data {
579                self.blend_values(&synthetic_value, real, reality_ratio)
580            } else if let Some(recorded) = recorded_data {
581                // Fallback to recorded if real not available
582                self.blend_values(&synthetic_value, recorded, reality_ratio)
583            } else {
584                // No real or recorded data, use synthetic
585                Ok(synthetic_value)
586            }
587        }
588    }
589
590    /// Blend two values based on reality ratio
591    ///
592    /// Simple blending strategy: weighted average for numbers, weighted selection for strings/booleans
593    fn blend_values(&self, synthetic: &Value, other: &Value, ratio: f64) -> Result<Value> {
594        match (synthetic, other) {
595            // Both numbers - weighted average
596            (Value::Number(syn_num), Value::Number(other_num)) => {
597                if let (Some(syn_f64), Some(other_f64)) = (syn_num.as_f64(), other_num.as_f64()) {
598                    // Blend: synthetic * (1 - ratio) + other * ratio
599                    // But adjust ratio for medium reality (0.3-0.7) to favor recorded
600                    let adjusted_ratio = if ratio < 0.7 {
601                        // Medium reality: map 0.3-0.7 to 0.0-1.0 for recorded blending
602                        (ratio - 0.3) / 0.4
603                    } else {
604                        // High reality: map 0.7-1.0 to 0.0-1.0 for real blending
605                        (ratio - 0.7) / 0.3
606                    };
607                    let blended = syn_f64 * (1.0 - adjusted_ratio) + other_f64 * adjusted_ratio;
608                    Ok(Value::Number(
609                        serde_json::Number::from_f64(blended).unwrap_or(syn_num.clone()),
610                    ))
611                } else {
612                    Ok(synthetic.clone())
613                }
614            }
615            // Both strings - weighted selection
616            (Value::String(_), Value::String(other_str)) => {
617                let adjusted_ratio = if ratio < 0.7 {
618                    (ratio - 0.3) / 0.4
619                } else {
620                    (ratio - 0.7) / 0.3
621                };
622                if adjusted_ratio >= 0.5 {
623                    Ok(Value::String(other_str.clone()))
624                } else {
625                    Ok(synthetic.clone())
626                }
627            }
628            // Both booleans - weighted selection
629            (Value::Bool(_), Value::Bool(other_bool)) => {
630                let adjusted_ratio = if ratio < 0.7 {
631                    (ratio - 0.3) / 0.4
632                } else {
633                    (ratio - 0.7) / 0.3
634                };
635                if adjusted_ratio >= 0.5 {
636                    Ok(Value::Bool(*other_bool))
637                } else {
638                    Ok(synthetic.clone())
639                }
640            }
641            // Type mismatch - prefer other if ratio is high enough
642            _ => {
643                let adjusted_ratio = if ratio < 0.7 {
644                    (ratio - 0.3) / 0.4
645                } else {
646                    (ratio - 0.7) / 0.3
647                };
648                if adjusted_ratio >= 0.5 {
649                    Ok(other.clone())
650                } else {
651                    Ok(synthetic.clone())
652                }
653            }
654        }
655    }
656
657    /// Generate traits from a persona's backstory
658    ///
659    /// Analyzes the backstory to extract or infer trait values that align
660    /// with the narrative. This ensures traits are coherent with the backstory.
661    pub fn generate_traits_from_backstory(
662        &self,
663        persona: &PersonaProfile,
664    ) -> Result<HashMap<String, String>> {
665        let mut inferred_traits = HashMap::new();
666
667        // If no backstory exists, return empty traits
668        let backstory = match persona.get_backstory() {
669            Some(bs) => bs,
670            None => return Ok(inferred_traits),
671        };
672
673        let backstory_lower = backstory.to_lowercase();
674
675        // Domain-specific trait inference from backstory
676        match persona.domain {
677            Domain::Finance => {
678                // Infer spending level from backstory keywords
679                if backstory_lower.contains("high-spending")
680                    || backstory_lower.contains("high spending")
681                    || backstory_lower.contains("big spender")
682                {
683                    inferred_traits.insert("spending_level".to_string(), "high".to_string());
684                } else if backstory_lower.contains("conservative")
685                    || backstory_lower.contains("low spending")
686                    || backstory_lower.contains("frugal")
687                {
688                    inferred_traits
689                        .insert("spending_level".to_string(), "conservative".to_string());
690                } else if backstory_lower.contains("moderate") {
691                    inferred_traits.insert("spending_level".to_string(), "moderate".to_string());
692                }
693
694                // Infer account type
695                if backstory_lower.contains("premium") {
696                    inferred_traits.insert("account_type".to_string(), "premium".to_string());
697                } else if backstory_lower.contains("business") {
698                    inferred_traits.insert("account_type".to_string(), "business".to_string());
699                } else if backstory_lower.contains("savings") {
700                    inferred_traits.insert("account_type".to_string(), "savings".to_string());
701                } else if backstory_lower.contains("checking") {
702                    inferred_traits.insert("account_type".to_string(), "checking".to_string());
703                }
704
705                // Extract currency if mentioned
706                let currencies = ["usd", "eur", "gbp", "jpy", "cny"];
707                for currency in &currencies {
708                    if backstory_lower.contains(currency) {
709                        inferred_traits
710                            .insert("preferred_currency".to_string(), currency.to_uppercase());
711                        break;
712                    }
713                }
714
715                // Infer account age
716                if backstory_lower.contains("long-term") || backstory_lower.contains("long term") {
717                    inferred_traits.insert("account_age".to_string(), "long_term".to_string());
718                } else if backstory_lower.contains("established") {
719                    inferred_traits.insert("account_age".to_string(), "established".to_string());
720                } else if backstory_lower.contains("new") {
721                    inferred_traits.insert("account_age".to_string(), "new".to_string());
722                }
723            }
724            Domain::Ecommerce => {
725                // Infer customer segment
726                if backstory_lower.contains("vip") {
727                    inferred_traits.insert("customer_segment".to_string(), "VIP".to_string());
728                } else if backstory_lower.contains("new") {
729                    inferred_traits.insert("customer_segment".to_string(), "new".to_string());
730                } else {
731                    inferred_traits.insert("customer_segment".to_string(), "regular".to_string());
732                }
733
734                // Infer purchase frequency
735                if backstory_lower.contains("frequent") {
736                    inferred_traits
737                        .insert("purchase_frequency".to_string(), "frequent".to_string());
738                } else if backstory_lower.contains("occasional") {
739                    inferred_traits
740                        .insert("purchase_frequency".to_string(), "occasional".to_string());
741                } else if backstory_lower.contains("regular") {
742                    inferred_traits.insert("purchase_frequency".to_string(), "regular".to_string());
743                }
744
745                // Extract category if mentioned
746                let categories = ["electronics", "clothing", "books", "home", "sports"];
747                for category in &categories {
748                    if backstory_lower.contains(category) {
749                        inferred_traits
750                            .insert("preferred_category".to_string(), category.to_string());
751                        break;
752                    }
753                }
754
755                // Infer shipping preference
756                if backstory_lower.contains("express") || backstory_lower.contains("overnight") {
757                    inferred_traits.insert("preferred_shipping".to_string(), "express".to_string());
758                } else if backstory_lower.contains("standard") {
759                    inferred_traits
760                        .insert("preferred_shipping".to_string(), "standard".to_string());
761                }
762            }
763            Domain::Healthcare => {
764                // Infer insurance type
765                if backstory_lower.contains("private") {
766                    inferred_traits.insert("insurance_type".to_string(), "private".to_string());
767                } else if backstory_lower.contains("medicare") {
768                    inferred_traits.insert("insurance_type".to_string(), "medicare".to_string());
769                } else if backstory_lower.contains("medicaid") {
770                    inferred_traits.insert("insurance_type".to_string(), "medicaid".to_string());
771                } else if backstory_lower.contains("uninsured") {
772                    inferred_traits.insert("insurance_type".to_string(), "uninsured".to_string());
773                }
774
775                // Extract blood type if mentioned
776                let blood_types = ["a+", "a-", "b+", "b-", "ab+", "ab-", "o+", "o-"];
777                for blood_type in &blood_types {
778                    if backstory_lower.contains(blood_type) {
779                        inferred_traits.insert("blood_type".to_string(), blood_type.to_uppercase());
780                        break;
781                    }
782                }
783
784                // Infer age group
785                if backstory_lower.contains("pediatric") || backstory_lower.contains("child") {
786                    inferred_traits.insert("age_group".to_string(), "pediatric".to_string());
787                } else if backstory_lower.contains("senior") || backstory_lower.contains("elderly")
788                {
789                    inferred_traits.insert("age_group".to_string(), "senior".to_string());
790                } else {
791                    inferred_traits.insert("age_group".to_string(), "adult".to_string());
792                }
793
794                // Infer visit frequency
795                if backstory_lower.contains("frequent") {
796                    inferred_traits.insert("visit_frequency".to_string(), "frequent".to_string());
797                } else if backstory_lower.contains("regular") {
798                    inferred_traits.insert("visit_frequency".to_string(), "regular".to_string());
799                } else if backstory_lower.contains("occasional") {
800                    inferred_traits.insert("visit_frequency".to_string(), "occasional".to_string());
801                } else if backstory_lower.contains("rare") {
802                    inferred_traits.insert("visit_frequency".to_string(), "rare".to_string());
803                }
804
805                // Infer chronic conditions
806                if backstory_lower.contains("multiple") || backstory_lower.contains("several") {
807                    inferred_traits
808                        .insert("chronic_conditions".to_string(), "multiple".to_string());
809                } else if backstory_lower.contains("single") || backstory_lower.contains("one") {
810                    inferred_traits.insert("chronic_conditions".to_string(), "single".to_string());
811                } else if backstory_lower.contains("none")
812                    || backstory_lower.contains("no conditions")
813                {
814                    inferred_traits.insert("chronic_conditions".to_string(), "none".to_string());
815                }
816            }
817            _ => {
818                // For other domains, minimal inference
819            }
820        }
821
822        Ok(inferred_traits)
823    }
824
825    /// Apply persona traits to influence generated values
826    ///
827    /// Modifies the generated value based on persona traits. For example,
828    /// a high-spending persona might generate larger transaction amounts.
829    /// If the persona has a backstory, traits inferred from the backstory
830    /// are also considered.
831    fn apply_persona_traits(
832        &self,
833        persona: &PersonaProfile,
834        field_type: &str,
835        value: Value,
836        _rng: &mut StdRng,
837    ) -> Result<Value> {
838        // If persona has a backstory but is missing traits, try to infer them
839        let mut effective_persona = persona.clone();
840        if persona.has_backstory() && persona.traits.is_empty() {
841            if let Ok(inferred_traits) = self.generate_traits_from_backstory(persona) {
842                for (key, val) in inferred_traits {
843                    effective_persona.set_trait(key, val);
844                }
845            }
846        }
847
848        match effective_persona.domain {
849            Domain::Finance => self.apply_finance_traits(&effective_persona, field_type, value),
850            Domain::Ecommerce => self.apply_ecommerce_traits(&effective_persona, field_type, value),
851            Domain::Healthcare => {
852                self.apply_healthcare_traits(&effective_persona, field_type, value)
853            }
854            _ => Ok(value), // For other domains, return value as-is for now
855        }
856    }
857
858    /// Apply finance-specific persona traits
859    fn apply_finance_traits(
860        &self,
861        persona: &PersonaProfile,
862        field_type: &str,
863        value: Value,
864    ) -> Result<Value> {
865        match field_type {
866            "amount" | "balance" | "transaction_amount" => {
867                // Adjust amount based on spending level trait
868                if let Some(spending_level) = persona.get_trait("spending_level") {
869                    let multiplier = match spending_level.as_str() {
870                        "high" => 2.0,
871                        "moderate" => 1.0,
872                        "conservative" | "low" => 0.5,
873                        _ => 1.0,
874                    };
875
876                    if let Some(num) = value.as_f64() {
877                        return Ok(Value::Number(
878                            serde_json::Number::from_f64(num * multiplier)
879                                .unwrap_or_else(|| serde_json::Number::from(0)),
880                        ));
881                    }
882                }
883                Ok(value)
884            }
885            "currency" => {
886                // Use preferred currency if trait exists
887                if let Some(currency) = persona.get_trait("preferred_currency") {
888                    return Ok(Value::String(currency.clone()));
889                }
890                Ok(value)
891            }
892            "account_type" => {
893                // Use account type trait if exists
894                if let Some(account_type) = persona.get_trait("account_type") {
895                    return Ok(Value::String(account_type.clone()));
896                }
897                Ok(value)
898            }
899            _ => Ok(value),
900        }
901    }
902
903    /// Apply e-commerce-specific persona traits
904    fn apply_ecommerce_traits(
905        &self,
906        persona: &PersonaProfile,
907        field_type: &str,
908        value: Value,
909    ) -> Result<Value> {
910        match field_type {
911            "price" | "order_total" => {
912                // Adjust price based on customer segment
913                if let Some(segment) = persona.get_trait("customer_segment") {
914                    let multiplier = match segment.as_str() {
915                        "VIP" => 1.5,
916                        "regular" => 1.0,
917                        "new" => 0.7,
918                        _ => 1.0,
919                    };
920
921                    if let Some(num) = value.as_f64() {
922                        return Ok(Value::Number(
923                            serde_json::Number::from_f64(num * multiplier)
924                                .unwrap_or_else(|| serde_json::Number::from(0)),
925                        ));
926                    }
927                }
928                Ok(value)
929            }
930            "shipping_method" => {
931                // Use preferred shipping method if trait exists
932                if let Some(shipping) = persona.get_trait("preferred_shipping") {
933                    return Ok(Value::String(shipping.clone()));
934                }
935                Ok(value)
936            }
937            _ => Ok(value),
938        }
939    }
940
941    /// Apply healthcare-specific persona traits
942    fn apply_healthcare_traits(
943        &self,
944        persona: &PersonaProfile,
945        field_type: &str,
946        value: Value,
947    ) -> Result<Value> {
948        match field_type {
949            "insurance_type" => {
950                // Use insurance type trait if exists
951                if let Some(insurance) = persona.get_trait("insurance_type") {
952                    return Ok(Value::String(insurance.clone()));
953                }
954                Ok(value)
955            }
956            "blood_type" => {
957                // Use blood type trait if exists
958                if let Some(blood_type) = persona.get_trait("blood_type") {
959                    return Ok(Value::String(blood_type.clone()));
960                }
961                Ok(value)
962            }
963            _ => Ok(value),
964        }
965    }
966}
967
968#[cfg(test)]
969mod tests {
970    use super::*;
971
972    #[test]
973    fn test_persona_profile_new() {
974        let persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
975        assert_eq!(persona.id, "user123");
976        assert_eq!(persona.domain, Domain::Finance);
977        assert!(persona.traits.is_empty());
978        assert!(persona.seed > 0);
979    }
980
981    #[test]
982    fn test_persona_profile_deterministic_seed() {
983        let persona1 = PersonaProfile::new("user123".to_string(), Domain::Finance);
984        let persona2 = PersonaProfile::new("user123".to_string(), Domain::Finance);
985
986        // Same ID and domain should produce same seed
987        assert_eq!(persona1.seed, persona2.seed);
988    }
989
990    #[test]
991    fn test_persona_profile_different_seeds() {
992        let persona1 = PersonaProfile::new("user123".to_string(), Domain::Finance);
993        let persona2 = PersonaProfile::new("user456".to_string(), Domain::Finance);
994
995        // Different IDs should produce different seeds
996        assert_ne!(persona1.seed, persona2.seed);
997    }
998
999    #[test]
1000    fn test_persona_profile_traits() {
1001        let mut persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
1002        persona.set_trait("spending_level".to_string(), "high".to_string());
1003
1004        assert_eq!(persona.get_trait("spending_level"), Some(&"high".to_string()));
1005        assert_eq!(persona.get_trait("nonexistent"), None);
1006    }
1007
1008    #[test]
1009    fn test_persona_registry_get_or_create() {
1010        let registry = PersonaRegistry::new();
1011
1012        let persona1 = registry.get_or_create_persona("user123".to_string(), Domain::Finance);
1013        let persona2 = registry.get_or_create_persona("user123".to_string(), Domain::Finance);
1014
1015        // Should return the same persona
1016        assert_eq!(persona1.id, persona2.id);
1017        assert_eq!(persona1.seed, persona2.seed);
1018    }
1019
1020    #[test]
1021    fn test_persona_registry_default_traits() {
1022        let mut default_traits = HashMap::new();
1023        default_traits.insert("spending_level".to_string(), "high".to_string());
1024
1025        let registry = PersonaRegistry::with_default_traits(default_traits);
1026        let persona = registry.get_or_create_persona("user123".to_string(), Domain::Finance);
1027
1028        assert_eq!(persona.get_trait("spending_level"), Some(&"high".to_string()));
1029    }
1030
1031    #[test]
1032    fn test_persona_registry_update() {
1033        let registry = PersonaRegistry::new();
1034        registry.get_or_create_persona("user123".to_string(), Domain::Finance);
1035
1036        let mut traits = HashMap::new();
1037        traits.insert("spending_level".to_string(), "low".to_string());
1038
1039        registry.update_persona("user123", traits).unwrap();
1040
1041        let persona = registry.get_persona("user123").unwrap();
1042        assert_eq!(persona.get_trait("spending_level"), Some(&"low".to_string()));
1043    }
1044
1045    #[test]
1046    fn test_persona_generator_finance_traits() {
1047        let generator = PersonaGenerator::new(Domain::Finance);
1048        let mut persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
1049        persona.set_trait("spending_level".to_string(), "high".to_string());
1050
1051        // Generate amount - should be influenced by high spending level
1052        let value = generator.generate_for_persona(&persona, "amount").unwrap();
1053        assert!(value.is_string() || value.is_number());
1054    }
1055
1056    #[test]
1057    fn test_persona_generator_consistency() {
1058        let generator = PersonaGenerator::new(Domain::Finance);
1059        let persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
1060
1061        // Generate multiple values - should be consistent due to deterministic seed
1062        let value1 = generator.generate_for_persona(&persona, "amount").unwrap();
1063        let value2 = generator.generate_for_persona(&persona, "amount").unwrap();
1064
1065        // Note: Due to how domain generator works, values might differ,
1066        // but the seed ensures the RNG state is consistent
1067        assert!(value1.is_string() || value1.is_number());
1068        assert!(value2.is_string() || value2.is_number());
1069    }
1070
1071    #[test]
1072    fn test_persona_backstory() {
1073        let mut persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
1074        assert!(!persona.has_backstory());
1075        assert_eq!(persona.get_backstory(), None);
1076
1077        persona
1078            .set_backstory("High-spending finance professional with premium account".to_string());
1079        assert!(persona.has_backstory());
1080        assert!(persona.get_backstory().is_some());
1081        assert!(persona.get_backstory().unwrap().contains("High-spending"));
1082    }
1083
1084    #[test]
1085    fn test_persona_relationships() {
1086        let mut persona = PersonaProfile::new("user123".to_string(), Domain::Finance);
1087
1088        // Add relationships
1089        persona.add_relationship("owns_devices".to_string(), "device1".to_string());
1090        persona.add_relationship("owns_devices".to_string(), "device2".to_string());
1091        persona.add_relationship("belongs_to_org".to_string(), "org1".to_string());
1092
1093        // Test getting relationships
1094        let devices = persona.get_related_personas("owns_devices");
1095        assert_eq!(devices.len(), 2);
1096        assert!(devices.contains(&"device1".to_string()));
1097        assert!(devices.contains(&"device2".to_string()));
1098
1099        let orgs = persona.get_related_personas("belongs_to_org");
1100        assert_eq!(orgs.len(), 1);
1101        assert_eq!(orgs[0], "org1");
1102
1103        // Test relationship types
1104        let types = persona.get_relationship_types();
1105        assert_eq!(types.len(), 2);
1106        assert!(types.contains(&"owns_devices".to_string()));
1107        assert!(types.contains(&"belongs_to_org".to_string()));
1108
1109        // Test removing relationship
1110        assert!(persona.remove_relationship("owns_devices", "device1"));
1111        let devices_after = persona.get_related_personas("owns_devices");
1112        assert_eq!(devices_after.len(), 1);
1113        assert_eq!(devices_after[0], "device2");
1114    }
1115
1116    #[test]
1117    fn test_persona_registry_relationships() {
1118        let registry = PersonaRegistry::new();
1119
1120        // Create personas
1121        let _user = registry.get_or_create_persona("user123".to_string(), Domain::Finance);
1122        let _device = registry.get_or_create_persona("device1".to_string(), Domain::Iot);
1123        let _org = registry.get_or_create_persona("org1".to_string(), Domain::General);
1124
1125        // Add relationships
1126        registry
1127            .add_relationship("user123", "owns_devices".to_string(), "device1".to_string())
1128            .unwrap();
1129        registry
1130            .add_relationship("user123", "belongs_to_org".to_string(), "org1".to_string())
1131            .unwrap();
1132
1133        // Test getting related personas
1134        let related_devices = registry.get_related_personas("user123", "owns_devices").unwrap();
1135        assert_eq!(related_devices.len(), 1);
1136        assert_eq!(related_devices[0].id, "device1");
1137
1138        // Test reverse lookup
1139        let owners = registry.find_personas_with_relationship_to("device1", "owns_devices");
1140        assert_eq!(owners.len(), 1);
1141        assert_eq!(owners[0].id, "user123");
1142    }
1143}