mockforge_intelligence/ai_studio/
persona_generator.rs1use 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
19pub struct PersonaGenerator {
21 llm_client: LlmClient,
23 config: IntelligentBehaviorConfig,
25}
26
27impl PersonaGenerator {
28 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 pub fn with_config(config: IntelligentBehaviorConfig) -> Self {
39 Self {
40 llm_client: LlmClient::new(config.behavior_model.clone()),
41 config,
42 }
43 }
44
45 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 if ai_mode == Some(crate::ai_studio::config::AiMode::GenerateOnceFreeze) {
63 let freezer = ArtifactFreezer::new();
64
65 let mut hasher = DefaultHasher::new();
67 request.description.hash(&mut hasher);
68 let description_hash = format!("{:x}", hasher.finish());
69
70 if let Some(frozen) = freezer.load_frozen("persona", Some(&description_hash)).await? {
72 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 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, max_tokens: 1500,
126 schema: None,
127 seed: None,
128 };
129
130 let response = self.llm_client.generate(&llm_request).await?;
132
133 let persona_json = if let Some(_id) = response.get("id") {
135 response.clone()
137 } else {
138 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 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 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 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 let mut hasher = Sha256::new();
185 hasher.update(request.description.as_bytes());
186 let prompt_hash = format!("{:x}", hasher.finalize());
187
188 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, 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 pub async fn tweak(
239 &self,
240 base_persona: &serde_json::Value,
241 description: &str,
242 ) -> Result<PersonaGenerationResponse> {
243 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct PersonaGenerationRequest {
291 pub description: String,
293
294 pub base_persona_id: Option<String>,
296
297 pub workspace_id: Option<String>,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct PersonaGenerationResponse {
304 pub persona: Option<serde_json::Value>,
306
307 pub message: String,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub frozen_artifact: Option<crate::ai_studio::artifact_freezer::FrozenArtifact>,
313}