//! CRM Attendance Keywords //! //! Provides BASIC keywords for queue management, LLM-assisted attendant features, //! and customer journey tracking. These keywords enable flexible CRM automation //! directly from BASIC scripts. //! //! ## Queue Management Keywords //! //! - `GET QUEUE` - Get current queue status and items //! - `NEXT IN QUEUE` - Get next waiting conversation //! - `ASSIGN CONVERSATION` - Assign conversation to attendant //! - `RESOLVE CONVERSATION` - Mark conversation as resolved //! - `SET PRIORITY` - Change conversation priority //! //! ## Attendant Keywords //! //! - `GET ATTENDANTS` - List available attendants //! - `SET ATTENDANT STATUS` - Change attendant status //! - `GET ATTENDANT STATS` - Get performance metrics //! //! ## LLM Assist Keywords //! //! - `GET TIPS` - Generate AI tips for conversation //! - `POLISH MESSAGE` - Improve message with AI //! - `GET SMART REPLIES` - Get AI reply suggestions //! - `GET SUMMARY` - Get conversation summary //! - `ANALYZE SENTIMENT` - Analyze customer sentiment //! //! ## Customer Journey Keywords //! //! - `TAG CONVERSATION` - Add tags to conversation //! - `ADD NOTE` - Add internal note //! - `GET CUSTOMER HISTORY` - Get previous interactions //! //! ## Example Usage //! //! ```basic //! ' Check queue and assign next conversation //! queue = GET QUEUE //! TALK "Queue has " + queue.waiting + " waiting" //! //! IF queue.waiting > 0 THEN //! next_conv = NEXT IN QUEUE //! ASSIGN CONVERSATION next_conv.session_id, "att-001" //! END IF //! //! ' Use LLM assist //! tips = GET TIPS session_id, customer_message //! FOR EACH tip IN tips //! TALK tip.content //! NEXT //! //! ' Polish before sending //! polished = POLISH MESSAGE "thx for ur msg ill help u now" //! SEND polished.text TO customer //! //! ' Analyze sentiment //! sentiment = ANALYZE SENTIMENT session_id, message //! IF sentiment.escalation_risk = "high" THEN //! SET PRIORITY session_id, "urgent" //! TRANSFER TO HUMAN "support", "urgent", "High escalation risk" //! END IF //! ``` use crate::shared::models::UserSession; use crate::shared::state::AppState; use chrono::Utc; use diesel::prelude::*; use log::{debug, error, info}; use rhai::{Array, Dynamic, Engine, Map}; use std::sync::Arc; use uuid::Uuid; // Registration /// Register all CRM attendance keywords pub fn register_attendance_keywords(state: Arc, user: UserSession, engine: &mut Engine) { debug!("Registering CRM attendance keywords..."); // Queue Management register_get_queue(state.clone(), user.clone(), engine); register_next_in_queue(state.clone(), user.clone(), engine); register_assign_conversation(state.clone(), user.clone(), engine); register_resolve_conversation(state.clone(), user.clone(), engine); register_set_priority(state.clone(), user.clone(), engine); // Attendant Management register_get_attendants(state.clone(), user.clone(), engine); register_set_attendant_status(state.clone(), user.clone(), engine); register_get_attendant_stats(state.clone(), user.clone(), engine); // LLM Assist register_get_tips(state.clone(), user.clone(), engine); register_polish_message(state.clone(), user.clone(), engine); register_get_smart_replies(state.clone(), user.clone(), engine); register_get_summary(state.clone(), user.clone(), engine); register_analyze_sentiment(state.clone(), user.clone(), engine); // Customer Journey register_tag_conversation(state.clone(), user.clone(), engine); register_add_note(state.clone(), user.clone(), engine); register_get_customer_history(state.clone(), user.clone(), engine); debug!("CRM attendance keywords registered successfully"); } // Queue Management Keywords /// GET QUEUE - Get current queue status /// /// Returns queue statistics and optionally filtered items. /// /// ```basic /// ' Get queue stats /// queue = GET QUEUE /// TALK "Waiting: " + queue.waiting + ", Active: " + queue.active /// /// ' Get queue with filter /// queue = GET QUEUE "channel=whatsapp" /// queue = GET QUEUE "department=sales" /// queue = GET QUEUE "priority=high" /// ``` fn register_get_queue(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); // GET QUEUE engine.register_fn("get_queue", move || -> Dynamic { get_queue_impl(&state_clone, None) }); let state_clone2 = state.clone(); engine.register_fn("get_queue", move |filter: &str| -> Dynamic { get_queue_impl(&state_clone2, Some(filter.to_string())) }); // Also register as GET_QUEUE for compatibility let state_clone3 = state.clone(); engine .register_custom_syntax(&["GET", "QUEUE"], false, move |_context, _inputs| { Ok(get_queue_impl(&state_clone3, None)) }) .unwrap(); let state_clone4 = state.clone(); engine .register_custom_syntax( &["GET", "QUEUE", "$expr$"], false, move |context, inputs| { let filter = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(get_queue_impl(&state_clone4, Some(filter))) }, ) .unwrap(); } pub fn get_queue_impl(state: &Arc, filter: Option) -> Dynamic { let conn = state.conn.clone(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => { error!("DB connection error: {}", e); return create_error_result(&format!("DB error: {}", e)); } }; use crate::shared::models::schema::user_sessions; // Build query let mut query = user_sessions::table .filter( user_sessions::context_data .retrieve_as_text("needs_human") .eq("true"), ) .into_boxed(); // Apply filters if let Some(ref filter_str) = filter { if filter_str.contains("channel=") { let channel = filter_str.replace("channel=", ""); query = query.filter( user_sessions::context_data .retrieve_as_text("channel") .eq(channel), ); } // Add more filters as needed } let sessions: Vec = match query.load(&mut db_conn) { Ok(s) => s, Err(e) => { error!("Query error: {}", e); return create_error_result(&format!("Query error: {}", e)); } }; // Calculate stats let mut waiting = 0; let mut assigned = 0; let mut active = 0; let mut resolved = 0; for session in &sessions { let status = session .context_data .get("status") .and_then(|v| v.as_str()) .unwrap_or("waiting"); match status { "waiting" => waiting += 1, "assigned" => assigned += 1, "active" => active += 1, "resolved" => resolved += 1, _ => waiting += 1, } } let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("total".into(), Dynamic::from(sessions.len() as i64)); result.insert("waiting".into(), Dynamic::from(waiting)); result.insert("assigned".into(), Dynamic::from(assigned)); result.insert("active".into(), Dynamic::from(active)); result.insert("resolved".into(), Dynamic::from(resolved)); // Add items array let items: Array = sessions .iter() .take(20) // Limit to 20 items .map(|s| { let mut item = Map::new(); item.insert("session_id".into(), Dynamic::from(s.id.to_string())); item.insert("user_id".into(), Dynamic::from(s.user_id.to_string())); item.insert( "channel".into(), Dynamic::from( s.context_data .get("channel") .and_then(|v| v.as_str()) .unwrap_or("web") .to_string(), ), ); item.insert( "status".into(), Dynamic::from( s.context_data .get("status") .and_then(|v| v.as_str()) .unwrap_or("waiting") .to_string(), ), ); item.insert( "name".into(), Dynamic::from( s.context_data .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown") .to_string(), ), ); item.insert( "priority".into(), Dynamic::from( s.context_data .get("priority") .and_then(|v| v.as_i64()) .unwrap_or(1), ), ); Dynamic::from(item) }) .collect(); result.insert("items".into(), Dynamic::from(items)); Dynamic::from(result) }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// NEXT IN QUEUE - Get next waiting conversation /// /// ```basic /// next = NEXT IN QUEUE /// IF next.success THEN /// TALK "Next customer: " + next.name /// ASSIGN CONVERSATION next.session_id, attendant_id /// END IF /// ``` fn register_next_in_queue(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax(&["NEXT", "IN", "QUEUE"], false, move |_context, _inputs| { Ok(next_in_queue_impl(&state_clone)) }) .unwrap(); let state_clone2 = state.clone(); engine.register_fn("next_in_queue", move || -> Dynamic { next_in_queue_impl(&state_clone2) }); } pub fn next_in_queue_impl(state: &Arc) -> Dynamic { let conn = state.conn.clone(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; // Find next waiting session (ordered by priority and time) let session: Option = user_sessions::table .filter( user_sessions::context_data .retrieve_as_text("needs_human") .eq("true"), ) .filter( user_sessions::context_data .retrieve_as_text("status") .eq("waiting"), ) .order(user_sessions::created_at.asc()) .first(&mut db_conn) .optional() .unwrap_or(None); match session { Some(s) => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("session_id".into(), Dynamic::from(s.id.to_string())); result.insert("user_id".into(), Dynamic::from(s.user_id.to_string())); result.insert( "name".into(), Dynamic::from( s.context_data .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown") .to_string(), ), ); result.insert( "channel".into(), Dynamic::from( s.context_data .get("channel") .and_then(|v| v.as_str()) .unwrap_or("web") .to_string(), ), ); result.insert( "phone".into(), Dynamic::from( s.context_data .get("phone") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), ), ); Dynamic::from(result) } None => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(false)); result.insert("message".into(), Dynamic::from("Queue is empty")); Dynamic::from(result) } } }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// ASSIGN CONVERSATION - Assign conversation to attendant /// /// ```basic /// result = ASSIGN CONVERSATION session_id, attendant_id /// result = ASSIGN CONVERSATION session_id, "att-001" /// ``` fn register_assign_conversation(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["ASSIGN", "CONVERSATION", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let attendant_id = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(assign_conversation_impl( &state_clone, &session_id, &attendant_id, )) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn( "assign_conversation", move |session_id: &str, attendant_id: &str| -> Dynamic { assign_conversation_impl(&state_clone2, session_id, attendant_id) }, ); } pub fn assign_conversation_impl( state: &Arc, session_id: &str, attendant_id: &str, ) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid session ID"), }; let attendant = attendant_id.to_string(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; // Get current session let session: UserSession = match user_sessions::table.find(session_uuid).first(&mut db_conn) { Ok(s) => s, Err(_) => return create_error_result("Session not found"), }; // Update context let mut ctx = session.context_data.clone(); ctx["assigned_to"] = serde_json::json!(attendant); ctx["assigned_at"] = serde_json::json!(Utc::now().to_rfc3339()); ctx["status"] = serde_json::json!("assigned"); match diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_uuid))) .set(user_sessions::context_data.eq(&ctx)) .execute(&mut db_conn) { Ok(_) => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("session_id".into(), Dynamic::from(session_uuid.to_string())); result.insert("assigned_to".into(), Dynamic::from(attendant)); result.insert("message".into(), Dynamic::from("Conversation assigned")); Dynamic::from(result) } Err(e) => create_error_result(&format!("Update error: {}", e)), } }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// RESOLVE CONVERSATION - Mark conversation as resolved /// /// ```basic /// RESOLVE CONVERSATION session_id /// RESOLVE CONVERSATION session_id, "Issue fixed" /// ``` fn register_resolve_conversation(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["RESOLVE", "CONVERSATION", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(resolve_conversation_impl(&state_clone, &session_id, None)) }, ) .unwrap(); let state_clone2 = state.clone(); engine .register_custom_syntax( &["RESOLVE", "CONVERSATION", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let reason = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(resolve_conversation_impl( &state_clone2, &session_id, Some(reason), )) }, ) .unwrap(); let state_clone3 = state.clone(); engine.register_fn("resolve_conversation", move |session_id: &str| -> Dynamic { resolve_conversation_impl(&state_clone3, session_id, None) }); } pub fn resolve_conversation_impl( state: &Arc, session_id: &str, reason: Option, ) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid session ID"), }; let reason_clone = reason.clone(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; let session: UserSession = match user_sessions::table.find(session_uuid).first(&mut db_conn) { Ok(s) => s, Err(_) => return create_error_result("Session not found"), }; let mut ctx = session.context_data.clone(); ctx["needs_human"] = serde_json::json!(false); ctx["status"] = serde_json::json!("resolved"); ctx["resolved_at"] = serde_json::json!(Utc::now().to_rfc3339()); if let Some(r) = reason_clone { ctx["resolution_reason"] = serde_json::json!(r); } match diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_uuid))) .set(user_sessions::context_data.eq(&ctx)) .execute(&mut db_conn) { Ok(_) => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("session_id".into(), Dynamic::from(session_uuid.to_string())); result.insert("message".into(), Dynamic::from("Conversation resolved")); Dynamic::from(result) } Err(e) => create_error_result(&format!("Update error: {}", e)), } }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// SET PRIORITY - Change conversation priority /// /// ```basic /// SET PRIORITY session_id, "high" /// SET PRIORITY session_id, "urgent" /// SET PRIORITY session_id, 3 ' numeric (1=low, 2=normal, 3=high, 4=urgent) /// ``` fn register_set_priority(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["SET", "PRIORITY", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let priority = context.eval_expression_tree(&inputs[1])?; Ok(set_priority_impl(&state_clone, &session_id, priority)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn( "set_priority", move |session_id: &str, priority: &str| -> Dynamic { set_priority_impl( &state_clone2, session_id, Dynamic::from(priority.to_string()), ) }, ); } pub fn set_priority_impl(state: &Arc, session_id: &str, priority: Dynamic) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid session ID"), }; // Convert priority to numeric let priority_num: i64 = if priority.is_int() { priority.as_int().unwrap_or(2) } else { let p = priority.to_string().to_lowercase(); match p.as_str() { "low" => 1, "normal" => 2, "high" => 3, "urgent" => 4, _ => 2, } }; let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; let session: UserSession = match user_sessions::table.find(session_uuid).first(&mut db_conn) { Ok(s) => s, Err(_) => return create_error_result("Session not found"), }; let mut ctx = session.context_data.clone(); ctx["priority"] = serde_json::json!(priority_num); match diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_uuid))) .set(user_sessions::context_data.eq(&ctx)) .execute(&mut db_conn) { Ok(_) => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("priority".into(), Dynamic::from(priority_num)); Dynamic::from(result) } Err(e) => create_error_result(&format!("Update error: {}", e)), } }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } // Attendant Management Keywords /// GET ATTENDANTS - List available attendants /// /// ```basic /// attendants = GET ATTENDANTS /// FOR EACH att IN attendants.items /// TALK att.name + " - " + att.status /// NEXT /// /// ' Filter by status /// online = GET ATTENDANTS "online" /// ``` fn register_get_attendants(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax(&["GET", "ATTENDANTS"], false, move |_context, _inputs| { Ok(get_attendants_impl(&state_clone, None)) }) .unwrap(); let state_clone2 = state.clone(); engine .register_custom_syntax( &["GET", "ATTENDANTS", "$expr$"], false, move |context, inputs| { let filter = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(get_attendants_impl(&state_clone2, Some(filter))) }, ) .unwrap(); let state_clone3 = state.clone(); engine.register_fn("get_attendants", move || -> Dynamic { get_attendants_impl(&state_clone3, None) }); } pub fn get_attendants_impl(_state: &Arc, status_filter: Option) -> Dynamic { // Read from attendant.csv let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let mut attendants = Vec::new(); // Scan for attendant.csv files if let Ok(entries) = std::fs::read_dir(&work_path) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() && path.to_string_lossy().ends_with(".gbai") { let attendant_path = path.join("attendant.csv"); if attendant_path.exists() { if let Ok(content) = std::fs::read_to_string(&attendant_path) { for line in content.lines().skip(1) { let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); if parts.len() >= 4 { let mut att = Map::new(); att.insert("id".into(), Dynamic::from(parts[0].to_string())); att.insert("name".into(), Dynamic::from(parts[1].to_string())); att.insert("channel".into(), Dynamic::from(parts[2].to_string())); att.insert( "preferences".into(), Dynamic::from(parts[3].to_string()), ); att.insert("status".into(), Dynamic::from("online".to_string())); // Default if parts.len() >= 5 { att.insert( "department".into(), Dynamic::from(parts[4].to_string()), ); } attendants.push(Dynamic::from(att)); } } } } } } } // Apply filter if let Some(filter) = status_filter { attendants.retain(|att| { if let Some(map) = att.clone().try_cast::() { if let Some(status) = map.get("status") { return status.to_string().to_lowercase() == filter.to_lowercase(); } } true }); } let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("count".into(), Dynamic::from(attendants.len() as i64)); result.insert("items".into(), Dynamic::from(attendants)); Dynamic::from(result) } /// SET ATTENDANT STATUS - Change attendant status /// /// ```basic /// SET ATTENDANT STATUS "att-001", "busy" /// SET ATTENDANT STATUS attendant_id, "away" /// ``` fn register_set_attendant_status(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["SET", "ATTENDANT", "STATUS", "$expr$", "$expr$"], false, move |context, inputs| { let attendant_id = context.eval_expression_tree(&inputs[0])?.to_string(); let status = context.eval_expression_tree(&inputs[1])?.to_string(); let now = Utc::now().to_rfc3339(); let mut conn = state_clone .conn .get() .map_err(|e| format!("DB connection error: {}", e))?; let query = diesel::sql_query( "UPDATE attendants SET status = $1, updated_at = $2 WHERE id = $3", ) .bind::(&status) .bind::(&now) .bind::(&attendant_id); let rows_affected = query.execute(&mut *conn).unwrap_or(0); info!( "Set attendant {} status to {} (rows_affected={})", attendant_id, status, rows_affected ); let mut result = Map::new(); result.insert("success".into(), Dynamic::from(rows_affected > 0)); result.insert("attendant_id".into(), Dynamic::from(attendant_id)); result.insert("status".into(), Dynamic::from(status)); result.insert("rows_affected".into(), Dynamic::from(rows_affected as i64)); Ok(Dynamic::from(result)) }, ) .unwrap(); } /// GET ATTENDANT STATS - Get performance metrics /// /// ```basic /// stats = GET ATTENDANT STATS "att-001" /// TALK "Resolved: " + stats.resolved_today /// ``` fn register_get_attendant_stats(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["GET", "ATTENDANT", "STATS", "$expr$"], false, move |context, inputs| { let attendant_id = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(get_attendant_stats_impl(&state_clone, &attendant_id)) }, ) .unwrap(); } pub fn get_attendant_stats_impl(state: &Arc, attendant_id: &str) -> Dynamic { let conn = state.conn.clone(); let att_id = attendant_id.to_string(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; // Count resolved conversations today let today_start = Utc::now().date_naive().and_hms_opt(0, 0, 0).unwrap(); let resolved_today: i64 = user_sessions::table .filter( user_sessions::context_data .retrieve_as_text("assigned_to") .eq(&att_id), ) .filter( user_sessions::context_data .retrieve_as_text("status") .eq("resolved"), ) .filter(user_sessions::updated_at.ge(today_start)) .count() .get_result(&mut db_conn) .unwrap_or(0); // Count active conversations let active: i64 = user_sessions::table .filter( user_sessions::context_data .retrieve_as_text("assigned_to") .eq(&att_id), ) .filter( user_sessions::context_data .retrieve_as_text("status") .ne("resolved"), ) .count() .get_result(&mut db_conn) .unwrap_or(0); let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("attendant_id".into(), Dynamic::from(att_id)); result.insert("resolved_today".into(), Dynamic::from(resolved_today)); result.insert("active_conversations".into(), Dynamic::from(active)); Dynamic::from(result) }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } // LLM Assist Keywords /// GET TIPS - Generate AI tips for conversation /// /// ```basic /// tips = GET TIPS session_id, customer_message /// FOR EACH tip IN tips.items /// TALK tip.type + ": " + tip.content /// NEXT /// ``` fn register_get_tips(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["GET", "TIPS", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let message = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(get_tips_impl(&state_clone, &session_id, &message)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn( "get_tips", move |session_id: &str, message: &str| -> Dynamic { get_tips_impl(&state_clone2, session_id, message) }, ); } pub fn get_tips_impl(_state: &Arc, _session_id: &str, message: &str) -> Dynamic { create_fallback_tips(message) } pub fn create_fallback_tips(message: &str) -> Dynamic { let msg_lower = message.to_lowercase(); let mut tips = Vec::new(); // Urgent detection if msg_lower.contains("urgent") || msg_lower.contains("asap") || msg_lower.contains("emergency") { let mut tip = Map::new(); tip.insert("type".into(), Dynamic::from("warning")); tip.insert( "content".into(), Dynamic::from("Customer indicates urgency - prioritize quick response"), ); tip.insert("priority".into(), Dynamic::from(1_i64)); tips.push(Dynamic::from(tip)); } // Question detection if message.contains('?') { let mut tip = Map::new(); tip.insert("type".into(), Dynamic::from("intent")); tip.insert( "content".into(), Dynamic::from("Customer is asking a question - provide clear answer"), ); tip.insert("priority".into(), Dynamic::from(2_i64)); tips.push(Dynamic::from(tip)); } // Problem detection if msg_lower.contains("problem") || msg_lower.contains("issue") || msg_lower.contains("not working") { let mut tip = Map::new(); tip.insert("type".into(), Dynamic::from("action")); tip.insert( "content".into(), Dynamic::from("Customer reporting issue - acknowledge and gather details"), ); tip.insert("priority".into(), Dynamic::from(2_i64)); tips.push(Dynamic::from(tip)); } // Default tip if tips.is_empty() { let mut tip = Map::new(); tip.insert("type".into(), Dynamic::from("general")); tip.insert( "content".into(), Dynamic::from("Read carefully and respond helpfully"), ); tip.insert("priority".into(), Dynamic::from(3_i64)); tips.push(Dynamic::from(tip)); } let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("items".into(), Dynamic::from(tips)); Dynamic::from(result) } /// POLISH MESSAGE - Improve message with AI /// /// ```basic /// polished = POLISH MESSAGE "thx for contacting us, ill help u" /// TALK polished.text /// /// ' With tone /// polished = POLISH MESSAGE message, "empathetic" /// ``` fn register_polish_message(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["POLISH", "MESSAGE", "$expr$"], false, move |context, inputs| { let message = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(polish_message_impl(&state_clone, &message, "professional")) }, ) .unwrap(); let state_clone2 = state.clone(); engine .register_custom_syntax( &["POLISH", "MESSAGE", "$expr$", "$expr$"], false, move |context, inputs| { let message = context.eval_expression_tree(&inputs[0])?.to_string(); let tone = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(polish_message_impl(&state_clone2, &message, &tone)) }, ) .unwrap(); let state_clone3 = state.clone(); engine.register_fn("polish_message", move |message: &str| -> Dynamic { polish_message_impl(&state_clone3, message, "professional") }); } pub fn polish_message_impl(_state: &Arc, message: &str, _tone: &str) -> Dynamic { // Simple polishing without LLM (fallback) let mut polished = message.to_string(); // Basic improvements polished = polished .replace("thx", "Thank you") .replace("u ", "you ") .replace(" u", " you") .replace("ur ", "your ") .replace("ill ", "I'll ") .replace("dont ", "don't ") .replace("cant ", "can't ") .replace("wont ", "won't ") .replace("im ", "I'm ") .replace("ive ", "I've "); // Capitalize first letter if let Some(first_char) = polished.chars().next() { polished = first_char.to_uppercase().to_string() + &polished[1..]; } // Ensure ending punctuation if !polished.ends_with('.') && !polished.ends_with('!') && !polished.ends_with('?') { polished.push('.'); } let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("original".into(), Dynamic::from(message.to_string())); result.insert("text".into(), Dynamic::from(polished.clone())); result.insert("polished".into(), Dynamic::from(polished)); Dynamic::from(result) } /// GET SMART REPLIES - Get AI reply suggestions /// /// ```basic /// replies = GET SMART REPLIES session_id /// FOR EACH reply IN replies.items /// TALK reply.tone + ": " + reply.text /// NEXT /// ``` fn register_get_smart_replies(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["GET", "SMART", "REPLIES", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(get_smart_replies_impl(&state_clone, &session_id)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn("get_smart_replies", move |session_id: &str| -> Dynamic { get_smart_replies_impl(&state_clone2, session_id) }); } pub fn get_smart_replies_impl(_state: &Arc, _session_id: &str) -> Dynamic { // Return default replies (fallback without LLM) let mut replies = Vec::new(); let mut reply1 = Map::new(); reply1.insert( "text".into(), Dynamic::from("Thank you for reaching out! I'd be happy to help you with that."), ); reply1.insert("tone".into(), Dynamic::from("friendly")); reply1.insert("category".into(), Dynamic::from("greeting")); replies.push(Dynamic::from(reply1)); let mut reply2 = Map::new(); reply2.insert( "text".into(), Dynamic::from("I understand your concern. Let me look into this for you right away."), ); reply2.insert("tone".into(), Dynamic::from("empathetic")); reply2.insert("category".into(), Dynamic::from("acknowledgment")); replies.push(Dynamic::from(reply2)); let mut reply3 = Map::new(); reply3.insert( "text".into(), Dynamic::from("Is there anything else I can help you with today?"), ); reply3.insert("tone".into(), Dynamic::from("professional")); reply3.insert("category".into(), Dynamic::from("follow_up")); replies.push(Dynamic::from(reply3)); let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("items".into(), Dynamic::from(replies)); Dynamic::from(result) } /// GET SUMMARY - Get conversation summary /// /// ```basic /// summary = GET SUMMARY session_id /// TALK "Brief: " + summary.brief /// TALK "Key points: " + summary.key_points /// ``` fn register_get_summary(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["GET", "SUMMARY", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(get_summary_impl(&state_clone, &session_id)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn("get_summary", move |session_id: &str| -> Dynamic { get_summary_impl(&state_clone2, session_id) }); } pub fn get_summary_impl(state: &Arc, session_id: &str) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid session ID"), }; let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::message_history; // Get message count let message_count: i64 = message_history::table .filter(message_history::session_id.eq(session_uuid)) .count() .get_result(&mut db_conn) .unwrap_or(0); let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("session_id".into(), Dynamic::from(session_uuid.to_string())); result.insert("message_count".into(), Dynamic::from(message_count)); result.insert( "brief".into(), Dynamic::from(format!("Conversation with {} messages", message_count)), ); result.insert("key_points".into(), Dynamic::from(Vec::::new())); result.insert("sentiment_trend".into(), Dynamic::from("neutral")); Dynamic::from(result) }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// ANALYZE SENTIMENT - Analyze customer sentiment /// /// ```basic /// sentiment = ANALYZE SENTIMENT session_id, message /// IF sentiment.escalation_risk = "high" THEN /// SET PRIORITY session_id, "urgent" /// END IF /// ``` fn register_analyze_sentiment(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["ANALYZE", "SENTIMENT", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let message = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(analyze_sentiment_impl(&state_clone, &session_id, &message)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn( "analyze_sentiment", move |session_id: &str, message: &str| -> Dynamic { analyze_sentiment_impl(&state_clone2, session_id, message) }, ); } pub fn analyze_sentiment_impl(_state: &Arc, _session_id: &str, message: &str) -> Dynamic { // Keyword-based sentiment analysis (fallback without LLM) let msg_lower = message.to_lowercase(); let positive_words = [ "thank", "great", "perfect", "awesome", "excellent", "good", "happy", "love", ]; let negative_words = [ "angry", "frustrated", "terrible", "awful", "horrible", "hate", "disappointed", "problem", "issue", ]; let urgent_words = [ "urgent", "asap", "immediately", "emergency", "now", "critical", ]; let positive_count = positive_words .iter() .filter(|w| msg_lower.contains(*w)) .count(); let negative_count = negative_words .iter() .filter(|w| msg_lower.contains(*w)) .count(); let urgent_count = urgent_words .iter() .filter(|w| msg_lower.contains(*w)) .count(); let (overall, score, emoji) = if positive_count > negative_count { ("positive", 0.5, "😊") } else if negative_count > positive_count { ("negative", -0.5, "😟") } else { ("neutral", 0.0, "😐") }; let escalation_risk = if negative_count >= 3 { "high" } else if negative_count >= 1 { "medium" } else { "low" }; let urgency = if urgent_count >= 2 { "urgent" } else if urgent_count >= 1 { "high" } else { "normal" }; let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("overall".into(), Dynamic::from(overall)); result.insert("score".into(), Dynamic::from(score)); result.insert("emoji".into(), Dynamic::from(emoji)); result.insert("escalation_risk".into(), Dynamic::from(escalation_risk)); result.insert("urgency".into(), Dynamic::from(urgency)); Dynamic::from(result) } // Customer Journey Keywords /// TAG CONVERSATION - Add tags to conversation /// /// ```basic /// TAG CONVERSATION session_id, "billing" /// TAG CONVERSATION session_id, "vip", "urgent" /// ``` fn register_tag_conversation(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["TAG", "CONVERSATION", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let tag = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(tag_conversation_impl(&state_clone, &session_id, vec![tag])) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn( "tag_conversation", move |session_id: &str, tag: &str| -> Dynamic { tag_conversation_impl(&state_clone2, session_id, vec![tag.to_string()]) }, ); } pub fn tag_conversation_impl(state: &Arc, session_id: &str, tags: Vec) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid session ID"), }; let tags_clone = tags.clone(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; let session: UserSession = match user_sessions::table.find(session_uuid).first(&mut db_conn) { Ok(s) => s, Err(_) => return create_error_result("Session not found"), }; let mut ctx = session.context_data.clone(); // Get existing tags or create empty array let mut existing_tags: Vec = ctx .get("tags") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) .collect() }) .unwrap_or_default(); // Add new tags for tag in tags_clone { if !existing_tags.contains(&tag) { existing_tags.push(tag); } } ctx["tags"] = serde_json::json!(existing_tags); match diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_uuid))) .set(user_sessions::context_data.eq(&ctx)) .execute(&mut db_conn) { Ok(_) => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert( "tags".into(), Dynamic::from( existing_tags .iter() .map(|t| Dynamic::from(t.clone())) .collect::>(), ), ); Dynamic::from(result) } Err(e) => create_error_result(&format!("Update error: {}", e)), } }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// ADD NOTE - Add internal note to conversation /// /// ```basic /// ADD NOTE session_id, "Customer is a VIP - handle with care" /// ADD NOTE session_id, note_text, attendant_id /// ``` fn register_add_note(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["ADD", "NOTE", "$expr$", "$expr$"], false, move |context, inputs| { let session_id = context.eval_expression_tree(&inputs[0])?.to_string(); let note = context.eval_expression_tree(&inputs[1])?.to_string(); Ok(add_note_impl(&state_clone, &session_id, ¬e, None)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn("add_note", move |session_id: &str, note: &str| -> Dynamic { add_note_impl(&state_clone2, session_id, note, None) }); } pub fn add_note_impl( state: &Arc, session_id: &str, note: &str, author: Option, ) -> Dynamic { let conn = state.conn.clone(); let session_uuid = match Uuid::parse_str(session_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid session ID"), }; let note_clone = note.to_string(); let author_clone = author.clone(); let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; let session: UserSession = match user_sessions::table.find(session_uuid).first(&mut db_conn) { Ok(s) => s, Err(_) => return create_error_result("Session not found"), }; let mut ctx = session.context_data.clone(); // Get existing notes or create empty array let mut notes: Vec = ctx .get("notes") .and_then(|v| v.as_array()) .cloned() .unwrap_or_default(); // Add new note notes.push(serde_json::json!({ "text": note_clone, "author": author_clone.unwrap_or_else(|| "system".to_string()), "timestamp": Utc::now().to_rfc3339() })); ctx["notes"] = serde_json::json!(notes); match diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_uuid))) .set(user_sessions::context_data.eq(&ctx)) .execute(&mut db_conn) { Ok(_) => { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("note_count".into(), Dynamic::from(notes.len() as i64)); Dynamic::from(result) } Err(e) => create_error_result(&format!("Update error: {}", e)), } }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } /// GET CUSTOMER HISTORY - Get previous interactions /// /// ```basic /// history = GET CUSTOMER HISTORY user_id /// TALK "Previous sessions: " + history.session_count /// FOR EACH session IN history.sessions /// TALK session.channel + " - " + session.resolved_at /// NEXT /// ``` fn register_get_customer_history(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); engine .register_custom_syntax( &["GET", "CUSTOMER", "HISTORY", "$expr$"], false, move |context, inputs| { let user_id = context.eval_expression_tree(&inputs[0])?.to_string(); Ok(get_customer_history_impl(&state_clone, &user_id)) }, ) .unwrap(); let state_clone2 = state.clone(); engine.register_fn("get_customer_history", move |user_id: &str| -> Dynamic { get_customer_history_impl(&state_clone2, user_id) }); } pub fn get_customer_history_impl(state: &Arc, user_id: &str) -> Dynamic { let conn = state.conn.clone(); let user_uuid = match Uuid::parse_str(user_id) { Ok(u) => u, Err(_) => return create_error_result("Invalid user ID"), }; let result = std::thread::spawn(move || { let mut db_conn = match conn.get() { Ok(c) => c, Err(e) => return create_error_result(&format!("DB error: {}", e)), }; use crate::shared::models::schema::user_sessions; let sessions: Vec = user_sessions::table .filter(user_sessions::user_id.eq(user_uuid)) .order(user_sessions::created_at.desc()) .limit(10) .load(&mut db_conn) .unwrap_or_default(); let session_items: Vec = sessions .iter() .map(|s| { let mut item = Map::new(); item.insert("session_id".into(), Dynamic::from(s.id.to_string())); item.insert( "channel".into(), Dynamic::from( s.context_data .get("channel") .and_then(|v| v.as_str()) .unwrap_or("web") .to_string(), ), ); item.insert( "status".into(), Dynamic::from( s.context_data .get("status") .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(), ), ); item.insert("created_at".into(), Dynamic::from(s.created_at.to_string())); Dynamic::from(item) }) .collect(); let mut result = Map::new(); result.insert("success".into(), Dynamic::from(true)); result.insert("user_id".into(), Dynamic::from(user_uuid.to_string())); result.insert("session_count".into(), Dynamic::from(sessions.len() as i64)); result.insert("sessions".into(), Dynamic::from(session_items)); Dynamic::from(result) }) .join() .unwrap_or_else(|_| create_error_result("Thread panic")); result } // Helper Functions pub fn create_error_result(message: &str) -> Dynamic { let mut result = Map::new(); result.insert("success".into(), Dynamic::from(false)); result.insert("error".into(), Dynamic::from(message.to_string())); Dynamic::from(result) } // Tests