The business problem
Enterprise support teams handle millions of tickets annually. Poor routing, sending a billing question to a technical specialist, or a complex integration issue to a junior agent, increases average resolution time by 40% and costs enterprises $22 billion annually in wasted agent time and escalations. Good routing gets the right ticket to the right agent on the first try.
Why flat ML fails
- Keyword matching is brittle: “Payment failed” could be a billing issue, a technical integration bug, or a fraud case. Keywords alone cannot disambiguate.
- No expertise modeling: Agent skills are not just tags. An agent who resolved 200 complex API integration tickets has deep expertise that a tag-based system cannot quantify.
- No customer context: A ticket from a customer with 10 previous billing issues is likely billing-related, regardless of keyword content. Flat models miss this history.
- No load balancing: The best agent for a ticket may be overloaded. Graph-aware routing considers agent capacity alongside match quality.
The relational schema
Node types:
Agent (id, team, seniority, current_load)
Ticket (id, content_emb, priority, channel, created_at)
Skill (id, category, complexity_level)
Customer (id, plan, tenure, ltv, support_history_count)
Edge types:
Agent --[has_skill]--> Skill (proficiency, tickets_resolved)
Agent --[resolved]--> Ticket (resolution_time, csat)
Ticket --[requires]--> Skill (importance)
Ticket --[filed_by]--> Customer
Customer --[has_history]--> Ticket (outcome, satisfaction)Agent-skill edges capture demonstrated expertise. Ticket-skill edges capture requirements. The GNN matches expertise to requirements while considering customer context.
PyG architecture: HeteroConv for ticket-agent matching
import torch
import torch.nn.functional as F
from torch_geometric.nn import SAGEConv, HeteroConv, Linear
class TicketRoutingGNN(torch.nn.Module):
def __init__(self, hidden_dim=128):
super().__init__()
self.agent_lin = Linear(-1, hidden_dim)
self.ticket_lin = Linear(-1, hidden_dim)
self.skill_lin = Linear(-1, hidden_dim)
self.customer_lin = Linear(-1, hidden_dim)
self.conv1 = HeteroConv({
('agent', 'has_skill', 'skill'): SAGEConv(
hidden_dim, hidden_dim),
('agent', 'resolved', 'ticket'): SAGEConv(
hidden_dim, hidden_dim),
('ticket', 'requires', 'skill'): SAGEConv(
hidden_dim, hidden_dim),
('ticket', 'filed_by', 'customer'): SAGEConv(
hidden_dim, hidden_dim),
}, aggr='sum')
self.conv2 = HeteroConv({
('agent', 'has_skill', 'skill'): SAGEConv(
hidden_dim, hidden_dim),
('ticket', 'requires', 'skill'): SAGEConv(
hidden_dim, hidden_dim),
('ticket', 'filed_by', 'customer'): SAGEConv(
hidden_dim, hidden_dim),
}, aggr='sum')
def encode(self, x_dict, edge_index_dict):
x_dict['agent'] = self.agent_lin(x_dict['agent'])
x_dict['ticket'] = self.ticket_lin(x_dict['ticket'])
x_dict['skill'] = self.skill_lin(x_dict['skill'])
x_dict['customer'] = self.customer_lin(
x_dict['customer'])
x_dict = {k: F.relu(v) for k, v in
self.conv1(x_dict, edge_index_dict).items()}
x_dict = self.conv2(x_dict, edge_index_dict)
return x_dict
def match_score(self, agent_emb, ticket_emb):
return (agent_emb * ticket_emb).sum(dim=-1)Agent embeddings encode demonstrated expertise from resolution history. Ticket embeddings encode requirements and customer context. Dot product scores the match quality.
Expected performance
- Round-robin routing: ~35 AUROC (random baseline)
- LightGBM (keyword features): 62.44 AUROC
- GNN (expertise graph): 75.83 AUROC
- KumoRFM (zero-shot): 76.71 AUROC
Or use KumoRFM in one line
PREDICT best_agent FOR ticket
USING agent, ticket, skill, customer, resolution_historyOne PQL query. KumoRFM learns agent expertise from resolution history and matches tickets to optimal agents.