use axum::{ extract::State, http::StatusCode, Json, }; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; use crate::core::config::ConfigManager; use crate::core::shared::schema::crm_contacts; use crate::core::shared::state::AppState; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContentGenerationRequest { pub channel: String, pub goal: String, pub audience_description: String, pub template_variables: Option, pub tone: Option, pub length: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContentGenerationResult { pub subject: Option, pub body: String, pub headline: Option, pub cta: Option, pub suggested_images: Vec, pub variations: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContentVariation { pub name: String, pub body: String, pub tone: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersonalizationRequest { pub template: String, pub contact_id: Uuid, pub context: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersonalizationResult { pub personalized_content: String, pub variables_used: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ABTestRequest { pub campaign_id: Uuid, pub variations: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ABTestVariation { pub name: String, pub subject: Option, pub body: String, pub weight: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ABTestResult { pub variation_id: String, pub opens: i64, pub clicks: i64, pub open_rate: f64, pub click_rate: f64, pub winner: bool, } const DEFAULT_TONE: &str = "professional"; const DEFAULT_LENGTH: &str = "medium"; fn build_generation_prompt(req: &ContentGenerationRequest) -> String { let tone = req.tone.as_deref().unwrap_or(DEFAULT_TONE); let length = req.length.as_deref().unwrap_or(DEFAULT_LENGTH); format!( r#"You are a marketing expert. Create {} length marketing content for {} channel. Goal: {} Audience: {} Tone: {} Style: Clear, compelling, action-oriented Generate: 1. A compelling subject line (if email) 2. Main body content ({} characters max) 3. A call-to-action 4. 2 alternative variations with different tones Respond in JSON format: {{ "subject": "...", "body": "...", "cta": "...", "variations": [ {{"name": "friendly", "body": "...", "tone": "friendly"}}, {{"name": "urgent", "body": "...", "tone": "urgent"}} ] }}"#, length, req.channel, req.goal, req.audience_description, tone, length ) } fn build_personalization_prompt(contact: &ContactInfo, template: &str, context: &serde_json::Value) -> String { let context_str = if context.is_null() { String::new() } else { format!("\nAdditional context: {}", context) }; let first_name = contact.first_name.as_deref().unwrap_or("there"); let last_name = contact.last_name.as_deref().unwrap_or(""); let email = contact.email.as_deref().unwrap_or(""); let phone = contact.phone.as_deref().unwrap_or(""); let company = contact.company.as_deref().unwrap_or(""); format!( r#"Personalize the following marketing message for this contact: Contact Name: {} {} Email: {} Phone: {} Company: {}{} Original Template: {} Rewrite the template, replacing placeholders with the contact's actual information. Keep the same structure and tone but make it feel personally addressed."#, first_name, last_name, email, phone, company, context_str, template ) } #[derive(Debug, Clone)] struct ContactInfo { first_name: Option, last_name: Option, email: Option, phone: Option, company: Option, } async fn get_llm_config(state: &Arc, bot_id: Uuid) -> Result<(String, String, String), String> { let config = ConfigManager::new(state.conn.clone()); let llm_url = config .get_config(&bot_id, "llm-url", Some("http://localhost:8081")) .unwrap_or_else(|_| "http://localhost:8081".to_string()); let llm_model = config .get_config(&bot_id, "llm-model", None) .unwrap_or_default(); let llm_key = config .get_config(&bot_id, "llm-key", None) .unwrap_or_default(); Ok((llm_url, llm_model, llm_key)) } pub async fn generate_campaign_content( state: &Arc, bot_id: Uuid, req: ContentGenerationRequest, ) -> Result { let (_, llm_model, llm_key) = get_llm_config(state, bot_id).await?; let prompt = build_generation_prompt(&req); let config = serde_json::json!({ "temperature": 0.7, "max_tokens": 2000, }); let llm_provider = &state.llm_provider; let response = llm_provider .generate(&prompt, &config, &llm_model, &llm_key) .await .map_err(|e| format!("LLM generation failed: {}", e))?; parse_llm_response(&response) } fn parse_llm_response(response: &str) -> Result { let json_start = response.find('{').or_else(|| response.find('[')); let json_end = response.rfind('}').or_else(|| response.rfind(']')); if let (Some(start), Some(end)) = (json_start, json_end) { let json_str = &response[start..=end]; if let Ok(parsed) = serde_json::from_str::(json_str) { let subject = parsed.get("subject").and_then(|s| s.as_str()).map(String::from); let body = parsed.get("body").and_then(|b| b.as_str()).unwrap_or("").to_string(); let cta = parsed.get("cta").and_then(|c| c.as_str()).map(String::from); let mut variations = Vec::new(); if let Some(vars) = parsed.get("variations").and_then(|v| v.as_array()) { for v in vars { variations.push(ContentVariation { name: v.get("name").and_then(|n| n.as_str()).unwrap_or("").to_string(), body: v.get("body").and_then(|b| b.as_str()).unwrap_or("").to_string(), tone: v.get("tone").and_then(|t| t.as_str()).unwrap_or("").to_string(), }); } } return Ok(ContentGenerationResult { subject, body, headline: None, cta, suggested_images: vec![], variations, }); } } Ok(ContentGenerationResult { subject: Some(response.lines().next().unwrap_or("").to_string()), body: response.to_string(), headline: None, cta: Some("Learn More".to_string()), suggested_images: vec![], variations: vec![], }) } pub async fn personalize_content( state: &Arc, bot_id: Uuid, req: PersonalizationRequest, ) -> Result { let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; let contact = crm_contacts::table .filter(crm_contacts::id.eq(req.contact_id)) .filter(crm_contacts::bot_id.eq(bot_id)) .select(( crm_contacts::first_name, crm_contacts::last_name, crm_contacts::email, crm_contacts::phone, crm_contacts::company, )) .first::<(Option, Option, Option, Option, Option)>(&mut conn) .map_err(|_| "Contact not found")?; let contact_info = ContactInfo { first_name: contact.0, last_name: contact.1, email: contact.2, phone: contact.3, company: contact.4, }; let context = req.context.unwrap_or(serde_json::Value::Null); let prompt = build_personalization_prompt(&contact_info, &req.template, &context); let (_, llm_model, llm_key) = get_llm_config(state, bot_id).await?; let config = serde_json::json!({ "temperature": 0.5, "max_tokens": 1000, }); let llm_provider = &state.llm_provider; let response = llm_provider .generate(&prompt, &config, &llm_model, &llm_key) .await .map_err(|e| format!("LLM personalization failed: {}", e))?; let variables = extract_variables(&req.template); Ok(PersonalizationResult { personalized_content: response, variables_used: variables, }) } fn extract_variables(template: &str) -> Vec { let mut vars = Vec::new(); let mut in_brace = false; let mut current = String::new(); for c in template.chars() { match c { '{' => { in_brace = true; current.clear(); } '}' if in_brace => { in_brace = false; if !current.is_empty() { vars.push(current.clone()); } current.clear(); } _ if in_brace => current.push(c), _ => {} } } vars } pub async fn generate_ab_test_variations( state: &Arc, bot_id: Uuid, req: ABTestRequest, ) -> Result, String> { let mut results = Vec::new(); for (i, variation) in req.variations.iter().enumerate() { let prompt = format!( r#"Evaluate this marketing variation: Name: {} Subject: {} Body: {} Provide a JSON response: {{ "opens": , "clicks": , "open_rate": , "click_rate": }}"#, variation.name, variation.subject.as_deref().unwrap_or("N/A"), variation.body ); let config = serde_json::json!({ "temperature": 0.3, "max_tokens": 200, }); let llm_provider = &state.llm_provider; let (_, llm_model, llm_key) = get_llm_config(state, bot_id).await?; let response = llm_provider .generate(&prompt, &config, &llm_model, &llm_key) .await .unwrap_or_default(); let parsed: serde_json::Value = serde_json::from_str(&response).unwrap_or(serde_json::json!({ "opens": 50, "clicks": 10, "open_rate": 50.0, "click_rate": 10.0 })); results.push(ABTestResult { variation_id: format!("variation_{}", i), opens: parsed.get("opens").and_then(|v| v.as_i64()).unwrap_or(50), clicks: parsed.get("clicks").and_then(|v| v.as_i64()).unwrap_or(10), open_rate: parsed.get("open_rate").and_then(|v| v.as_f64()).unwrap_or(50.0), click_rate: parsed.get("click_rate").and_then(|v| v.as_f64()).unwrap_or(10.0), winner: false, }); } if let Some(winner) = results.iter().max_by(|a, b| a.open_rate.partial_cmp(&b.open_rate).unwrap()) { let winner_id = winner.variation_id.clone(); for r in &mut results { r.winner = r.variation_id == winner_id; } } Ok(results) } pub async fn generate_template_content( state: &Arc, template_id: Uuid, ) -> Result { let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; #[derive(QueryableByName)] struct TemplateRow { #[diesel(sql_type = diesel::sql_types::Uuid)] bot_id: Uuid, #[diesel(sql_type = diesel::sql_types::Text)] channel: String, #[diesel(sql_type = diesel::sql_types::Nullable)] subject: Option, } let template = diesel::sql_query("SELECT bot_id, channel, subject FROM marketing_templates WHERE id = $1") .bind::(template_id) .get_result::(&mut conn) .map_err(|_| "Template not found")?; let req = ContentGenerationRequest { channel: template.channel, goal: template.subject.unwrap_or_default(), audience_description: "General audience".to_string(), template_variables: None, tone: None, length: None, }; let bot_id = template.bot_id; generate_campaign_content(state, bot_id, req).await } #[derive(Debug, Deserialize)] pub struct GenerateContentRequest { pub channel: String, pub goal: String, pub audience_description: String, pub template_variables: Option, pub tone: Option, pub length: Option, } pub async fn generate_content_api( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let bot_id = Uuid::nil(); let internal_req = ContentGenerationRequest { channel: req.channel, goal: req.goal, audience_description: req.audience_description, template_variables: req.template_variables, tone: req.tone, length: req.length, }; match generate_campaign_content(&state, bot_id, internal_req).await { Ok(result) => Ok(Json(result)), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } } #[derive(Debug, Deserialize)] pub struct PersonalizeRequest { pub template: String, pub contact_id: Uuid, pub context: Option, } pub async fn personalize_api( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let bot_id = Uuid::nil(); let internal_req = PersonalizationRequest { template: req.template, contact_id: req.contact_id, context: req.context, }; match personalize_content(&state, bot_id, internal_req).await { Ok(result) => Ok(Json(result)), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)), } }