168 lines
6.0 KiB
Python
168 lines
6.0 KiB
Python
from sqlalchemy.orm import Session
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
from datetime import datetime
|
|
from app.models.queue import Queue, QueueAgent, AssignmentMethod
|
|
from app.models.whatsapp import Conversation, ConversationStatus
|
|
from app.models.user import User, UserStatus
|
|
|
|
|
|
class AssignmentService:
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
|
|
def assign_conversation(self, conversation_id: UUID, queue_id: UUID) -> Optional[User]:
|
|
"""Assign a conversation to an agent based on queue settings"""
|
|
queue = self.db.query(Queue).filter(Queue.id == queue_id).first()
|
|
if not queue or not queue.is_active:
|
|
return None
|
|
|
|
conversation = self.db.query(Conversation).filter(
|
|
Conversation.id == conversation_id
|
|
).first()
|
|
if not conversation:
|
|
return None
|
|
|
|
available_agents = self._get_available_agents(queue)
|
|
if not available_agents:
|
|
return None
|
|
|
|
if queue.assignment_method == AssignmentMethod.ROUND_ROBIN:
|
|
agent = self._round_robin_select(queue, available_agents)
|
|
elif queue.assignment_method == AssignmentMethod.LEAST_BUSY:
|
|
agent = self._least_busy_select(available_agents)
|
|
elif queue.assignment_method == AssignmentMethod.SKILL_BASED:
|
|
agent = self._skill_based_select(conversation, available_agents)
|
|
else:
|
|
agent = available_agents[0] if available_agents else None
|
|
|
|
if agent:
|
|
conversation.assigned_to = agent.id
|
|
conversation.queue_id = queue_id
|
|
conversation.status = ConversationStatus.ACTIVE
|
|
self.db.commit()
|
|
|
|
return agent
|
|
|
|
def _get_available_agents(self, queue: Queue) -> list:
|
|
"""Get agents who are online and have capacity"""
|
|
agents = []
|
|
for qa in queue.agents:
|
|
user = qa.user
|
|
if not user or user.status != UserStatus.ONLINE:
|
|
continue
|
|
|
|
active_count = self.db.query(Conversation).filter(
|
|
Conversation.assigned_to == user.id,
|
|
Conversation.status.in_([ConversationStatus.ACTIVE, ConversationStatus.WAITING])
|
|
).count()
|
|
|
|
if active_count < queue.max_per_agent:
|
|
agents.append(user)
|
|
|
|
return agents
|
|
|
|
def _round_robin_select(self, queue: Queue, agents: list) -> Optional[User]:
|
|
"""Select agent with round-robin: least recently assigned"""
|
|
if not agents:
|
|
return None
|
|
|
|
agent_last_assign = {}
|
|
for agent in agents:
|
|
last_conv = self.db.query(Conversation).filter(
|
|
Conversation.assigned_to == agent.id
|
|
).order_by(Conversation.created_at.desc()).first()
|
|
|
|
agent_last_assign[agent.id] = last_conv.created_at if last_conv else datetime.min
|
|
|
|
sorted_agents = sorted(agents, key=lambda a: agent_last_assign[a.id])
|
|
return sorted_agents[0] if sorted_agents else None
|
|
|
|
def _least_busy_select(self, agents: list) -> Optional[User]:
|
|
"""Select agent with fewest active conversations"""
|
|
if not agents:
|
|
return None
|
|
|
|
min_count = float('inf')
|
|
selected = None
|
|
|
|
for agent in agents:
|
|
count = self.db.query(Conversation).filter(
|
|
Conversation.assigned_to == agent.id,
|
|
Conversation.status.in_([ConversationStatus.ACTIVE, ConversationStatus.WAITING])
|
|
).count()
|
|
|
|
if count < min_count:
|
|
min_count = count
|
|
selected = agent
|
|
|
|
return selected
|
|
|
|
def _skill_based_select(self, conversation: Conversation, agents: list) -> Optional[User]:
|
|
"""Select agent based on skills (for now, just return least busy)"""
|
|
return self._least_busy_select(agents)
|
|
|
|
def transfer_to_queue(self, conversation_id: UUID, target_queue_id: UUID, keep_history: bool = True) -> bool:
|
|
"""Transfer conversation to another queue"""
|
|
conversation = self.db.query(Conversation).filter(
|
|
Conversation.id == conversation_id
|
|
).first()
|
|
if not conversation:
|
|
return False
|
|
|
|
conversation.queue_id = target_queue_id
|
|
conversation.assigned_to = None
|
|
conversation.status = ConversationStatus.WAITING
|
|
self.db.commit()
|
|
|
|
self.assign_conversation(conversation_id, target_queue_id)
|
|
return True
|
|
|
|
def transfer_to_agent(self, conversation_id: UUID, agent_id: UUID) -> bool:
|
|
"""Transfer conversation directly to a specific agent"""
|
|
conversation = self.db.query(Conversation).filter(
|
|
Conversation.id == conversation_id
|
|
).first()
|
|
if not conversation:
|
|
return False
|
|
|
|
agent = self.db.query(User).filter(User.id == agent_id).first()
|
|
if not agent:
|
|
return False
|
|
|
|
conversation.assigned_to = agent_id
|
|
conversation.status = ConversationStatus.ACTIVE
|
|
self.db.commit()
|
|
return True
|
|
|
|
def transfer_to_bot(self, conversation_id: UUID) -> bool:
|
|
"""Transfer conversation back to bot"""
|
|
conversation = self.db.query(Conversation).filter(
|
|
Conversation.id == conversation_id
|
|
).first()
|
|
if not conversation:
|
|
return False
|
|
|
|
conversation.assigned_to = None
|
|
conversation.status = ConversationStatus.BOT
|
|
self.db.commit()
|
|
return True
|
|
|
|
def resolve_conversation(self, conversation_id: UUID, csat_score: int = None, csat_feedback: str = None) -> bool:
|
|
"""Mark conversation as resolved"""
|
|
conversation = self.db.query(Conversation).filter(
|
|
Conversation.id == conversation_id
|
|
).first()
|
|
if not conversation:
|
|
return False
|
|
|
|
conversation.status = ConversationStatus.RESOLVED
|
|
conversation.resolved_at = datetime.utcnow()
|
|
if csat_score:
|
|
conversation.csat_score = csat_score
|
|
if csat_feedback:
|
|
conversation.csat_feedback = csat_feedback
|
|
|
|
self.db.commit()
|
|
return True
|