From 49462a3538497aa4d33d3e810bb95954c9299e4a Mon Sep 17 00:00:00 2001 From: Claude AI Date: Thu, 29 Jan 2026 10:54:12 +0000 Subject: [PATCH] feat(fase3): add AssignmentService for queue management Co-Authored-By: Claude Opus 4.5 --- .../api-gateway/app/services/assignment.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 services/api-gateway/app/services/assignment.py diff --git a/services/api-gateway/app/services/assignment.py b/services/api-gateway/app/services/assignment.py new file mode 100644 index 0000000..635b8bb --- /dev/null +++ b/services/api-gateway/app/services/assignment.py @@ -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