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:
Rodrigo Rodriguez (Pragmatismo) 2025-12-05 14:54:55 -03:00
parent bde3244ce9
commit 1b669d4c11
10 changed files with 3293 additions and 15 deletions

View file

@ -1159,8 +1159,8 @@ async fn handle_take_command(
} }
async fn handle_status_command( async fn handle_status_command(
_state: &Arc<AppState>, state: &Arc<AppState>,
_attendant_phone: &str, attendant_phone: &str,
args: Vec<&str>, args: Vec<&str>,
) -> Result<String, String> { ) -> Result<String, String> {
if args.is_empty() { if args.is_empty() {
@ -1171,11 +1171,11 @@ async fn handle_status_command(
} }
let status = args[0].to_lowercase(); let status = args[0].to_lowercase();
let (emoji, text) = match status.as_str() { let (emoji, text, status_value) = match status.as_str() {
"online" => ("🟢", "Online - Available for conversations"), "online" => ("🟢", "Online - Available for conversations", "online"),
"busy" => ("🟡", "Busy - Handling conversations"), "busy" => ("🟡", "Busy - Handling conversations", "busy"),
"away" => ("🟠", "Away - Temporarily unavailable"), "away" => ("🟠", "Away - Temporarily unavailable", "away"),
"offline" => ("", "Offline - Not available"), "offline" => ("", "Offline - Not available", "offline"),
_ => { _ => {
return Err(format!( return Err(format!(
"Invalid status: {}. Use online, busy, away, or offline.", "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();
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)) 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( async fn handle_transfer_command(
_state: &Arc<AppState>, state: &Arc<AppState>,
current_session: Option<Uuid>, current_session: Option<Uuid>,
args: Vec<&str>, args: Vec<&str>,
) -> Result<String, String> { ) -> Result<String, String> {
@ -1201,13 +1250,65 @@ async fn handle_transfer_command(
} }
let target = args.join(" "); 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!( 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], &session_id.to_string()[..8],
target target_clean
)) ))
} }

View file

@ -65,6 +65,12 @@ pub struct AttendantCSV {
pub name: String, pub name: String,
pub channel: String, pub channel: String,
pub preferences: 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)] #[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(), name: parts[1].to_string(),
channel: parts[2].to_string(), channel: parts[2].to_string(),
preferences: parts[3].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 /api/queue/list
/// Get all conversations in queue (only if bot has transfer=true) /// Get all conversations in queue (only if bot has transfer=true)
pub async fn list_queue( pub async fn list_queue(

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
pub mod attendance;
pub mod score_lead; pub mod score_lead;
use crate::shared::models::UserSession; use crate::shared::models::UserSession;
@ -6,12 +7,21 @@ use log::debug;
use rhai::Engine; use rhai::Engine;
use std::sync::Arc; 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) { 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::score_lead_keyword(state.clone(), user.clone(), engine);
score_lead::get_lead_score_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::qualify_lead_keyword(state.clone(), user.clone(), engine);
score_lead::update_lead_score_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)");
} }

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
1 name,value
2 # Bot Identity
3 bot-name,Attendance CRM Bot
4 bot-description,Hybrid AI + Human support with CRM integration
5 # CRM / Human Handoff - Required
6 crm-enabled,true
7 # LLM Assist Features for Attendants
8 attendant-llm-tips,true
9 attendant-polish-message,true
10 attendant-smart-replies,true
11 attendant-auto-summary,true
12 attendant-sentiment-analysis,true
13 # Bot Personality (used for LLM assist context)
14 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.
15 # Auto-transfer triggers
16 auto-transfer-on-frustration,true
17 auto-transfer-threshold,3
18 # Queue Settings
19 queue-timeout-minutes,30
20 queue-notify-interval,5
21 # Lead Scoring
22 lead-score-threshold-hot,70
23 lead-score-threshold-warm,50
24 # Follow-up Automation
25 follow-up-1-day,true
26 follow-up-3-day,true
27 follow-up-7-day,true
28 # Collections Automation
29 collections-enabled,true
30 collections-grace-days,3
31 # Working Hours (for auto-responses)
32 business-hours-start,09:00
33 business-hours-end,18:00
34 business-days,1-5
35 # Notifications
36 notify-on-vip,true
37 notify-on-escalation,true
38 notify-email,support@company.com

View 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
1 id name channel preferences department aliases phone email teams google
2 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
3 att-002 Maria Santos whatsapp support customer-service maria;ms +5511999990002 maria.santos@company.com maria.santos@company.onmicrosoft.com maria.santos@gmail.com
4 att-003 Pedro Costa web technical engineering pedro;pc;tech +5511999990003 pedro.costa@company.com pedro.costa@company.onmicrosoft.com pedro.costa@company.com
5 att-004 Ana Oliveira all collections finance ana;ao;cobranca +5511999990004 ana.oliveira@company.com ana.oliveira@company.onmicrosoft.com ana.oliveira@company.com
6 att-005 Carlos Souza whatsapp sales commercial carlos;cs +5511999990005 carlos.souza@company.com carlos.souza@company.onmicrosoft.com carlos.souza@gmail.com