from __future__ import annotations from models import HelpdeskTicketAction, HelpdeskTicketRecord ISSUE_TYPE_SIMILARITY = { ("billing_license", "service_request"): 0.4, ("service_request", "billing_license"): 0.4, ("application_support", "identity_access"): 0.5, ("identity_access", "application_support"): 0.5, ("application_support", "feature_request"): 0.35, ("feature_request", "application_support"): 0.35, ("onboarding", "identity_access"): 0.4, ("identity_access", "onboarding"): 0.4, ("general_inquiry", "feature_request"): 0.3, ("feature_request", "general_inquiry"): 0.3, ("general_inquiry", "service_request"): 0.25, ("service_request", "general_inquiry"): 0.25, ("spam_phishing", "security_compliance"): 0.4, ("security_compliance", "spam_phishing"): 0.4, ("security_compliance", "billing_license"): 0.2, ("billing_license", "security_compliance"): 0.2, } ASSIGNMENT_GROUP_SIMILARITY = { ("procurement", "license_ops"): 0.55, ("license_ops", "procurement"): 0.55, ("service_desk", "onboarding_ops"): 0.5, ("onboarding_ops", "service_desk"): 0.5, ("application_team", "security_team"): 0.35, ("security_team", "application_team"): 0.35, ("service_desk", "application_team"): 0.25, ("application_team", "service_desk"): 0.25, ("service_desk", "security_team"): 0.2, ("security_team", "service_desk"): 0.2, } RESOLUTION_ACTION_SIMILARITY = { ("assign", "escalate"): 0.6, ("escalate", "assign"): 0.6, ("acknowledge", "fulfill"): 0.35, ("fulfill", "acknowledge"): 0.35, ("assign", "fulfill"): 0.25, ("fulfill", "assign"): 0.25, ("escalate", "fulfill"): 0.2, ("fulfill", "escalate"): 0.2, ("acknowledge", "assign"): 0.2, ("assign", "acknowledge"): 0.2, } PRIORITY_SCORES = { ("critical", "high"): 0.6, ("high", "critical"): 0.6, ("high", "medium"): 0.5, ("medium", "high"): 0.5, ("medium", "low"): 0.4, ("low", "medium"): 0.4, ("critical", "medium"): 0.3, ("medium", "critical"): 0.3, ("critical", "low"): 0.1, ("low", "critical"): 0.1, ("high", "low"): 0.2, ("low", "high"): 0.2, } TASK_WEIGHTS = { 1: { "issue_type": 0.40, "priority": 0.20, "assignment_group": 0.20, "resolution_action": 0.20, }, 2: { "issue_type": 0.32, "priority": 0.20, "assignment_group": 0.24, "resolution_action": 0.24, }, 3: { "issue_type": 0.30, "priority": 0.20, "assignment_group": 0.25, "resolution_action": 0.25, }, } def _normalized(value: str | None) -> str: return (value or "").strip().lower() def _score_exact_or_similar(predicted: str | None, expected: str) -> float: pred = _normalized(predicted) exp = _normalized(expected) if not pred: return 0.0 if pred == exp: return 1.0 return ISSUE_TYPE_SIMILARITY.get((pred, exp), 0.0) def _score_exact_or_table( predicted: str | None, expected: str, similarity_table: dict[tuple[str, str], float], ) -> float: pred = _normalized(predicted) exp = _normalized(expected) if not pred: return 0.0 if pred == exp: return 1.0 return similarity_table.get((pred, exp), 0.0) def _score_priority(predicted: str | None, expected: str) -> float: pred = _normalized(predicted) exp = _normalized(expected) if not pred: return 0.0 if pred == exp: return 1.0 return PRIORITY_SCORES.get((pred, exp), 0.0) def _score_exact(predicted: str | None, expected: str) -> float: return 1.0 if _normalized(predicted) == _normalized(expected) and predicted else 0.0 def _score_route( action: HelpdeskTicketAction, *, issue_type: str, priority: str, assignment_group: str, resolution_action: str, score_multiplier: float, task_id: int, ) -> tuple[float, dict[str, float]]: field_scores = { "issue_type": _score_exact_or_similar(action.issue_type, issue_type), "priority": _score_priority(action.priority, priority), "assignment_group": _score_exact_or_table( action.assignment_group, assignment_group, ASSIGNMENT_GROUP_SIMILARITY, ), "resolution_action": _score_exact_or_table( action.resolution_action, resolution_action, RESOLUTION_ACTION_SIMILARITY, ), } if score_multiplier != 1.0: field_scores = { field: round(score * score_multiplier, 4) for field, score in field_scores.items() } weights = TASK_WEIGHTS[task_id] raw_score = sum(field_scores[field] * weight for field, weight in weights.items()) return raw_score, field_scores def _alternate_route_available(ticket: HelpdeskTicketRecord) -> bool: return any( value is not None for value in ( ticket.alternate_issue_type, ticket.alternate_priority, ticket.alternate_assignment_group, ticket.alternate_resolution_action, ) ) and ticket.alternate_route_score_multiplier > 0.0 def grade_action( action: HelpdeskTicketAction, ticket: HelpdeskTicketRecord, task_id: int, ) -> tuple[float, dict[str, float]]: if task_id not in TASK_WEIGHTS: raise ValueError(f"Unsupported task_id: {task_id}") primary_score, primary_field_scores = _score_route( action, issue_type=ticket.issue_type, priority=ticket.priority, assignment_group=ticket.assignment_group, resolution_action=ticket.resolution_action, score_multiplier=1.0, task_id=task_id, ) chosen_score = primary_score chosen_field_scores = primary_field_scores if _alternate_route_available(ticket): alternate_score, alternate_field_scores = _score_route( action, issue_type=ticket.alternate_issue_type or ticket.issue_type, priority=ticket.alternate_priority or ticket.priority, assignment_group=( ticket.alternate_assignment_group or ticket.assignment_group ), resolution_action=( ticket.alternate_resolution_action or ticket.resolution_action ), score_multiplier=ticket.alternate_route_score_multiplier, task_id=task_id, ) if alternate_score > chosen_score: chosen_score = alternate_score chosen_field_scores = alternate_field_scores score = max(0.0, min(1.0, chosen_score)) breakdown = {field: chosen_field_scores[field] for field in TASK_WEIGHTS[task_id]} return score, breakdown