- Fix match arms with identical bodies by consolidating patterns - Fix case-insensitive file extension comparisons using eq_ignore_ascii_case - Fix unnecessary Debug formatting in log/format macros - Fix clone_from usage instead of clone assignment - Fix let...else patterns where appropriate - Fix format! append to String using write! macro - Fix unwrap_or with function calls to use unwrap_or_else - Add missing fields to manual Debug implementations - Fix duplicate code in if blocks - Add type aliases for complex types - Rename struct fields to avoid common prefixes - Various other clippy warning fixes Note: Some 'unused async' warnings remain for functions that are called with .await but don't contain await internally - these are kept async for API compatibility.
762 lines
24 KiB
Rust
762 lines
24 KiB
Rust
use crate::shared::models::UserSession;
|
|
use crate::shared::state::AppState;
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
Json,
|
|
};
|
|
use chrono::Utc;
|
|
use diesel::prelude::*;
|
|
use log::{error, info, warn};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct QueueItem {
|
|
pub session_id: Uuid,
|
|
pub user_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub channel: String,
|
|
pub user_name: String,
|
|
pub user_email: Option<String>,
|
|
pub last_message: String,
|
|
pub last_message_time: String,
|
|
pub waiting_time_seconds: i64,
|
|
pub priority: i32,
|
|
pub status: QueueStatus,
|
|
pub assigned_to: Option<Uuid>,
|
|
pub assigned_to_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum QueueStatus {
|
|
Waiting,
|
|
Assigned,
|
|
Active,
|
|
Resolved,
|
|
Abandoned,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AttendantStats {
|
|
pub attendant_id: String,
|
|
pub attendant_name: String,
|
|
pub channel: String,
|
|
pub preferences: String,
|
|
pub active_conversations: i32,
|
|
pub total_handled_today: i32,
|
|
pub avg_response_time_seconds: i32,
|
|
pub status: AttendantStatus,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AttendantCSV {
|
|
pub id: String,
|
|
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)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AttendantStatus {
|
|
Online,
|
|
Busy,
|
|
Away,
|
|
Offline,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AssignRequest {
|
|
pub session_id: Uuid,
|
|
pub attendant_id: Uuid,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TransferRequest {
|
|
pub session_id: Uuid,
|
|
pub from_attendant_id: Uuid,
|
|
pub to_attendant_id: Uuid,
|
|
pub reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct QueueFilters {
|
|
pub channel: Option<String>,
|
|
pub status: Option<String>,
|
|
pub assigned_to: Option<Uuid>,
|
|
}
|
|
|
|
fn is_transfer_enabled(bot_id: Uuid, work_path: &str) -> bool {
|
|
let config_path = PathBuf::from(work_path)
|
|
.join(format!("{}.gbai", bot_id))
|
|
.join("config.csv");
|
|
|
|
if !config_path.exists() {
|
|
let alt_path = PathBuf::from(work_path).join("config.csv");
|
|
if alt_path.exists() {
|
|
return check_config_for_crm_enabled(&alt_path);
|
|
}
|
|
warn!("Config file not found: {}", config_path.display());
|
|
return false;
|
|
}
|
|
|
|
check_config_for_crm_enabled(&config_path)
|
|
}
|
|
|
|
fn check_config_for_crm_enabled(config_path: &PathBuf) -> bool {
|
|
match std::fs::read_to_string(config_path) {
|
|
Ok(content) => {
|
|
for line in content.lines() {
|
|
let line_lower = line.to_lowercase();
|
|
|
|
if (line_lower.contains("crm-enabled") || line_lower.contains("crm_enabled"))
|
|
&& line_lower.contains("true")
|
|
{
|
|
info!("CRM enabled via crm-enabled setting");
|
|
return true;
|
|
}
|
|
|
|
if line_lower.contains("transfer") && line_lower.contains("true") {
|
|
info!("CRM enabled via legacy transfer setting");
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to read config file: {}", e);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
fn read_attendants_csv(bot_id: Uuid, work_path: &str) -> Vec<AttendantCSV> {
|
|
let attendant_path = PathBuf::from(work_path)
|
|
.join(format!("{}.gbai", bot_id))
|
|
.join("attendant.csv");
|
|
|
|
if !attendant_path.exists() {
|
|
warn!("Attendant file not found: {}", attendant_path.display());
|
|
return Vec::new();
|
|
}
|
|
|
|
match std::fs::read_to_string(&attendant_path) {
|
|
Ok(content) => {
|
|
let mut attendants = Vec::new();
|
|
let mut lines = content.lines();
|
|
|
|
lines.next();
|
|
|
|
for line in lines {
|
|
let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
|
|
if parts.len() >= 4 {
|
|
attendants.push(AttendantCSV {
|
|
id: (*parts[0]).to_string(),
|
|
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()),
|
|
});
|
|
}
|
|
}
|
|
attendants
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to read attendant file: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn find_attendant_by_identifier(
|
|
bot_id: Uuid,
|
|
work_path: &str,
|
|
identifier: &str,
|
|
) -> Option<AttendantCSV> {
|
|
let attendants = read_attendants_csv(bot_id, work_path);
|
|
let identifier_lower = identifier.to_lowercase().trim().to_string();
|
|
|
|
for att in attendants {
|
|
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 {
|
|
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);
|
|
}
|
|
}
|
|
|
|
if let Some(ref aliases) = att.aliases {
|
|
for alias in aliases.split(';') {
|
|
if alias.trim().to_lowercase() == identifier_lower {
|
|
return Some(att);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn find_attendants_by_channel(
|
|
bot_id: Uuid,
|
|
work_path: &str,
|
|
channel: &str,
|
|
) -> Vec<AttendantCSV> {
|
|
let attendants = read_attendants_csv(bot_id, work_path);
|
|
let channel_lower = channel.to_lowercase();
|
|
|
|
attendants
|
|
.into_iter()
|
|
.filter(|att| {
|
|
att.channel.to_lowercase() == "all" || att.channel.to_lowercase() == channel_lower
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn find_attendants_by_department(
|
|
bot_id: Uuid,
|
|
work_path: &str,
|
|
department: &str,
|
|
) -> Vec<AttendantCSV> {
|
|
let attendants = read_attendants_csv(bot_id, work_path);
|
|
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()
|
|
}
|
|
|
|
pub async fn list_queue(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(filters): Query<QueueFilters>,
|
|
) -> impl IntoResponse {
|
|
info!("Listing queue items with filters: {:?}", filters);
|
|
|
|
let result = tokio::task::spawn_blocking({
|
|
let conn = state.conn.clone();
|
|
move || {
|
|
let mut db_conn = conn
|
|
.get()
|
|
.map_err(|e| format!("Failed to get database connection: {}", e))?;
|
|
|
|
use crate::shared::models::schema::user_sessions;
|
|
use crate::shared::models::schema::users;
|
|
|
|
let sessions_data: Vec<UserSession> = user_sessions::table
|
|
.order(user_sessions::created_at.desc())
|
|
.limit(50)
|
|
.load(&mut db_conn)
|
|
.map_err(|e| format!("Failed to load sessions: {}", e))?;
|
|
|
|
let mut queue_items = Vec::new();
|
|
|
|
for session_data in sessions_data {
|
|
let user_info: Option<(String, String)> = users::table
|
|
.filter(users::id.eq(session_data.user_id))
|
|
.select((users::username, users::email))
|
|
.first(&mut db_conn)
|
|
.optional()
|
|
.map_err(|e| format!("Failed to load user: {}", e))?;
|
|
|
|
let (uname, uemail) = user_info.unwrap_or_else(|| {
|
|
(
|
|
format!("user_{}", session_data.user_id),
|
|
format!("{}@unknown.local", session_data.user_id),
|
|
)
|
|
});
|
|
|
|
let channel = session_data
|
|
.context_data
|
|
.get("channel")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("web")
|
|
.to_string();
|
|
|
|
let waiting_time = (Utc::now() - session_data.updated_at).num_seconds();
|
|
|
|
queue_items.push(QueueItem {
|
|
session_id: session_data.id,
|
|
user_id: session_data.user_id,
|
|
bot_id: session_data.bot_id,
|
|
channel,
|
|
user_name: uname,
|
|
user_email: Some(uemail),
|
|
last_message: session_data.title.clone(),
|
|
last_message_time: session_data.updated_at.to_rfc3339(),
|
|
waiting_time_seconds: waiting_time,
|
|
priority: if waiting_time > 300 { 2 } else { 1 },
|
|
status: QueueStatus::Waiting,
|
|
assigned_to: None,
|
|
assigned_to_name: None,
|
|
});
|
|
}
|
|
|
|
Ok::<Vec<QueueItem>, String>(queue_items)
|
|
}
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(queue_items)) => {
|
|
info!("Found {} queue items", queue_items.len());
|
|
(StatusCode::OK, Json(queue_items))
|
|
}
|
|
Ok(Err(e)) => {
|
|
error!("Queue list error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(vec![] as Vec<QueueItem>),
|
|
)
|
|
}
|
|
Err(e) => {
|
|
error!("Task error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(vec![] as Vec<QueueItem>),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn list_attendants(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
info!("Listing attendants");
|
|
|
|
let bot_id_str = params.get("bot_id").cloned().unwrap_or_default();
|
|
let bot_id = match Uuid::parse_str(&bot_id_str) {
|
|
Ok(id) => id,
|
|
Err(_) => {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().ok()?;
|
|
use crate::shared::models::schema::bots;
|
|
bots::table
|
|
.filter(bots::is_active.eq(true))
|
|
.select(bots::id)
|
|
.first::<Uuid>(&mut db_conn)
|
|
.ok()
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Some(id)) => id,
|
|
_ => {
|
|
error!("No valid bot_id provided and no default bot found");
|
|
return (StatusCode::BAD_REQUEST, Json(vec![] as Vec<AttendantStats>));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let work_path = "./work";
|
|
if !is_transfer_enabled(bot_id, work_path) {
|
|
warn!("Transfer not enabled for bot {}", bot_id);
|
|
return (StatusCode::OK, Json(vec![] as Vec<AttendantStats>));
|
|
}
|
|
|
|
let attendant_csvs = read_attendants_csv(bot_id, work_path);
|
|
|
|
let attendants: Vec<AttendantStats> = attendant_csvs
|
|
.into_iter()
|
|
.map(|att| AttendantStats {
|
|
attendant_id: att.id,
|
|
attendant_name: att.name,
|
|
channel: att.channel,
|
|
preferences: att.preferences,
|
|
active_conversations: 0,
|
|
total_handled_today: 0,
|
|
avg_response_time_seconds: 0,
|
|
status: AttendantStatus::Online,
|
|
})
|
|
.collect();
|
|
|
|
info!("Found {} attendants from CSV", attendants.len());
|
|
(StatusCode::OK, Json(attendants))
|
|
}
|
|
|
|
pub async fn assign_conversation(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(request): Json<AssignRequest>,
|
|
) -> impl IntoResponse {
|
|
info!(
|
|
"Assigning session {} to attendant {}",
|
|
request.session_id, request.attendant_id
|
|
);
|
|
|
|
let result = tokio::task::spawn_blocking({
|
|
let conn = state.conn.clone();
|
|
let session_id = request.session_id;
|
|
let attendant_id = request.attendant_id;
|
|
|
|
move || {
|
|
let mut db_conn = conn
|
|
.get()
|
|
.map_err(|e| format!("Failed to get database connection: {}", e))?;
|
|
|
|
use crate::shared::models::schema::user_sessions;
|
|
|
|
let session: UserSession = user_sessions::table
|
|
.filter(user_sessions::id.eq(session_id))
|
|
.first(&mut db_conn)
|
|
.map_err(|e| format!("Session not found: {}", e))?;
|
|
|
|
let mut ctx = session.context_data;
|
|
ctx["assigned_to"] = serde_json::json!(attendant_id.to_string());
|
|
ctx["assigned_at"] = serde_json::json!(Utc::now().to_rfc3339());
|
|
ctx["status"] = serde_json::json!("assigned");
|
|
|
|
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| format!("Failed to update session: {}", e))?;
|
|
|
|
Ok::<(), String>(())
|
|
}
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(())) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"success": true,
|
|
"session_id": request.session_id,
|
|
"attendant_id": request.attendant_id,
|
|
"assigned_at": Utc::now().to_rfc3339()
|
|
})),
|
|
),
|
|
Ok(Err(e)) => {
|
|
error!("Assignment error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"success": false,
|
|
"error": e
|
|
})),
|
|
)
|
|
}
|
|
Err(e) => {
|
|
error!("Assignment error: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"success": false,
|
|
"error": format!("{:?}", e)
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn transfer_conversation(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(request): Json<TransferRequest>,
|
|
) -> impl IntoResponse {
|
|
info!(
|
|
"Transferring session {} from {} to {}",
|
|
request.session_id, request.from_attendant_id, request.to_attendant_id
|
|
);
|
|
|
|
let result = tokio::task::spawn_blocking({
|
|
let conn = state.conn.clone();
|
|
let session_id = request.session_id;
|
|
let to_attendant = request.to_attendant_id;
|
|
let reason = request.reason.clone();
|
|
|
|
move || {
|
|
let mut db_conn = conn
|
|
.get()
|
|
.map_err(|e| format!("Failed to get database connection: {}", e))?;
|
|
|
|
use crate::shared::models::schema::user_sessions;
|
|
|
|
let session: UserSession = user_sessions::table
|
|
.filter(user_sessions::id.eq(session_id))
|
|
.first(&mut db_conn)
|
|
.map_err(|e| format!("Session not found: {}", e))?;
|
|
|
|
let mut ctx = session.context_data;
|
|
ctx["assigned_to"] = serde_json::json!(to_attendant.to_string());
|
|
ctx["transferred_at"] = serde_json::json!(Utc::now().to_rfc3339());
|
|
ctx["transfer_reason"] = serde_json::json!(reason.unwrap_or_default());
|
|
ctx["status"] = serde_json::json!("transferred");
|
|
|
|
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>(())
|
|
}
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(())) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"success": true,
|
|
"session_id": request.session_id,
|
|
"from_attendant": request.from_attendant_id,
|
|
"to_attendant": request.to_attendant_id,
|
|
"transferred_at": Utc::now().to_rfc3339()
|
|
})),
|
|
),
|
|
Ok(Err(e)) => {
|
|
error!("Transfer error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"success": false,
|
|
"error": e
|
|
})),
|
|
)
|
|
}
|
|
Err(e) => {
|
|
error!("Transfer error: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"success": false,
|
|
"error": format!("{:?}", e)
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn resolve_conversation(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(payload): Json<serde_json::Value>,
|
|
) -> impl IntoResponse {
|
|
let session_id = payload
|
|
.get("session_id")
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| Uuid::parse_str(s).ok())
|
|
.unwrap_or_else(Uuid::nil);
|
|
|
|
info!("Resolving session {}", session_id);
|
|
|
|
let result = tokio::task::spawn_blocking({
|
|
let conn = state.conn.clone();
|
|
|
|
move || {
|
|
let mut db_conn = conn
|
|
.get()
|
|
.map_err(|e| format!("Failed to get database connection: {}", e))?;
|
|
|
|
use crate::shared::models::schema::user_sessions;
|
|
|
|
let session: UserSession = user_sessions::table
|
|
.filter(user_sessions::id.eq(session_id))
|
|
.first(&mut db_conn)
|
|
.map_err(|e| format!("Session not found: {}", e))?;
|
|
|
|
let mut ctx = session.context_data;
|
|
ctx["status"] = serde_json::json!("resolved");
|
|
ctx["resolved_at"] = serde_json::json!(Utc::now().to_rfc3339());
|
|
ctx["resolved"] = 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>(())
|
|
}
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(())) => (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"success": true,
|
|
"session_id": session_id,
|
|
"resolved_at": Utc::now().to_rfc3339()
|
|
})),
|
|
),
|
|
Ok(Err(e)) => {
|
|
error!("Resolve error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"success": false,
|
|
"error": e
|
|
})),
|
|
)
|
|
}
|
|
Err(e) => {
|
|
error!("Resolve error: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"success": false,
|
|
"error": format!("{:?}", e)
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_insights(
|
|
State(state): State<Arc<AppState>>,
|
|
Path(session_id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
info!("Getting insights for session {}", session_id);
|
|
|
|
let result = tokio::task::spawn_blocking({
|
|
let conn = state.conn.clone();
|
|
move || {
|
|
let mut db_conn = conn
|
|
.get()
|
|
.map_err(|e| format!("Failed to get database connection: {}", e))?;
|
|
|
|
use crate::shared::models::schema::message_history;
|
|
|
|
let messages: Vec<(String, i32)> = message_history::table
|
|
.filter(message_history::session_id.eq(session_id))
|
|
.select((message_history::content_encrypted, message_history::role))
|
|
.order(message_history::created_at.desc())
|
|
.limit(10)
|
|
.load(&mut db_conn)
|
|
.map_err(|e| format!("Failed to load messages: {}", e))?;
|
|
|
|
let user_messages: Vec<String> = messages
|
|
.iter()
|
|
.filter(|(_, r)| *r == 0)
|
|
.map(|(c, _)| c.clone())
|
|
.collect();
|
|
|
|
let sentiment = if user_messages.iter().any(|m| {
|
|
m.to_lowercase().contains("urgent")
|
|
|| m.to_lowercase().contains("problem")
|
|
|| m.to_lowercase().contains("issue")
|
|
}) {
|
|
"negative"
|
|
} else if user_messages
|
|
.iter()
|
|
.any(|m| m.to_lowercase().contains("thanks") || m.to_lowercase().contains("great"))
|
|
{
|
|
"positive"
|
|
} else {
|
|
"neutral"
|
|
};
|
|
|
|
let suggested_reply = if sentiment == "negative" {
|
|
"I understand this is frustrating. Let me help you resolve this immediately."
|
|
} else {
|
|
"How can I assist you further?"
|
|
};
|
|
|
|
Ok::<serde_json::Value, String>(serde_json::json!({
|
|
"session_id": session_id,
|
|
"sentiment": sentiment,
|
|
"message_count": messages.len(),
|
|
"suggested_reply": suggested_reply,
|
|
"key_topics": ["support", "technical"],
|
|
"priority": if sentiment == "negative" { "high" } else { "normal" },
|
|
"language": "en"
|
|
}))
|
|
}
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(insights)) => (StatusCode::OK, Json(insights)),
|
|
Ok(Err(e)) => {
|
|
error!("Insights error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"error": e
|
|
})),
|
|
)
|
|
}
|
|
Err(e) => {
|
|
error!("Task error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({
|
|
"error": format!("Task error: {}", e)
|
|
})),
|
|
)
|
|
}
|
|
}
|
|
}
|