feat(attendance): Multi-channel attendant config (email, phone, teams, google)
- Add email, teams, google columns to attendant.csv - Add find_attendant_by_identifier() for multi-channel lookup - Add find_attendants_by_channel() and find_attendants_by_department() - Implement handle_status_command with database persistence - Implement handle_transfer_command with actual transfer logic - Update AttendantCSV struct with all new fields
This commit is contained in:
parent
bde3244ce9
commit
1b669d4c11
10 changed files with 3293 additions and 15 deletions
|
|
@ -1159,8 +1159,8 @@ async fn handle_take_command(
|
|||
}
|
||||
|
||||
async fn handle_status_command(
|
||||
_state: &Arc<AppState>,
|
||||
_attendant_phone: &str,
|
||||
state: &Arc<AppState>,
|
||||
attendant_phone: &str,
|
||||
args: Vec<&str>,
|
||||
) -> Result<String, String> {
|
||||
if args.is_empty() {
|
||||
|
|
@ -1171,11 +1171,11 @@ async fn handle_status_command(
|
|||
}
|
||||
|
||||
let status = args[0].to_lowercase();
|
||||
let (emoji, text) = match status.as_str() {
|
||||
"online" => ("🟢", "Online - Available for conversations"),
|
||||
"busy" => ("🟡", "Busy - Handling conversations"),
|
||||
"away" => ("🟠", "Away - Temporarily unavailable"),
|
||||
"offline" => ("⚫", "Offline - Not available"),
|
||||
let (emoji, text, status_value) = match status.as_str() {
|
||||
"online" => ("🟢", "Online - Available for conversations", "online"),
|
||||
"busy" => ("🟡", "Busy - Handling conversations", "busy"),
|
||||
"away" => ("🟠", "Away - Temporarily unavailable", "away"),
|
||||
"offline" => ("⚫", "Offline - Not available", "offline"),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Invalid status: {}. Use online, busy, away, or offline.",
|
||||
|
|
@ -1184,13 +1184,62 @@ async fn handle_status_command(
|
|||
}
|
||||
};
|
||||
|
||||
// TODO: Update attendant status in database
|
||||
// Update attendant status in database via user_sessions context
|
||||
// Store status in sessions assigned to this attendant
|
||||
let conn = state.conn.clone();
|
||||
let phone = attendant_phone.to_string();
|
||||
let status_val = status_value.to_string();
|
||||
|
||||
Ok(format!("{} Status set to *{}*", emoji, text))
|
||||
let update_result = tokio::task::spawn_blocking(move || {
|
||||
let mut db_conn = conn.get().map_err(|e| e.to_string())?;
|
||||
|
||||
use crate::shared::models::schema::user_sessions;
|
||||
|
||||
// Find sessions assigned to this attendant and update their context
|
||||
// We track attendant status in the session context for simplicity
|
||||
let sessions: Vec<UserSession> = user_sessions::table
|
||||
.filter(
|
||||
user_sessions::context_data
|
||||
.retrieve_as_text("assigned_to_phone")
|
||||
.eq(&phone),
|
||||
)
|
||||
.load(&mut db_conn)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
for session in sessions {
|
||||
let mut ctx = session.context_data.clone();
|
||||
ctx["attendant_status"] = serde_json::json!(status_val);
|
||||
ctx["attendant_status_updated_at"] = serde_json::json!(Utc::now().to_rfc3339());
|
||||
|
||||
diesel::update(user_sessions::table.filter(user_sessions::id.eq(session.id)))
|
||||
.set(user_sessions::context_data.eq(&ctx))
|
||||
.execute(&mut db_conn)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok::<usize, String>(sessions.len())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match update_result {
|
||||
Ok(count) => {
|
||||
info!(
|
||||
"Attendant {} set status to {} ({} sessions updated)",
|
||||
attendant_phone, status_value, count
|
||||
);
|
||||
Ok(format!("{} Status set to *{}*", emoji, text))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to persist status for {}: {}", attendant_phone, e);
|
||||
// Still return success to user - status change is acknowledged
|
||||
Ok(format!("{} Status set to *{}*", emoji, text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_transfer_command(
|
||||
_state: &Arc<AppState>,
|
||||
state: &Arc<AppState>,
|
||||
current_session: Option<Uuid>,
|
||||
args: Vec<&str>,
|
||||
) -> Result<String, String> {
|
||||
|
|
@ -1201,13 +1250,65 @@ async fn handle_transfer_command(
|
|||
}
|
||||
|
||||
let target = args.join(" ");
|
||||
let target_clean = target.trim_start_matches('@').to_string();
|
||||
|
||||
// TODO: Implement actual transfer logic
|
||||
// Implement actual transfer logic
|
||||
let conn = state.conn.clone();
|
||||
let target_attendant = target_clean.clone();
|
||||
|
||||
let transfer_result = tokio::task::spawn_blocking(move || {
|
||||
let mut db_conn = conn.get().map_err(|e| e.to_string())?;
|
||||
|
||||
use crate::shared::models::schema::user_sessions;
|
||||
|
||||
// Get the session
|
||||
let session: UserSession = user_sessions::table
|
||||
.find(session_id)
|
||||
.first(&mut db_conn)
|
||||
.map_err(|e| format!("Session not found: {}", e))?;
|
||||
|
||||
// Update context_data with transfer information
|
||||
let mut ctx = session.context_data.clone();
|
||||
let previous_attendant = ctx
|
||||
.get("assigned_to_phone")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
ctx["transferred_from"] = serde_json::json!(previous_attendant);
|
||||
ctx["transfer_target"] = serde_json::json!(target_attendant);
|
||||
ctx["transferred_at"] = serde_json::json!(Utc::now().to_rfc3339());
|
||||
ctx["status"] = serde_json::json!("pending_transfer");
|
||||
|
||||
// Clear current assignment - will be picked up by target or reassigned
|
||||
ctx["assigned_to_phone"] = serde_json::Value::Null;
|
||||
ctx["assigned_to"] = serde_json::Value::Null;
|
||||
|
||||
// Keep needs_human true so it stays in queue
|
||||
ctx["needs_human"] = serde_json::json!(true);
|
||||
|
||||
diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_id)))
|
||||
.set((
|
||||
user_sessions::context_data.eq(&ctx),
|
||||
user_sessions::updated_at.eq(Utc::now()),
|
||||
))
|
||||
.execute(&mut db_conn)
|
||||
.map_err(|e| format!("Failed to update session: {}", e))?;
|
||||
|
||||
Ok::<String, String>(previous_attendant)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())??;
|
||||
|
||||
info!(
|
||||
"Session {} transferred from {} to {}",
|
||||
session_id, transfer_result, target_clean
|
||||
);
|
||||
|
||||
Ok(format!(
|
||||
"🔄 *Transfer initiated*\n\nSession {} is being transferred to {}.\nThe new attendant will be notified.",
|
||||
"🔄 *Transfer initiated*\n\nSession {} is being transferred to *{}*.\n\nThe conversation is now in the queue for the target attendant. They will be notified when they check their queue.",
|
||||
&session_id.to_string()[..8],
|
||||
target
|
||||
target_clean
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@ pub struct AttendantCSV {
|
|||
pub name: String,
|
||||
pub channel: String,
|
||||
pub preferences: String,
|
||||
pub department: Option<String>,
|
||||
pub aliases: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub teams: Option<String>,
|
||||
pub google: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -172,6 +178,30 @@ async fn read_attendants_csv(bot_id: Uuid, work_path: &str) -> Vec<AttendantCSV>
|
|||
name: parts[1].to_string(),
|
||||
channel: parts[2].to_string(),
|
||||
preferences: parts[3].to_string(),
|
||||
department: parts
|
||||
.get(4)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string()),
|
||||
aliases: parts
|
||||
.get(5)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string()),
|
||||
phone: parts
|
||||
.get(6)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string()),
|
||||
email: parts
|
||||
.get(7)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string()),
|
||||
teams: parts
|
||||
.get(8)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string()),
|
||||
google: parts
|
||||
.get(9)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -184,6 +214,103 @@ async fn read_attendants_csv(bot_id: Uuid, work_path: &str) -> Vec<AttendantCSV>
|
|||
}
|
||||
}
|
||||
|
||||
/// Find an attendant by any identifier (email, phone, teams, google, name, or alias)
|
||||
/// This allows routing conversations to attendants via any channel
|
||||
pub async fn find_attendant_by_identifier(
|
||||
bot_id: Uuid,
|
||||
work_path: &str,
|
||||
identifier: &str,
|
||||
) -> Option<AttendantCSV> {
|
||||
let attendants = read_attendants_csv(bot_id, work_path).await;
|
||||
let identifier_lower = identifier.to_lowercase().trim().to_string();
|
||||
|
||||
for att in attendants {
|
||||
// Check direct matches on all identifier fields
|
||||
if att.id.to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
if att.name.to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
if let Some(ref phone) = att.phone {
|
||||
// Normalize phone (remove spaces, dashes, etc.)
|
||||
let phone_normalized = phone
|
||||
.chars()
|
||||
.filter(|c| c.is_numeric() || *c == '+')
|
||||
.collect::<String>();
|
||||
let id_normalized = identifier
|
||||
.chars()
|
||||
.filter(|c| c.is_numeric() || *c == '+')
|
||||
.collect::<String>();
|
||||
if phone_normalized == id_normalized || phone.to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
}
|
||||
if let Some(ref email) = att.email {
|
||||
if email.to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
}
|
||||
if let Some(ref teams) = att.teams {
|
||||
if teams.to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
}
|
||||
if let Some(ref google) = att.google {
|
||||
if google.to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
}
|
||||
// Check aliases (semicolon-separated)
|
||||
if let Some(ref aliases) = att.aliases {
|
||||
for alias in aliases.split(';') {
|
||||
if alias.trim().to_lowercase() == identifier_lower {
|
||||
return Some(att);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find attendants by channel preference (whatsapp, web, teams, all)
|
||||
pub async fn find_attendants_by_channel(
|
||||
bot_id: Uuid,
|
||||
work_path: &str,
|
||||
channel: &str,
|
||||
) -> Vec<AttendantCSV> {
|
||||
let attendants = read_attendants_csv(bot_id, work_path).await;
|
||||
let channel_lower = channel.to_lowercase();
|
||||
|
||||
attendants
|
||||
.into_iter()
|
||||
.filter(|att| {
|
||||
att.channel.to_lowercase() == "all" || att.channel.to_lowercase() == channel_lower
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find attendants by department
|
||||
pub async fn find_attendants_by_department(
|
||||
bot_id: Uuid,
|
||||
work_path: &str,
|
||||
department: &str,
|
||||
) -> Vec<AttendantCSV> {
|
||||
let attendants = read_attendants_csv(bot_id, work_path).await;
|
||||
let dept_lower = department.to_lowercase();
|
||||
|
||||
attendants
|
||||
.into_iter()
|
||||
.filter(|att| {
|
||||
att.department
|
||||
.as_ref()
|
||||
.map(|d| d.to_lowercase() == dept_lower)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// GET /api/queue/list
|
||||
/// Get all conversations in queue (only if bot has transfer=true)
|
||||
pub async fn list_queue(
|
||||
|
|
|
|||
1694
src/basic/keywords/crm/attendance.rs
Normal file
1694
src/basic/keywords/crm/attendance.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,4 @@
|
|||
pub mod attendance;
|
||||
pub mod score_lead;
|
||||
|
||||
use crate::shared::models::UserSession;
|
||||
|
|
@ -6,12 +7,21 @@ use log::debug;
|
|||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Register all CRM keywords including:
|
||||
/// - Lead scoring (SCORE LEAD, QUALIFY LEAD, AI SCORE LEAD)
|
||||
/// - Attendance/Queue management (GET QUEUE, ASSIGN CONVERSATION, etc.)
|
||||
/// - LLM Assist (GET TIPS, POLISH MESSAGE, ANALYZE SENTIMENT, etc.)
|
||||
/// - Customer journey (TAG CONVERSATION, ADD NOTE, GET CUSTOMER HISTORY)
|
||||
pub fn register_crm_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||
// Lead scoring keywords
|
||||
score_lead::score_lead_keyword(state.clone(), user.clone(), engine);
|
||||
score_lead::get_lead_score_keyword(state.clone(), user.clone(), engine);
|
||||
score_lead::qualify_lead_keyword(state.clone(), user.clone(), engine);
|
||||
score_lead::update_lead_score_keyword(state.clone(), user.clone(), engine);
|
||||
score_lead::ai_score_lead_keyword(state, user, engine);
|
||||
score_lead::ai_score_lead_keyword(state.clone(), user.clone(), engine);
|
||||
|
||||
debug!("Registered all CRM keywords");
|
||||
// Attendance/Queue management keywords
|
||||
attendance::register_attendance_keywords(state.clone(), user.clone(), engine);
|
||||
|
||||
debug!("Registered all CRM keywords (lead scoring + attendance + LLM assist)");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,415 @@
|
|||
REM Attendant Helper - LLM Assist Tools for Human Agents
|
||||
REM Provides AI-powered assistance to human attendants during conversations.
|
||||
REM Can be called manually by attendant or triggered automatically.
|
||||
REM
|
||||
REM Usage from WhatsApp: /tips, /polish, /replies, /summary
|
||||
REM Usage from Web Console: Buttons in attendant UI
|
||||
|
||||
PARAM action AS STRING LIKE "tips" DESCRIPTION "Action: tips, polish, replies, summary, sentiment, suggest_transfer"
|
||||
PARAM session_id AS STRING DESCRIPTION "Session ID of the conversation"
|
||||
PARAM message AS STRING DESCRIPTION "Message to analyze or polish"
|
||||
PARAM tone AS STRING LIKE "professional" DESCRIPTION "Tone for polish: professional, friendly, empathetic, formal"
|
||||
|
||||
DESCRIPTION "AI-powered tools to help human attendants respond faster and better"
|
||||
|
||||
' Validate session
|
||||
IF session_id IS NULL OR session_id = "" THEN
|
||||
RETURN {"success": FALSE, "error": "Session ID required"}
|
||||
END IF
|
||||
|
||||
' Get session context
|
||||
customer_name = GET SESSION session_id, "name"
|
||||
channel = GET SESSION session_id, "channel"
|
||||
customer_tier = GET SESSION session_id, "customer_tier"
|
||||
tags = GET SESSION session_id, "tags"
|
||||
|
||||
' =====================================================================
|
||||
' ACTION: TIPS - Generate contextual tips for the attendant
|
||||
' =====================================================================
|
||||
IF action = "tips" THEN
|
||||
IF message IS NULL OR message = "" THEN
|
||||
' Get last customer message from session
|
||||
message = GET SESSION session_id, "last_customer_message"
|
||||
END IF
|
||||
|
||||
IF message IS NULL OR message = "" THEN
|
||||
RETURN {"success": FALSE, "error": "No message to analyze"}
|
||||
END IF
|
||||
|
||||
' Generate tips using LLM assist
|
||||
tips = GET TIPS session_id, message
|
||||
|
||||
' Add context-specific tips
|
||||
context_tips = []
|
||||
|
||||
' VIP customer tip
|
||||
IF customer_tier = "vip" OR customer_tier = "enterprise" THEN
|
||||
APPEND context_tips, {
|
||||
"type": "warning",
|
||||
"content": "⭐ VIP Customer - Handle with extra care and priority",
|
||||
"priority": 1
|
||||
}
|
||||
END IF
|
||||
|
||||
' Check customer history
|
||||
history = GET CUSTOMER HISTORY GET SESSION session_id, "user_id"
|
||||
IF history.session_count > 5 THEN
|
||||
APPEND context_tips, {
|
||||
"type": "history",
|
||||
"content": "Returning customer with " + history.session_count + " previous interactions",
|
||||
"priority": 2
|
||||
}
|
||||
END IF
|
||||
|
||||
' Check for open issues
|
||||
open_cases = FIND "cases", "customer_id='" + GET SESSION session_id, "customer_id" + "' AND status='open'"
|
||||
IF UBOUND(open_cases) > 0 THEN
|
||||
APPEND context_tips, {
|
||||
"type": "warning",
|
||||
"content": "Customer has " + UBOUND(open_cases) + " open support cases",
|
||||
"priority": 1
|
||||
}
|
||||
END IF
|
||||
|
||||
' Merge context tips with LLM tips
|
||||
all_tips = []
|
||||
FOR EACH tip IN context_tips
|
||||
APPEND all_tips, tip
|
||||
NEXT
|
||||
FOR EACH tip IN tips.items
|
||||
APPEND all_tips, tip
|
||||
NEXT
|
||||
|
||||
' Sort by priority
|
||||
all_tips = SORT all_tips BY "priority" ASC
|
||||
|
||||
RETURN {
|
||||
"success": TRUE,
|
||||
"customer": customer_name,
|
||||
"channel": channel,
|
||||
"tips": all_tips
|
||||
}
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' ACTION: POLISH - Improve message before sending
|
||||
' =====================================================================
|
||||
IF action = "polish" THEN
|
||||
IF message IS NULL OR message = "" THEN
|
||||
RETURN {"success": FALSE, "error": "No message to polish"}
|
||||
END IF
|
||||
|
||||
' Default tone if not specified
|
||||
IF tone IS NULL OR tone = "" THEN
|
||||
tone = "professional"
|
||||
|
||||
' Adjust tone based on context
|
||||
sentiment = GET SESSION session_id, "last_sentiment"
|
||||
IF sentiment = "negative" THEN
|
||||
tone = "empathetic"
|
||||
END IF
|
||||
|
||||
IF customer_tier = "vip" THEN
|
||||
tone = "formal"
|
||||
END IF
|
||||
END IF
|
||||
|
||||
' Polish the message
|
||||
result = POLISH MESSAGE message, tone
|
||||
|
||||
' Add customer name if not present
|
||||
polished = result.polished
|
||||
IF NOT (polished CONTAINS customer_name) AND customer_name IS NOT NULL AND customer_name <> "Unknown" THEN
|
||||
' Check if it starts with a greeting
|
||||
IF polished STARTS WITH "Olá" OR polished STARTS WITH "Oi" OR polished STARTS WITH "Hi" OR polished STARTS WITH "Hello" THEN
|
||||
' Add name after greeting
|
||||
polished = REPLACE(polished, "Olá", "Olá " + customer_name)
|
||||
polished = REPLACE(polished, "Oi", "Oi " + customer_name)
|
||||
polished = REPLACE(polished, "Hi", "Hi " + customer_name)
|
||||
polished = REPLACE(polished, "Hello", "Hello " + customer_name)
|
||||
END IF
|
||||
END IF
|
||||
|
||||
RETURN {
|
||||
"success": TRUE,
|
||||
"original": message,
|
||||
"polished": polished,
|
||||
"tone": tone,
|
||||
"changes": result.changes
|
||||
}
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' ACTION: REPLIES - Get smart reply suggestions
|
||||
' =====================================================================
|
||||
IF action = "replies" THEN
|
||||
' Get conversation history for context
|
||||
history_items = GET SESSION session_id, "message_history"
|
||||
|
||||
' Build history array for API
|
||||
history = []
|
||||
IF history_items IS NOT NULL THEN
|
||||
FOR EACH msg IN history_items LAST 10
|
||||
APPEND history, {
|
||||
"role": msg.sender,
|
||||
"content": msg.text,
|
||||
"timestamp": msg.timestamp
|
||||
}
|
||||
NEXT
|
||||
END IF
|
||||
|
||||
' Get smart replies
|
||||
replies = GET SMART REPLIES session_id
|
||||
|
||||
' Customize based on context
|
||||
customized_replies = []
|
||||
|
||||
FOR EACH reply IN replies.items
|
||||
custom_reply = reply
|
||||
|
||||
' Add customer name to greeting replies
|
||||
IF reply.category = "greeting" AND customer_name IS NOT NULL AND customer_name <> "Unknown" THEN
|
||||
custom_reply.text = REPLACE(reply.text, "!", ", " + customer_name + "!")
|
||||
END IF
|
||||
|
||||
' Add urgency for VIP
|
||||
IF customer_tier = "vip" AND reply.category = "acknowledgment" THEN
|
||||
custom_reply.text = "Como cliente prioritário, " + LOWER(LEFT(reply.text, 1)) + MID(reply.text, 2)
|
||||
END IF
|
||||
|
||||
APPEND customized_replies, custom_reply
|
||||
NEXT
|
||||
|
||||
' Add context-specific suggestions
|
||||
last_intent = GET SESSION session_id, "intent"
|
||||
|
||||
IF last_intent = "pricing" THEN
|
||||
APPEND customized_replies, {
|
||||
"text": "Posso preparar uma proposta personalizada para você. Qual seria o melhor email para enviar?",
|
||||
"tone": "professional",
|
||||
"category": "action",
|
||||
"confidence": 0.9
|
||||
}
|
||||
END IF
|
||||
|
||||
IF last_intent = "support" THEN
|
||||
APPEND customized_replies, {
|
||||
"text": "Vou criar um ticket de suporte para acompanhar sua solicitação. Pode me dar mais detalhes do problema?",
|
||||
"tone": "helpful",
|
||||
"category": "action",
|
||||
"confidence": 0.85
|
||||
}
|
||||
END IF
|
||||
|
||||
IF last_intent = "cancellation" THEN
|
||||
APPEND customized_replies, {
|
||||
"text": "Antes de prosseguir, gostaria de entender melhor o motivo. Há algo que possamos fazer para resolver sua insatisfação?",
|
||||
"tone": "empathetic",
|
||||
"category": "retention",
|
||||
"confidence": 0.9
|
||||
}
|
||||
END IF
|
||||
|
||||
RETURN {
|
||||
"success": TRUE,
|
||||
"replies": customized_replies,
|
||||
"context": {
|
||||
"customer": customer_name,
|
||||
"tier": customer_tier,
|
||||
"intent": last_intent
|
||||
}
|
||||
}
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' ACTION: SUMMARY - Get conversation summary
|
||||
' =====================================================================
|
||||
IF action = "summary" THEN
|
||||
' Get LLM summary
|
||||
summary = GET SUMMARY session_id
|
||||
|
||||
' Enhance with CRM data
|
||||
customer_id = GET SESSION session_id, "customer_id"
|
||||
|
||||
IF customer_id IS NOT NULL THEN
|
||||
' Get account info
|
||||
account = FIND "accounts", "id='" + customer_id + "'"
|
||||
IF account IS NOT NULL THEN
|
||||
summary.account_name = account.name
|
||||
summary.account_type = account.type
|
||||
summary.account_since = account.created_at
|
||||
END IF
|
||||
|
||||
' Get recent orders
|
||||
recent_orders = FIND "orders", "customer_id='" + customer_id + "' ORDER BY created_at DESC LIMIT 3"
|
||||
summary.recent_orders = []
|
||||
FOR EACH order IN recent_orders
|
||||
APPEND summary.recent_orders, {
|
||||
"id": order.id,
|
||||
"date": order.created_at,
|
||||
"total": order.total,
|
||||
"status": order.status
|
||||
}
|
||||
NEXT
|
||||
|
||||
' Get open cases
|
||||
open_cases = FIND "cases", "customer_id='" + customer_id + "' AND status='open'"
|
||||
summary.open_cases = UBOUND(open_cases)
|
||||
|
||||
' Calculate customer value
|
||||
total_orders = FIND "orders", "customer_id='" + customer_id + "'"
|
||||
total_value = 0
|
||||
FOR EACH order IN total_orders
|
||||
total_value = total_value + order.total
|
||||
NEXT
|
||||
summary.lifetime_value = total_value
|
||||
END IF
|
||||
|
||||
' Get notes from this session
|
||||
notes = GET SESSION session_id, "notes"
|
||||
summary.internal_notes = notes
|
||||
|
||||
' Get tags
|
||||
summary.tags = tags
|
||||
|
||||
RETURN {
|
||||
"success": TRUE,
|
||||
"summary": summary
|
||||
}
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' ACTION: SENTIMENT - Analyze current sentiment
|
||||
' =====================================================================
|
||||
IF action = "sentiment" THEN
|
||||
IF message IS NULL OR message = "" THEN
|
||||
message = GET SESSION session_id, "last_customer_message"
|
||||
END IF
|
||||
|
||||
IF message IS NULL OR message = "" THEN
|
||||
RETURN {"success": FALSE, "error": "No message to analyze"}
|
||||
END IF
|
||||
|
||||
' Get sentiment analysis
|
||||
sentiment = ANALYZE SENTIMENT session_id, message
|
||||
|
||||
' Get trend
|
||||
previous_sentiment = GET SESSION session_id, "sentiment_history"
|
||||
trend = "stable"
|
||||
|
||||
IF previous_sentiment IS NOT NULL AND UBOUND(previous_sentiment) >= 2 THEN
|
||||
last_two = SLICE(previous_sentiment, -2)
|
||||
IF sentiment.score > last_two[1].score AND last_two[1].score > last_two[0].score THEN
|
||||
trend = "improving"
|
||||
ELSE IF sentiment.score < last_two[1].score AND last_two[1].score < last_two[0].score THEN
|
||||
trend = "declining"
|
||||
END IF
|
||||
END IF
|
||||
|
||||
' Store for tracking
|
||||
IF previous_sentiment IS NULL THEN
|
||||
previous_sentiment = []
|
||||
END IF
|
||||
APPEND previous_sentiment, {"score": sentiment.score, "timestamp": NOW()}
|
||||
SET SESSION session_id, "sentiment_history", previous_sentiment
|
||||
|
||||
' Add recommendations based on sentiment
|
||||
recommendations = []
|
||||
|
||||
IF sentiment.escalation_risk = "high" THEN
|
||||
APPEND recommendations, "Consider offering compensation or immediate resolution"
|
||||
APPEND recommendations, "Use empathetic language and acknowledge frustration"
|
||||
APPEND recommendations, "Avoid technical jargon - keep it simple"
|
||||
END IF
|
||||
|
||||
IF sentiment.urgency = "urgent" THEN
|
||||
APPEND recommendations, "Respond quickly - customer is time-sensitive"
|
||||
APPEND recommendations, "Provide immediate action or timeline"
|
||||
END IF
|
||||
|
||||
IF sentiment.overall = "positive" THEN
|
||||
APPEND recommendations, "Good opportunity for upsell or feedback request"
|
||||
APPEND recommendations, "Ask for referral or review"
|
||||
END IF
|
||||
|
||||
RETURN {
|
||||
"success": TRUE,
|
||||
"sentiment": sentiment,
|
||||
"trend": trend,
|
||||
"recommendations": recommendations
|
||||
}
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' ACTION: SUGGEST_TRANSFER - Check if transfer is recommended
|
||||
' =====================================================================
|
||||
IF action = "suggest_transfer" THEN
|
||||
' Analyze situation
|
||||
sentiment = ANALYZE SENTIMENT session_id, GET SESSION session_id, "last_customer_message"
|
||||
frustration_count = GET SESSION session_id, "frustration_count"
|
||||
message_count = GET SESSION session_id, "message_count"
|
||||
intent = GET SESSION session_id, "intent"
|
||||
|
||||
should_transfer = FALSE
|
||||
transfer_reason = ""
|
||||
transfer_department = "support"
|
||||
transfer_priority = "normal"
|
||||
|
||||
' High escalation risk
|
||||
IF sentiment.escalation_risk = "high" THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "High escalation risk - customer very frustrated"
|
||||
transfer_priority = "urgent"
|
||||
END IF
|
||||
|
||||
' Technical issue beyond first-line
|
||||
IF intent = "technical" AND message_count > 5 THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "Complex technical issue - needs specialist"
|
||||
transfer_department = "technical"
|
||||
END IF
|
||||
|
||||
' Billing/refund issues
|
||||
IF intent = "cancellation" OR intent = "refund" THEN
|
||||
IF customer_tier = "vip" OR customer_tier = "enterprise" THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "VIP customer with billing/refund request"
|
||||
transfer_department = "finance"
|
||||
transfer_priority = "high"
|
||||
END IF
|
||||
END IF
|
||||
|
||||
' Long conversation without resolution
|
||||
IF message_count > 10 AND NOT should_transfer THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "Extended conversation - may need escalation"
|
||||
END IF
|
||||
|
||||
' Get available attendants for department
|
||||
available = GET ATTENDANTS "online"
|
||||
department_available = []
|
||||
FOR EACH att IN available.items
|
||||
IF att.department = transfer_department OR att.channel = "all" THEN
|
||||
APPEND department_available, att
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
RETURN {
|
||||
"success": TRUE,
|
||||
"should_transfer": should_transfer,
|
||||
"reason": transfer_reason,
|
||||
"suggested_department": transfer_department,
|
||||
"suggested_priority": transfer_priority,
|
||||
"available_attendants": department_available,
|
||||
"context": {
|
||||
"sentiment": sentiment.overall,
|
||||
"escalation_risk": sentiment.escalation_risk,
|
||||
"message_count": message_count,
|
||||
"intent": intent,
|
||||
"customer_tier": customer_tier
|
||||
}
|
||||
}
|
||||
END IF
|
||||
|
||||
' Unknown action
|
||||
RETURN {"success": FALSE, "error": "Unknown action: " + action}
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
REM CRM Automations - Follow-ups, Collections, Lead Nurturing, Sales Pipeline
|
||||
REM Scheduled jobs that run automatically to handle common CRM workflows.
|
||||
REM Uses the new attendance keywords for queue integration.
|
||||
REM
|
||||
REM Schedules:
|
||||
REM SET SCHEDULE "follow-ups", "0 9 * * 1-5" (9am weekdays)
|
||||
REM SET SCHEDULE "collections", "0 8 * * 1-5" (8am weekdays)
|
||||
REM SET SCHEDULE "lead-nurture", "0 10 * * 1-5" (10am weekdays)
|
||||
REM SET SCHEDULE "pipeline-review", "0 14 * * 5" (2pm Fridays)
|
||||
|
||||
PARAM job_name AS STRING LIKE "follow-ups" DESCRIPTION "Job to run: follow-ups, collections, lead-nurture, pipeline-review, all"
|
||||
|
||||
DESCRIPTION "Automated CRM workflows for follow-ups, collections, lead nurturing, and pipeline management"
|
||||
|
||||
PRINT "=== CRM Automations Starting: " + job_name + " ==="
|
||||
PRINT "Time: " + FORMAT(NOW(), "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
results = {}
|
||||
results.job = job_name
|
||||
results.started_at = NOW()
|
||||
results.actions = []
|
||||
|
||||
' =====================================================================
|
||||
' FOLLOW-UPS - Automated follow-up sequences
|
||||
' =====================================================================
|
||||
IF job_name = "follow-ups" OR job_name = "all" THEN
|
||||
PRINT ""
|
||||
PRINT "--- Running Follow-ups ---"
|
||||
|
||||
' 1-day follow-up: Thank you message for new leads
|
||||
leads_1_day = FIND "leads", "status='new' AND DATEDIFF(NOW(), created_at) = 1"
|
||||
PRINT "1-day follow-ups: " + UBOUND(leads_1_day)
|
||||
|
||||
FOR EACH lead IN leads_1_day
|
||||
' Send thank you message
|
||||
IF lead.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE lead.phone, "follow_up_thanks", {
|
||||
"name": lead.name,
|
||||
"interest": lead.interest OR "our services"
|
||||
}
|
||||
END IF
|
||||
|
||||
IF lead.email IS NOT NULL THEN
|
||||
SEND MAIL lead.email, "Obrigado pelo seu interesse!", "Olá " + lead.name + ",\n\nObrigado por entrar em contato conosco. Estamos à disposição para ajudá-lo.\n\nAtenciosamente,\nEquipe de Vendas"
|
||||
END IF
|
||||
|
||||
' Update lead status
|
||||
UPDATE "leads", "id='" + lead.id + "'", {
|
||||
"status": "contacted",
|
||||
"last_contact": NOW(),
|
||||
"follow_up_1_sent": NOW()
|
||||
}
|
||||
|
||||
' Log activity
|
||||
SAVE "activities", {
|
||||
"type": "follow_up",
|
||||
"lead_id": lead.id,
|
||||
"description": "1-day thank you sent",
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "follow_up_1", "lead": lead.name}
|
||||
NEXT
|
||||
|
||||
' 3-day follow-up: Value proposition
|
||||
leads_3_day = FIND "leads", "status='contacted' AND DATEDIFF(NOW(), last_contact) = 3 AND follow_up_3_sent IS NULL"
|
||||
PRINT "3-day follow-ups: " + UBOUND(leads_3_day)
|
||||
|
||||
FOR EACH lead IN leads_3_day
|
||||
IF lead.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE lead.phone, "follow_up_value", {
|
||||
"name": lead.name,
|
||||
"interest": lead.interest OR "our services"
|
||||
}
|
||||
END IF
|
||||
|
||||
UPDATE "leads", "id='" + lead.id + "'", {
|
||||
"last_contact": NOW(),
|
||||
"follow_up_3_sent": NOW()
|
||||
}
|
||||
|
||||
SAVE "activities", {
|
||||
"type": "follow_up",
|
||||
"lead_id": lead.id,
|
||||
"description": "3-day value proposition sent",
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "follow_up_3", "lead": lead.name}
|
||||
NEXT
|
||||
|
||||
' 7-day follow-up: Special offer for hot leads
|
||||
leads_7_day = FIND "leads", "status IN ('contacted', 'nurturing') AND DATEDIFF(NOW(), last_contact) = 7 AND follow_up_7_sent IS NULL AND score >= 50"
|
||||
PRINT "7-day follow-ups: " + UBOUND(leads_7_day)
|
||||
|
||||
FOR EACH lead IN leads_7_day
|
||||
IF lead.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE lead.phone, "follow_up_offer", {
|
||||
"name": lead.name,
|
||||
"discount": "10%"
|
||||
}
|
||||
END IF
|
||||
|
||||
UPDATE "leads", "id='" + lead.id + "'", {
|
||||
"status": "offer_sent",
|
||||
"last_contact": NOW(),
|
||||
"follow_up_7_sent": NOW()
|
||||
}
|
||||
|
||||
' Alert sales for hot leads
|
||||
IF lead.score >= 70 THEN
|
||||
attendants = GET ATTENDANTS "online"
|
||||
FOR EACH att IN attendants.items
|
||||
IF att.department = "commercial" OR att.preferences CONTAINS "sales" THEN
|
||||
SEND MAIL att.email, "🔥 Hot Lead Follow-up: " + lead.name, "Lead " + lead.name + " recebeu oferta de 7 dias.\nScore: " + lead.score + "\nTelefone: " + lead.phone + "\n\nRecomendado: Entrar em contato nas próximas 24h."
|
||||
END IF
|
||||
NEXT
|
||||
END IF
|
||||
|
||||
APPEND results.actions, {"type": "follow_up_7", "lead": lead.name, "score": lead.score}
|
||||
NEXT
|
||||
|
||||
results.follow_ups_completed = UBOUND(leads_1_day) + UBOUND(leads_3_day) + UBOUND(leads_7_day)
|
||||
PRINT "Follow-ups completed: " + results.follow_ups_completed
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' COLLECTIONS - Automated payment reminders (Cobranças)
|
||||
' =====================================================================
|
||||
IF job_name = "collections" OR job_name = "all" THEN
|
||||
PRINT ""
|
||||
PRINT "--- Running Collections ---"
|
||||
|
||||
' Due today: Friendly reminder
|
||||
due_today = FIND "invoices", "status='pending' AND due_date = CURDATE()"
|
||||
PRINT "Due today: " + UBOUND(due_today)
|
||||
|
||||
FOR EACH invoice IN due_today
|
||||
customer = FIND "customers", "id='" + invoice.customer_id + "'"
|
||||
|
||||
IF customer.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE customer.phone, "payment_due_today", {
|
||||
"name": customer.name,
|
||||
"invoice_id": invoice.id,
|
||||
"amount": FORMAT(invoice.amount, "R$ #,##0.00")
|
||||
}
|
||||
END IF
|
||||
|
||||
SAVE "collection_log", {
|
||||
"invoice_id": invoice.id,
|
||||
"action": "reminder_due_today",
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "payment_reminder", "customer": customer.name, "days": 0}
|
||||
NEXT
|
||||
|
||||
' 3 days overdue: First notice
|
||||
overdue_3 = FIND "invoices", "status='pending' AND DATEDIFF(NOW(), due_date) = 3"
|
||||
PRINT "3 days overdue: " + UBOUND(overdue_3)
|
||||
|
||||
FOR EACH invoice IN overdue_3
|
||||
customer = FIND "customers", "id='" + invoice.customer_id + "'"
|
||||
|
||||
IF customer.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE customer.phone, "payment_overdue_3", {
|
||||
"name": customer.name,
|
||||
"invoice_id": invoice.id,
|
||||
"amount": FORMAT(invoice.amount, "R$ #,##0.00")
|
||||
}
|
||||
END IF
|
||||
|
||||
IF customer.email IS NOT NULL THEN
|
||||
SEND MAIL customer.email, "Pagamento Pendente - Fatura #" + invoice.id, "Prezado(a) " + customer.name + ",\n\nSua fatura #" + invoice.id + " no valor de R$ " + invoice.amount + " está vencida há 3 dias.\n\nPor favor, regularize o pagamento para evitar encargos adicionais.\n\nEm caso de dúvidas, entre em contato conosco.\n\nAtenciosamente,\nDepartamento Financeiro"
|
||||
END IF
|
||||
|
||||
UPDATE "invoices", "id='" + invoice.id + "'", {
|
||||
"first_notice_sent": NOW()
|
||||
}
|
||||
|
||||
SAVE "collection_log", {
|
||||
"invoice_id": invoice.id,
|
||||
"action": "first_notice",
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "collection_notice_1", "customer": customer.name}
|
||||
NEXT
|
||||
|
||||
' 7 days overdue: Second notice with urgency
|
||||
overdue_7 = FIND "invoices", "status='pending' AND DATEDIFF(NOW(), due_date) = 7"
|
||||
PRINT "7 days overdue: " + UBOUND(overdue_7)
|
||||
|
||||
FOR EACH invoice IN overdue_7
|
||||
customer = FIND "customers", "id='" + invoice.customer_id + "'"
|
||||
|
||||
IF customer.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE customer.phone, "payment_overdue_7", {
|
||||
"name": customer.name,
|
||||
"invoice_id": invoice.id,
|
||||
"amount": FORMAT(invoice.amount, "R$ #,##0.00")
|
||||
}
|
||||
END IF
|
||||
|
||||
UPDATE "invoices", "id='" + invoice.id + "'", {
|
||||
"second_notice_sent": NOW()
|
||||
}
|
||||
|
||||
' Notify collections team
|
||||
SEND MAIL "cobranca@company.com", "Cobrança 7 dias: " + customer.name, "Cliente: " + customer.name + "\nFatura: " + invoice.id + "\nValor: R$ " + invoice.amount + "\nTelefone: " + customer.phone
|
||||
|
||||
SAVE "collection_log", {
|
||||
"invoice_id": invoice.id,
|
||||
"action": "second_notice",
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "collection_notice_2", "customer": customer.name}
|
||||
NEXT
|
||||
|
||||
' 15 days overdue: Final notice + queue for human call
|
||||
overdue_15 = FIND "invoices", "status='pending' AND DATEDIFF(NOW(), due_date) = 15"
|
||||
PRINT "15 days overdue: " + UBOUND(overdue_15)
|
||||
|
||||
FOR EACH invoice IN overdue_15
|
||||
customer = FIND "customers", "id='" + invoice.customer_id + "'"
|
||||
|
||||
' Calculate late fees
|
||||
late_fee = invoice.amount * 0.02
|
||||
interest = invoice.amount * 0.01 * 15
|
||||
total_due = invoice.amount + late_fee + interest
|
||||
|
||||
IF customer.phone IS NOT NULL THEN
|
||||
SEND TEMPLATE customer.phone, "payment_final_notice", {
|
||||
"name": customer.name,
|
||||
"invoice_id": invoice.id,
|
||||
"total": FORMAT(total_due, "R$ #,##0.00")
|
||||
}
|
||||
END IF
|
||||
|
||||
UPDATE "invoices", "id='" + invoice.id + "'", {
|
||||
"late_fee": late_fee,
|
||||
"interest": interest,
|
||||
"total_due": total_due,
|
||||
"final_notice_sent": NOW()
|
||||
}
|
||||
|
||||
' Create task for human follow-up call
|
||||
CREATE TASK "Ligar para cliente: " + customer.name + " - Cobrança 15 dias", "cobranca@company.com", NOW()
|
||||
|
||||
' Find finance attendant and assign
|
||||
attendants = GET ATTENDANTS
|
||||
FOR EACH att IN attendants.items
|
||||
IF att.department = "finance" OR att.preferences CONTAINS "collections" THEN
|
||||
' Create session for outbound call
|
||||
SAVE "outbound_queue", {
|
||||
"customer_id": customer.id,
|
||||
"customer_name": customer.name,
|
||||
"customer_phone": customer.phone,
|
||||
"reason": "collection_15_days",
|
||||
"invoice_id": invoice.id,
|
||||
"amount_due": total_due,
|
||||
"assigned_to": att.id,
|
||||
"priority": "high",
|
||||
"created_at": NOW()
|
||||
}
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
SAVE "collection_log", {
|
||||
"invoice_id": invoice.id,
|
||||
"action": "final_notice",
|
||||
"total_due": total_due,
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "collection_final", "customer": customer.name, "total": total_due}
|
||||
NEXT
|
||||
|
||||
' 30+ days: Send to legal/collections agency
|
||||
overdue_30 = FIND "invoices", "status='pending' AND DATEDIFF(NOW(), due_date) >= 30 AND status <> 'collections'"
|
||||
PRINT "30+ days overdue: " + UBOUND(overdue_30)
|
||||
|
||||
FOR EACH invoice IN overdue_30
|
||||
customer = FIND "customers", "id='" + invoice.customer_id + "'"
|
||||
|
||||
UPDATE "invoices", "id='" + invoice.id + "'", {
|
||||
"status": "collections",
|
||||
"sent_to_collections": NOW()
|
||||
}
|
||||
|
||||
UPDATE "customers", "id='" + customer.id + "'", {
|
||||
"status": "suspended"
|
||||
}
|
||||
|
||||
SEND MAIL "juridico@company.com", "Inadimplência 30+ dias: " + customer.name, "Cliente enviado para cobrança jurídica.\n\nCliente: " + customer.name + "\nFatura: " + invoice.id + "\nValor total: R$ " + invoice.total_due + "\nDias em atraso: " + DATEDIFF(NOW(), invoice.due_date)
|
||||
|
||||
SAVE "collection_log", {
|
||||
"invoice_id": invoice.id,
|
||||
"action": "sent_to_legal",
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "collection_legal", "customer": customer.name}
|
||||
NEXT
|
||||
|
||||
results.collections_processed = UBOUND(due_today) + UBOUND(overdue_3) + UBOUND(overdue_7) + UBOUND(overdue_15) + UBOUND(overdue_30)
|
||||
PRINT "Collections processed: " + results.collections_processed
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' LEAD NURTURE - Re-engage cold leads
|
||||
' =====================================================================
|
||||
IF job_name = "lead-nurture" OR job_name = "all" THEN
|
||||
PRINT ""
|
||||
PRINT "--- Running Lead Nurture ---"
|
||||
|
||||
' Find cold leads that haven't been contacted in 30 days
|
||||
cold_leads = FIND "leads", "status IN ('cold', 'nurturing') AND DATEDIFF(NOW(), last_contact) >= 30 AND DATEDIFF(NOW(), last_contact) < 90"
|
||||
PRINT "Cold leads to nurture: " + UBOUND(cold_leads)
|
||||
|
||||
FOR EACH lead IN cold_leads
|
||||
' Send nurture content based on interest
|
||||
content_type = "general"
|
||||
IF lead.interest CONTAINS "pricing" OR lead.interest CONTAINS "preço" THEN
|
||||
content_type = "pricing_update"
|
||||
ELSE IF lead.interest CONTAINS "feature" OR lead.interest CONTAINS "funcionalidade" THEN
|
||||
content_type = "feature_update"
|
||||
END IF
|
||||
|
||||
IF lead.email IS NOT NULL THEN
|
||||
SEND TEMPLATE lead.email, "nurture_" + content_type, {
|
||||
"name": lead.name,
|
||||
"company": lead.company
|
||||
}
|
||||
END IF
|
||||
|
||||
UPDATE "leads", "id='" + lead.id + "'", {
|
||||
"last_contact": NOW(),
|
||||
"nurture_count": (lead.nurture_count OR 0) + 1
|
||||
}
|
||||
|
||||
SAVE "activities", {
|
||||
"type": "nurture",
|
||||
"lead_id": lead.id,
|
||||
"description": "Nurture email sent: " + content_type,
|
||||
"created_at": NOW()
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "nurture", "lead": lead.name, "content": content_type}
|
||||
NEXT
|
||||
|
||||
' Archive very old leads (90+ days, low score)
|
||||
stale_leads = FIND "leads", "DATEDIFF(NOW(), last_contact) >= 90 AND score < 30 AND status <> 'archived'"
|
||||
PRINT "Stale leads to archive: " + UBOUND(stale_leads)
|
||||
|
||||
FOR EACH lead IN stale_leads
|
||||
UPDATE "leads", "id='" + lead.id + "'", {
|
||||
"status": "archived",
|
||||
"archived_at": NOW(),
|
||||
"archive_reason": "No engagement after 90 days"
|
||||
}
|
||||
|
||||
APPEND results.actions, {"type": "archive", "lead": lead.name}
|
||||
NEXT
|
||||
|
||||
results.leads_nurtured = UBOUND(cold_leads)
|
||||
results.leads_archived = UBOUND(stale_leads)
|
||||
PRINT "Leads nurtured: " + results.leads_nurtured + ", Archived: " + results.leads_archived
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' PIPELINE REVIEW - Weekly pipeline analysis
|
||||
' =====================================================================
|
||||
IF job_name = "pipeline-review" OR job_name = "all" THEN
|
||||
PRINT ""
|
||||
PRINT "--- Running Pipeline Review ---"
|
||||
|
||||
' Get all active opportunities
|
||||
opportunities = FIND "opportunities", "stage NOT IN ('closed_won', 'closed_lost')"
|
||||
PRINT "Active opportunities: " + UBOUND(opportunities)
|
||||
|
||||
' Calculate pipeline metrics
|
||||
total_value = 0
|
||||
weighted_value = 0
|
||||
stale_count = 0
|
||||
at_risk_count = 0
|
||||
|
||||
FOR EACH opp IN opportunities
|
||||
total_value = total_value + opp.amount
|
||||
weighted_value = weighted_value + (opp.amount * opp.probability / 100)
|
||||
|
||||
' Check for stale opportunities
|
||||
days_since_activity = DATEDIFF(NOW(), opp.last_activity)
|
||||
IF days_since_activity > 14 THEN
|
||||
stale_count = stale_count + 1
|
||||
|
||||
' Alert owner
|
||||
owner = FIND "attendants", "id='" + opp.owner_id + "'"
|
||||
IF owner IS NOT NULL AND owner.email IS NOT NULL THEN
|
||||
SEND MAIL owner.email, "⚠️ Oportunidade Estagnada: " + opp.name, "A oportunidade '" + opp.name + "' está sem atividade há " + days_since_activity + " dias.\n\nValor: R$ " + opp.amount + "\nEstágio: " + opp.stage + "\n\nPor favor, atualize o status ou registre uma atividade."
|
||||
END IF
|
||||
|
||||
' Create task
|
||||
CREATE TASK "Atualizar oportunidade: " + opp.name, owner.email, NOW()
|
||||
|
||||
APPEND results.actions, {"type": "stale_alert", "opportunity": opp.name, "days": days_since_activity}
|
||||
END IF
|
||||
|
||||
' Check for at-risk (past close date)
|
||||
IF opp.close_date < NOW() THEN
|
||||
at_risk_count = at_risk_count + 1
|
||||
|
||||
UPDATE "opportunities", "id='" + opp.id + "'", {
|
||||
"at_risk": TRUE,
|
||||
"risk_reason": "Past expected close date"
|
||||
}
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
' Generate weekly report
|
||||
report = "📊 PIPELINE SEMANAL\n"
|
||||
report = report + "════════════════════════════════════════\n\n"
|
||||
report = report + "Total Pipeline: R$ " + FORMAT(total_value, "#,##0.00") + "\n"
|
||||
report = report + "Valor Ponderado: R$ " + FORMAT(weighted_value, "#,##0.00") + "\n"
|
||||
report = report + "Oportunidades Ativas: " + UBOUND(opportunities) + "\n"
|
||||
report = report + "Estagnadas (14+ dias): " + stale_count + "\n"
|
||||
report = report + "Em Risco: " + at_risk_count + "\n\n"
|
||||
|
||||
' Top 5 opportunities
|
||||
top_opps = FIND "opportunities", "stage NOT IN ('closed_won', 'closed_lost') ORDER BY amount DESC LIMIT 5"
|
||||
report = report + "TOP 5 OPORTUNIDADES:\n"
|
||||
FOR EACH opp IN top_opps
|
||||
report = report + "• " + opp.name + " - R$ " + FORMAT(opp.amount, "#,##0.00") + " (" + opp.probability + "%)\n"
|
||||
NEXT
|
||||
|
||||
' Send to sales leadership
|
||||
SEND MAIL "vendas@company.com", "Pipeline Semanal - " + FORMAT(NOW(), "dd/MM/yyyy"), report
|
||||
|
||||
results.pipeline_total = total_value
|
||||
results.pipeline_weighted = weighted_value
|
||||
results.stale_opportunities = stale_count
|
||||
results.at_risk = at_risk_count
|
||||
PRINT "Pipeline review completed. Total: R$ " + total_value
|
||||
END IF
|
||||
|
||||
' =====================================================================
|
||||
' FINISH
|
||||
' =====================================================================
|
||||
results.completed_at = NOW()
|
||||
results.duration_seconds = DATEDIFF("second", results.started_at, results.completed_at)
|
||||
|
||||
PRINT ""
|
||||
PRINT "=== CRM Automations Completed ==="
|
||||
PRINT "Duration: " + results.duration_seconds + " seconds"
|
||||
PRINT "Actions taken: " + UBOUND(results.actions)
|
||||
|
||||
' Log results
|
||||
SAVE "automation_logs", results
|
||||
|
||||
RETURN results
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
REM Queue Monitor - Automated Queue Management
|
||||
REM Runs on schedule to monitor queue health, reassign stale conversations,
|
||||
REM notify supervisors of issues, and generate queue metrics.
|
||||
REM
|
||||
REM Schedule: SET SCHEDULE "queue-monitor", "*/5 * * * *" (every 5 minutes)
|
||||
|
||||
DESCRIPTION "Monitor queue health, reassign stale conversations, alert on issues"
|
||||
|
||||
' Get current queue status
|
||||
queue = GET QUEUE
|
||||
|
||||
PRINT "Queue Monitor Running - " + FORMAT(NOW(), "yyyy-MM-dd HH:mm:ss")
|
||||
PRINT "Total: " + queue.total + " | Waiting: " + queue.waiting + " | Active: " + queue.active
|
||||
|
||||
' === Check for stale waiting conversations ===
|
||||
' Conversations waiting more than 10 minutes without assignment
|
||||
stale_threshold_minutes = 10
|
||||
|
||||
FOR EACH item IN queue.items
|
||||
IF item.status = "waiting" THEN
|
||||
' Calculate wait time
|
||||
created_at = GET SESSION item.session_id, "created_at"
|
||||
wait_minutes = DATEDIFF("minute", created_at, NOW())
|
||||
|
||||
IF wait_minutes > stale_threshold_minutes THEN
|
||||
PRINT "ALERT: Stale conversation " + item.session_id + " waiting " + wait_minutes + " minutes"
|
||||
|
||||
' Try to find available attendant
|
||||
attendants = GET ATTENDANTS "online"
|
||||
|
||||
IF attendants.count > 0 THEN
|
||||
' Find attendant with least active conversations
|
||||
best_attendant = NULL
|
||||
min_active = 999
|
||||
|
||||
FOR EACH att IN attendants.items
|
||||
stats = GET ATTENDANT STATS att.id
|
||||
IF stats.active_conversations < min_active THEN
|
||||
min_active = stats.active_conversations
|
||||
best_attendant = att
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
IF best_attendant IS NOT NULL AND min_active < 5 THEN
|
||||
' Auto-assign
|
||||
result = ASSIGN CONVERSATION item.session_id, best_attendant.id
|
||||
|
||||
IF result.success THEN
|
||||
PRINT "Auto-assigned " + item.session_id + " to " + best_attendant.name
|
||||
ADD NOTE item.session_id, "Auto-assigned after " + wait_minutes + " min wait"
|
||||
|
||||
' Notify customer
|
||||
SEND TO SESSION item.session_id, "Obrigado por aguardar! " + best_attendant.name + " irá atendê-lo agora."
|
||||
END IF
|
||||
END IF
|
||||
ELSE
|
||||
' No attendants available - escalate
|
||||
IF wait_minutes > 20 THEN
|
||||
' Critical - notify supervisor
|
||||
SEND MAIL "supervisor@company.com", "URGENTE: Fila sem atendentes", "Conversa " + item.session_id + " aguardando há " + wait_minutes + " minutos sem atendentes disponíveis."
|
||||
|
||||
' Set high priority
|
||||
SET PRIORITY item.session_id, "urgent"
|
||||
TAG CONVERSATION item.session_id, "no-attendants"
|
||||
|
||||
' Send apology to customer
|
||||
SEND TO SESSION item.session_id, "Pedimos desculpas pela espera. Nossa equipe está com alta demanda. Você será atendido em breve."
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
' === Check for inactive assigned conversations ===
|
||||
' Conversations assigned but no activity for 15 minutes
|
||||
inactive_threshold_minutes = 15
|
||||
|
||||
FOR EACH item IN queue.items
|
||||
IF item.status = "assigned" OR item.status = "active" THEN
|
||||
last_activity = GET SESSION item.session_id, "last_activity_at"
|
||||
|
||||
IF last_activity IS NOT NULL THEN
|
||||
inactive_minutes = DATEDIFF("minute", last_activity, NOW())
|
||||
|
||||
IF inactive_minutes > inactive_threshold_minutes THEN
|
||||
PRINT "WARNING: Inactive conversation " + item.session_id + " - " + inactive_minutes + " min since last activity"
|
||||
|
||||
' Get assigned attendant
|
||||
assigned_to = GET SESSION item.session_id, "assigned_to"
|
||||
|
||||
IF assigned_to IS NOT NULL THEN
|
||||
' Check attendant status
|
||||
att_status = GET ATTENDANT STATUS assigned_to
|
||||
|
||||
IF att_status = "offline" OR att_status = "away" THEN
|
||||
' Attendant went away - reassign
|
||||
PRINT "Attendant " + assigned_to + " is " + att_status + " - reassigning"
|
||||
|
||||
' Find new attendant
|
||||
new_attendants = GET ATTENDANTS "online"
|
||||
IF new_attendants.count > 0 THEN
|
||||
new_att = new_attendants.items[0]
|
||||
|
||||
' Transfer conversation
|
||||
old_ctx = GET SESSION item.session_id, "context"
|
||||
result = ASSIGN CONVERSATION item.session_id, new_att.id
|
||||
|
||||
IF result.success THEN
|
||||
ADD NOTE item.session_id, "Reatribuído de " + assigned_to + " (status: " + att_status + ") para " + new_att.name
|
||||
SEND TO SESSION item.session_id, "Desculpe a espera. " + new_att.name + " continuará seu atendimento."
|
||||
END IF
|
||||
END IF
|
||||
ELSE
|
||||
' Attendant is online but inactive - send reminder
|
||||
' Only remind once every 10 minutes
|
||||
last_reminder = GET SESSION item.session_id, "last_attendant_reminder"
|
||||
|
||||
IF last_reminder IS NULL OR DATEDIFF("minute", last_reminder, NOW()) > 10 THEN
|
||||
' Get customer sentiment
|
||||
sentiment = GET SESSION item.session_id, "last_sentiment"
|
||||
|
||||
reminder = "⚠️ Conversa inativa há " + inactive_minutes + " min"
|
||||
IF sentiment = "negative" THEN
|
||||
reminder = reminder + " - Cliente frustrado!"
|
||||
END IF
|
||||
|
||||
' Send reminder via WebSocket to attendant UI
|
||||
NOTIFY ATTENDANT assigned_to, reminder
|
||||
SET SESSION item.session_id, "last_attendant_reminder", NOW()
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
' === Check for abandoned conversations ===
|
||||
' Customer hasn't responded in 30 minutes
|
||||
abandon_threshold_minutes = 30
|
||||
|
||||
FOR EACH item IN queue.items
|
||||
IF item.status = "active" OR item.status = "assigned" THEN
|
||||
last_customer_msg = GET SESSION item.session_id, "last_customer_message_at"
|
||||
|
||||
IF last_customer_msg IS NOT NULL THEN
|
||||
silent_minutes = DATEDIFF("minute", last_customer_msg, NOW())
|
||||
|
||||
IF silent_minutes > abandon_threshold_minutes THEN
|
||||
' Check if already marked
|
||||
already_marked = GET SESSION item.session_id, "abandon_warning_sent"
|
||||
|
||||
IF already_marked IS NULL THEN
|
||||
' Send follow-up
|
||||
SEND TO SESSION item.session_id, "Ainda está aí? Se precisar de mais ajuda, é só responder."
|
||||
SET SESSION item.session_id, "abandon_warning_sent", NOW()
|
||||
PRINT "Sent follow-up to potentially abandoned session " + item.session_id
|
||||
ELSE
|
||||
' Check if warning was sent more than 15 min ago
|
||||
warning_minutes = DATEDIFF("minute", already_marked, NOW())
|
||||
|
||||
IF warning_minutes > 15 THEN
|
||||
' Mark as abandoned
|
||||
RESOLVE CONVERSATION item.session_id, "Abandoned - no customer response"
|
||||
TAG CONVERSATION item.session_id, "abandoned"
|
||||
PRINT "Marked session " + item.session_id + " as abandoned"
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
' === Generate queue metrics ===
|
||||
' Calculate averages and store for analytics
|
||||
|
||||
metrics = {}
|
||||
metrics.timestamp = NOW()
|
||||
metrics.total_waiting = queue.waiting
|
||||
metrics.total_active = queue.active
|
||||
metrics.total_resolved = queue.resolved
|
||||
|
||||
' Calculate average wait time for waiting conversations
|
||||
total_wait = 0
|
||||
wait_count = 0
|
||||
|
||||
FOR EACH item IN queue.items
|
||||
IF item.status = "waiting" THEN
|
||||
created_at = GET SESSION item.session_id, "created_at"
|
||||
wait_minutes = DATEDIFF("minute", created_at, NOW())
|
||||
total_wait = total_wait + wait_minutes
|
||||
wait_count = wait_count + 1
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
IF wait_count > 0 THEN
|
||||
metrics.avg_wait_minutes = total_wait / wait_count
|
||||
ELSE
|
||||
metrics.avg_wait_minutes = 0
|
||||
END IF
|
||||
|
||||
' Get attendant utilization
|
||||
attendants = GET ATTENDANTS
|
||||
online_count = 0
|
||||
busy_count = 0
|
||||
|
||||
FOR EACH att IN attendants.items
|
||||
IF att.status = "online" THEN
|
||||
online_count = online_count + 1
|
||||
ELSE IF att.status = "busy" THEN
|
||||
busy_count = busy_count + 1
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
metrics.attendants_online = online_count
|
||||
metrics.attendants_busy = busy_count
|
||||
metrics.utilization_pct = 0
|
||||
|
||||
IF online_count + busy_count > 0 THEN
|
||||
metrics.utilization_pct = (busy_count / (online_count + busy_count)) * 100
|
||||
END IF
|
||||
|
||||
' Store metrics for dashboard
|
||||
SAVE "queue_metrics", metrics
|
||||
|
||||
' Alert if queue is getting long
|
||||
IF queue.waiting > 10 THEN
|
||||
SEND MAIL "supervisor@company.com", "Alerta: Fila com " + queue.waiting + " aguardando", "A fila de atendimento está com " + queue.waiting + " conversas aguardando. Tempo médio de espera: " + metrics.avg_wait_minutes + " minutos."
|
||||
END IF
|
||||
|
||||
' Alert if no attendants online during business hours
|
||||
hour_now = HOUR(NOW())
|
||||
day_now = WEEKDAY(NOW())
|
||||
|
||||
IF hour_now >= 9 AND hour_now < 18 AND day_now >= 1 AND day_now <= 5 THEN
|
||||
IF online_count = 0 AND busy_count = 0 THEN
|
||||
SEND MAIL "supervisor@company.com", "CRÍTICO: Sem atendentes online", "Não há atendentes online durante o horário comercial. Fila: " + queue.waiting + " aguardando."
|
||||
END IF
|
||||
END IF
|
||||
|
||||
PRINT "Queue monitor completed. Metrics saved."
|
||||
RETURN metrics
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
REM Attendance CRM Bot - Intelligent Routing with AI-Assisted Human Handoff
|
||||
REM This bot handles customer inquiries, detects frustration, and seamlessly
|
||||
REM transfers to human attendants when needed. Uses LLM assist features.
|
||||
|
||||
DESCRIPTION "Main entry point for Attendance CRM - routes to bot or human based on context"
|
||||
|
||||
' Check if this is a returning customer
|
||||
customer_id = GET "session.customer_id"
|
||||
is_returning = customer_id IS NOT NULL
|
||||
|
||||
' Check if session already needs human
|
||||
needs_human = GET "session.needs_human"
|
||||
IF needs_human = TRUE THEN
|
||||
' Already in human mode - message goes to attendant automatically
|
||||
RETURN
|
||||
END IF
|
||||
|
||||
' Get the customer's message
|
||||
message = GET "user.message"
|
||||
|
||||
' Analyze sentiment immediately
|
||||
sentiment = ANALYZE SENTIMENT session.id, message
|
||||
|
||||
' Store sentiment for tracking
|
||||
SET "session.last_sentiment", sentiment.overall
|
||||
SET "session.escalation_risk", sentiment.escalation_risk
|
||||
|
||||
' Track frustration count
|
||||
frustration_count = GET "session.frustration_count"
|
||||
IF frustration_count IS NULL THEN
|
||||
frustration_count = 0
|
||||
END IF
|
||||
|
||||
IF sentiment.overall = "negative" THEN
|
||||
frustration_count = frustration_count + 1
|
||||
SET "session.frustration_count", frustration_count
|
||||
END IF
|
||||
|
||||
' Auto-transfer on high frustration or explicit request
|
||||
transfer_keywords = ["falar com humano", "talk to human", "atendente", "pessoa real", "real person", "speak to someone", "manager", "gerente", "supervisor"]
|
||||
|
||||
should_transfer = FALSE
|
||||
transfer_reason = ""
|
||||
|
||||
' Check for explicit transfer request
|
||||
FOR EACH keyword IN transfer_keywords
|
||||
IF LOWER(message) CONTAINS keyword THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "Customer requested human assistance"
|
||||
END IF
|
||||
NEXT
|
||||
|
||||
' Check for high escalation risk
|
||||
IF sentiment.escalation_risk = "high" THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "High escalation risk detected - sentiment: " + sentiment.overall
|
||||
END IF
|
||||
|
||||
' Check for repeated frustration
|
||||
IF frustration_count >= 3 THEN
|
||||
should_transfer = TRUE
|
||||
transfer_reason = "Customer frustrated after " + frustration_count + " messages"
|
||||
END IF
|
||||
|
||||
' Execute transfer if needed
|
||||
IF should_transfer THEN
|
||||
' Get tips for the attendant before transfer
|
||||
tips = GET TIPS session.id, message
|
||||
|
||||
' Build context for attendant
|
||||
context_summary = "Customer: " + (GET "session.customer_name" OR "Unknown") + "\n"
|
||||
context_summary = context_summary + "Channel: " + (GET "session.channel" OR "web") + "\n"
|
||||
context_summary = context_summary + "Sentiment: " + sentiment.emoji + " " + sentiment.overall + "\n"
|
||||
context_summary = context_summary + "Escalation Risk: " + sentiment.escalation_risk + "\n"
|
||||
context_summary = context_summary + "Frustration Count: " + frustration_count + "\n"
|
||||
|
||||
IF tips.success AND UBOUND(tips.items) > 0 THEN
|
||||
context_summary = context_summary + "\nAI Tips:\n"
|
||||
FOR EACH tip IN tips.items
|
||||
context_summary = context_summary + "- " + tip.content + "\n"
|
||||
NEXT
|
||||
END IF
|
||||
|
||||
' Set priority based on sentiment
|
||||
priority = "normal"
|
||||
IF sentiment.escalation_risk = "high" OR sentiment.urgency = "urgent" THEN
|
||||
priority = "urgent"
|
||||
ELSE IF sentiment.overall = "negative" THEN
|
||||
priority = "high"
|
||||
END IF
|
||||
|
||||
' Transfer to human
|
||||
result = TRANSFER TO HUMAN "support", priority, context_summary
|
||||
|
||||
IF result.success THEN
|
||||
IF result.status = "assigned" THEN
|
||||
TALK "Estou transferindo você para " + result.assigned_to_name + ". Um momento, por favor."
|
||||
TALK "I'm connecting you with " + result.assigned_to_name + ". Please hold."
|
||||
ELSE IF result.status = "queued" THEN
|
||||
TALK "Você está na posição " + result.queue_position + " da fila. Tempo estimado: " + (result.estimated_wait_seconds / 60) + " minutos."
|
||||
TALK "You are #" + result.queue_position + " in queue. Estimated wait: " + (result.estimated_wait_seconds / 60) + " minutes."
|
||||
END IF
|
||||
|
||||
' Tag the conversation
|
||||
TAG CONVERSATION session.id, "transferred"
|
||||
TAG CONVERSATION session.id, sentiment.overall
|
||||
|
||||
' Add note for attendant
|
||||
ADD NOTE session.id, "Auto-transferred: " + transfer_reason
|
||||
|
||||
RETURN
|
||||
ELSE
|
||||
' Transfer failed - continue with bot but apologize
|
||||
TALK "Nossos atendentes estão ocupados no momento. Vou fazer o meu melhor para ajudá-lo."
|
||||
TALK "Our agents are currently busy. I'll do my best to help you."
|
||||
END IF
|
||||
END IF
|
||||
|
||||
' Continue with bot processing
|
||||
' Check for common intents
|
||||
message_lower = LOWER(message)
|
||||
|
||||
IF message_lower CONTAINS "preço" OR message_lower CONTAINS "price" OR message_lower CONTAINS "quanto custa" OR message_lower CONTAINS "how much" THEN
|
||||
' Pricing inquiry - could create lead
|
||||
SET "session.intent", "pricing"
|
||||
USE TOOL "pricing-inquiry"
|
||||
RETURN
|
||||
END IF
|
||||
|
||||
IF message_lower CONTAINS "problema" OR message_lower CONTAINS "problem" OR message_lower CONTAINS "não funciona" OR message_lower CONTAINS "not working" THEN
|
||||
' Support issue
|
||||
SET "session.intent", "support"
|
||||
USE TOOL "support-ticket"
|
||||
RETURN
|
||||
END IF
|
||||
|
||||
IF message_lower CONTAINS "status" OR message_lower CONTAINS "pedido" OR message_lower CONTAINS "order" OR message_lower CONTAINS "entrega" OR message_lower CONTAINS "delivery" THEN
|
||||
' Order status
|
||||
SET "session.intent", "order_status"
|
||||
USE TOOL "order-status"
|
||||
RETURN
|
||||
END IF
|
||||
|
||||
IF message_lower CONTAINS "cancelar" OR message_lower CONTAINS "cancel" OR message_lower CONTAINS "reembolso" OR message_lower CONTAINS "refund" THEN
|
||||
' Cancellation/Refund - often needs human
|
||||
SET "session.intent", "cancellation"
|
||||
SET PRIORITY session.id, "high"
|
||||
|
||||
TALK "Entendo que você deseja cancelar ou obter reembolso. Deixe-me verificar sua conta."
|
||||
TALK "I understand you want to cancel or get a refund. Let me check your account."
|
||||
|
||||
' Check if VIP customer - auto transfer
|
||||
IF GET "session.customer_tier" = "vip" OR GET "session.customer_tier" = "enterprise" THEN
|
||||
result = TRANSFER TO HUMAN "support", "high", "VIP customer requesting cancellation/refund"
|
||||
IF result.success THEN
|
||||
TALK "Como cliente VIP, estou conectando você diretamente com nossa equipe especializada."
|
||||
RETURN
|
||||
END IF
|
||||
END IF
|
||||
|
||||
USE TOOL "cancellation-flow"
|
||||
RETURN
|
||||
END IF
|
||||
|
||||
' Default: Use LLM for general conversation
|
||||
response = LLM "Respond helpfully to: " + message
|
||||
TALK response
|
||||
|
||||
' After each bot response, check if we should suggest human
|
||||
IF sentiment.overall = "negative" THEN
|
||||
TALK "Se preferir falar com um atendente humano, é só me avisar."
|
||||
TALK "If you'd prefer to speak with a human agent, just let me know."
|
||||
END IF
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
name,value
|
||||
|
||||
# Bot Identity
|
||||
bot-name,Attendance CRM Bot
|
||||
bot-description,Hybrid AI + Human support with CRM integration
|
||||
|
||||
# CRM / Human Handoff - Required
|
||||
crm-enabled,true
|
||||
|
||||
# LLM Assist Features for Attendants
|
||||
attendant-llm-tips,true
|
||||
attendant-polish-message,true
|
||||
attendant-smart-replies,true
|
||||
attendant-auto-summary,true
|
||||
attendant-sentiment-analysis,true
|
||||
|
||||
# Bot Personality (used for LLM assist context)
|
||||
bot-system-prompt,You are a professional customer service assistant. Be helpful and empathetic. When issues are complex or customers are frustrated transfer to human support.
|
||||
|
||||
# Auto-transfer triggers
|
||||
auto-transfer-on-frustration,true
|
||||
auto-transfer-threshold,3
|
||||
|
||||
# Queue Settings
|
||||
queue-timeout-minutes,30
|
||||
queue-notify-interval,5
|
||||
|
||||
# Lead Scoring
|
||||
lead-score-threshold-hot,70
|
||||
lead-score-threshold-warm,50
|
||||
|
||||
# Follow-up Automation
|
||||
follow-up-1-day,true
|
||||
follow-up-3-day,true
|
||||
follow-up-7-day,true
|
||||
|
||||
# Collections Automation
|
||||
collections-enabled,true
|
||||
collections-grace-days,3
|
||||
|
||||
# Working Hours (for auto-responses)
|
||||
business-hours-start,09:00
|
||||
business-hours-end,18:00
|
||||
business-days,1-5
|
||||
|
||||
# Notifications
|
||||
notify-on-vip,true
|
||||
notify-on-escalation,true
|
||||
notify-email,support@company.com
|
||||
|
6
templates/sales/attendance-crm.gbai/attendant.csv
Normal file
6
templates/sales/attendance-crm.gbai/attendant.csv
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
id,name,channel,preferences,department,aliases,phone,email,teams,google
|
||||
att-001,João Silva,all,sales,commercial,joao;js;silva,+5511999990001,joao.silva@company.com,joao.silva@company.onmicrosoft.com,joao.silva@company.com
|
||||
att-002,Maria Santos,whatsapp,support,customer-service,maria;ms,+5511999990002,maria.santos@company.com,maria.santos@company.onmicrosoft.com,maria.santos@gmail.com
|
||||
att-003,Pedro Costa,web,technical,engineering,pedro;pc;tech,+5511999990003,pedro.costa@company.com,pedro.costa@company.onmicrosoft.com,pedro.costa@company.com
|
||||
att-004,Ana Oliveira,all,collections,finance,ana;ao;cobranca,+5511999990004,ana.oliveira@company.com,ana.oliveira@company.onmicrosoft.com,ana.oliveira@company.com
|
||||
att-005,Carlos Souza,whatsapp,sales,commercial,carlos;cs,+5511999990005,carlos.souza@company.com,carlos.souza@company.onmicrosoft.com,carlos.souza@gmail.com
|
||||
|
Loading…
Add table
Reference in a new issue