Skip to main content

mockforge_intelligence/ai_studio/
persona_generator.rs

1//! AI-powered persona generator
2//!
3//! This module provides functionality to generate and tweak personas using AI.
4//! It creates personas with realistic traits, backstories, and lifecycle configurations
5//! based on natural language descriptions.
6
7use crate::ai_studio::artifact_freezer::{ArtifactFreezer, FreezeMetadata};
8use crate::ai_studio::config::DeterministicModeConfig;
9use crate::intelligent_behavior::llm_client::LlmClient;
10use crate::intelligent_behavior::types::LlmGenerationRequest;
11use crate::intelligent_behavior::IntelligentBehaviorConfig;
12use mockforge_foundation::Result;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::collections::hash_map::DefaultHasher;
16use std::collections::HashMap;
17use std::hash::{Hash, Hasher};
18
19/// Persona generator for creating personas from descriptions
20pub struct PersonaGenerator {
21    /// LLM client for generating persona details
22    llm_client: LlmClient,
23    /// Configuration (for accessing LLM provider/model info)
24    config: IntelligentBehaviorConfig,
25}
26
27impl PersonaGenerator {
28    /// Create a new persona generator with default configuration
29    pub fn new() -> Self {
30        let config = IntelligentBehaviorConfig::default();
31        Self {
32            llm_client: LlmClient::new(config.behavior_model.clone()),
33            config,
34        }
35    }
36
37    /// Create a new persona generator with custom configuration
38    pub fn with_config(config: IntelligentBehaviorConfig) -> Self {
39        Self {
40            llm_client: LlmClient::new(config.behavior_model.clone()),
41            config,
42        }
43    }
44
45    /// Generate a persona from natural language description
46    ///
47    /// This method uses AI to generate a complete persona profile including:
48    /// - Realistic traits based on the description
49    /// - A narrative backstory
50    /// - Appropriate lifecycle configuration
51    /// - Domain-specific characteristics
52    ///
53    /// In deterministic mode (ai_mode = generate_once_freeze), this method will
54    /// first check for frozen artifacts before generating new ones.
55    pub async fn generate(
56        &self,
57        request: &PersonaGenerationRequest,
58        ai_mode: Option<crate::ai_studio::config::AiMode>,
59        deterministic_config: Option<&DeterministicModeConfig>,
60    ) -> Result<PersonaGenerationResponse> {
61        // In deterministic mode, check for frozen artifacts first
62        if ai_mode == Some(crate::ai_studio::config::AiMode::GenerateOnceFreeze) {
63            let freezer = ArtifactFreezer::new();
64
65            // Create identifier from description hash
66            let mut hasher = DefaultHasher::new();
67            request.description.hash(&mut hasher);
68            let description_hash = format!("{:x}", hasher.finish());
69
70            // Try to load frozen artifact
71            if let Some(frozen) = freezer.load_frozen("persona", Some(&description_hash)).await? {
72                // Extract persona from frozen content (remove metadata)
73                let mut persona = frozen.content.clone();
74                if let Some(obj) = persona.as_object_mut() {
75                    obj.remove("_frozen_metadata");
76                }
77
78                return Ok(PersonaGenerationResponse {
79                    persona: Some(persona),
80                    message: format!(
81                        "Loaded frozen persona artifact from {} (deterministic mode)",
82                        frozen.path
83                    ),
84                    frozen_artifact: Some(frozen),
85                });
86            }
87        }
88        // Build system prompt for persona generation
89        let system_prompt = r#"You are an expert at creating realistic user personas for API testing.
90Generate a complete persona profile from a natural language description.
91
92For the persona, provide:
931. A unique ID (e.g., "user:premium-001", "customer:churned-002")
942. A descriptive name
953. A business domain (e.g., "ecommerce", "saas", "banking", "healthcare")
964. Realistic traits as key-value pairs (e.g., "subscription_tier": "premium", "spending_level": "high")
975. A narrative backstory explaining the persona's characteristics
986. Optional lifecycle state (e.g., "active", "trial", "churned", "premium")
99
100Return your response as a JSON object with this structure:
101{
102  "id": "string (unique persona ID)",
103  "name": "string (descriptive name)",
104  "domain": "string (business domain)",
105  "traits": {
106    "trait_name": "trait_value",
107    ...
108  },
109  "backstory": "string (narrative description)",
110  "lifecycle_state": "string (optional, e.g., active, trial, churned)",
111  "metadata": {
112    "additional": "metadata fields"
113  }
114}
115
116Make the persona realistic and consistent. Traits should align with the description."#;
117
118        let user_prompt =
119            format!("Generate a persona from this description:\n\n{}", request.description);
120
121        let llm_request = LlmGenerationRequest {
122            system_prompt: system_prompt.to_string(),
123            user_prompt,
124            temperature: 0.7, // Higher temperature for more creative personas
125            max_tokens: 1500,
126            schema: None,
127            seed: None,
128        };
129
130        // Generate persona from LLM
131        let response = self.llm_client.generate(&llm_request).await?;
132
133        // Parse the response into a persona structure
134        let persona_json = if let Some(_id) = response.get("id") {
135            // Full persona structure
136            response.clone()
137        } else {
138            // Fallback: create a basic persona structure
139            let uuid_str = uuid::Uuid::new_v4().to_string();
140            let short_id = uuid_str.split('-').next().unwrap_or("generated");
141            serde_json::json!({
142                "id": format!("user:generated-{}", short_id),
143                "name": response.get("name").and_then(|v| v.as_str()).unwrap_or("Generated Persona"),
144                "domain": response.get("domain").and_then(|v| v.as_str()).unwrap_or("general"),
145                "traits": response.get("traits").cloned().unwrap_or_else(|| serde_json::json!({})),
146                "backstory": response.get("backstory").and_then(|v| v.as_str()).unwrap_or("AI-generated persona"),
147                "lifecycle_state": response.get("lifecycle_state").and_then(|v| v.as_str()).unwrap_or("active"),
148            })
149        };
150
151        // Convert to the simpler Persona format for response
152        let persona_name = persona_json
153            .get("name")
154            .and_then(|v| v.as_str())
155            .unwrap_or("Generated Persona")
156            .to_string();
157
158        let traits: HashMap<String, String> = persona_json
159            .get("traits")
160            .and_then(|v| v.as_object())
161            .map(|obj| {
162                obj.iter()
163                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
164                    .collect()
165            })
166            .unwrap_or_default();
167
168        // Build response persona (using the simpler Persona struct format)
169        let persona_value = serde_json::json!({
170            "name": persona_name,
171            "traits": traits,
172            "id": persona_json.get("id"),
173            "domain": persona_json.get("domain"),
174            "backstory": persona_json.get("backstory"),
175            "lifecycle_state": persona_json.get("lifecycle_state"),
176        });
177
178        // Auto-freeze if enabled
179        let frozen_artifact = if let Some(config) = deterministic_config {
180            if config.enabled && config.is_auto_freeze_enabled() {
181                let freezer = ArtifactFreezer::new();
182
183                // Calculate prompt hash
184                let mut hasher = Sha256::new();
185                hasher.update(request.description.as_bytes());
186                let prompt_hash = format!("{:x}", hasher.finalize());
187
188                // Create metadata
189                let metadata = if config.track_metadata {
190                    Some(FreezeMetadata {
191                        llm_provider: Some(self.config.behavior_model.llm_provider.clone()),
192                        llm_model: Some(self.config.behavior_model.model.clone()),
193                        llm_version: None,
194                        prompt_hash: Some(prompt_hash),
195                        output_hash: None, // Will be calculated by freezer
196                        original_prompt: Some(request.description.clone()),
197                    })
198                } else {
199                    None
200                };
201
202                let freeze_request = crate::ai_studio::artifact_freezer::FreezeRequest {
203                    artifact_type: "persona".to_string(),
204                    content: persona_value.clone(),
205                    format: config.freeze_format.clone(),
206                    path: None,
207                    metadata,
208                };
209
210                freezer.auto_freeze_if_enabled(&freeze_request, config).await?
211            } else {
212                None
213            }
214        } else {
215            None
216        };
217
218        Ok(PersonaGenerationResponse {
219            persona: Some(persona_value),
220            message: format!(
221                "Successfully generated persona '{}' with {} traits{}",
222                persona_name,
223                traits.len(),
224                if frozen_artifact.is_some() {
225                    " (auto-frozen)"
226                } else {
227                    ""
228                }
229            ),
230            frozen_artifact,
231        })
232    }
233
234    /// Tweak an existing persona based on a description
235    ///
236    /// This method modifies an existing persona by adjusting traits, adding new ones,
237    /// or updating the backstory based on the provided description.
238    pub async fn tweak(
239        &self,
240        base_persona: &serde_json::Value,
241        description: &str,
242    ) -> Result<PersonaGenerationResponse> {
243        // Build system prompt for persona tweaking
244        let system_prompt = r#"You are an expert at modifying user personas for API testing.
245Given an existing persona and a description of desired changes, update the persona accordingly.
246
247You can:
248- Modify existing traits
249- Add new traits
250- Update the backstory
251- Change lifecycle state
252- Adjust domain if needed
253
254Return the updated persona in the same JSON structure as the input."#;
255
256        let user_prompt = format!(
257            "Base persona:\n{}\n\nDesired changes: {}\n\nProvide the updated persona.",
258            serde_json::to_string_pretty(base_persona)?,
259            description
260        );
261
262        let llm_request = LlmGenerationRequest {
263            system_prompt: system_prompt.to_string(),
264            user_prompt,
265            temperature: 0.5,
266            max_tokens: 1500,
267            schema: None,
268            seed: None,
269        };
270
271        // Generate updated persona
272        let response = self.llm_client.generate(&llm_request).await?;
273
274        Ok(PersonaGenerationResponse {
275            persona: Some(response),
276            message: "Successfully updated persona".to_string(),
277            frozen_artifact: None,
278        })
279    }
280}
281
282impl Default for PersonaGenerator {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288/// Request for persona generation
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct PersonaGenerationRequest {
291    /// Natural language description
292    pub description: String,
293
294    /// Optional base persona to tweak
295    pub base_persona_id: Option<String>,
296
297    /// Workspace ID for context
298    pub workspace_id: Option<String>,
299}
300
301/// Response from persona generation
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct PersonaGenerationResponse {
304    /// Generated persona (if any)
305    pub persona: Option<serde_json::Value>,
306
307    /// Status message
308    pub message: String,
309
310    /// Frozen artifact (if auto-freeze was enabled)
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub frozen_artifact: Option<crate::ai_studio::artifact_freezer::FrozenArtifact>,
313}