feat(fase3): add AssignmentService for queue management
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
167
services/api-gateway/app/services/assignment.py
Normal file
167
services/api-gateway/app/services/assignment.py
Normal file
@@ -0,0 +1,167 @@
|
||||
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
|
||||
Reference in New Issue
Block a user