Skip to content
Learn Agentic AI14 min read0 views

Capstone: Building an AI-Powered Help Desk with Ticket Management and Escalation

Build a complete help desk system with AI ticket classification, automatic agent assignment, SLA tracking, escalation workflows, and a reporting dashboard for support team performance.

Help Desk Architecture

A modern AI-powered help desk goes beyond simple ticket tracking. It classifies incoming tickets by category and priority, suggests solutions from historical data, assigns tickets to the right team member, enforces SLA deadlines, and escalates automatically when SLAs are about to breach. This capstone builds all of these capabilities into a single, deployable system.

The system has six components: ticket ingestion (email, web form, API), AI classification (category, priority, and suggested resolution), assignment engine (skill-based routing to agents), SLA tracker (deadline enforcement with escalation), resolution workflow (agent workspace with AI-suggested responses), and reporting dashboard (team performance and SLA compliance metrics).

Data Model

# models.py
from sqlalchemy import Column, String, Text, Integer, Float, DateTime, ForeignKey, Enum
from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY
import uuid, enum

class Priority(str, enum.Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    URGENT = "urgent"

class TicketStatus(str, enum.Enum):
    NEW = "new"
    ASSIGNED = "assigned"
    IN_PROGRESS = "in_progress"
    WAITING_CUSTOMER = "waiting_customer"
    RESOLVED = "resolved"
    CLOSED = "closed"
    ESCALATED = "escalated"

class SupportAgent(Base):
    __tablename__ = "support_agents"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name = Column(String(200))
    email = Column(String(255), unique=True)
    skills = Column(ARRAY(String))  # ["billing", "technical", "account"]
    max_tickets = Column(Integer, default=10)
    is_available = Column(String(10), default="true")

class SupportTicket(Base):
    __tablename__ = "support_tickets"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    subject = Column(String(500))
    description = Column(Text)
    customer_email = Column(String(255), index=True)
    category = Column(String(100))  # billing, technical, account, feature_request
    priority = Column(Enum(Priority), default=Priority.MEDIUM)
    status = Column(Enum(TicketStatus), default=TicketStatus.NEW)
    assigned_to = Column(UUID(as_uuid=True), ForeignKey("support_agents.id"), nullable=True)
    sla_deadline = Column(DateTime, nullable=True)
    escalation_level = Column(Integer, default=0)
    ai_suggested_response = Column(Text, nullable=True)
    source = Column(String(50))  # "email", "web", "api"
    tags = Column(ARRAY(String), default=[])
    created_at = Column(DateTime, server_default="now()")
    resolved_at = Column(DateTime, nullable=True)

class TicketComment(Base):
    __tablename__ = "ticket_comments"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    ticket_id = Column(UUID(as_uuid=True), ForeignKey("support_tickets.id"))
    author_type = Column(String(20))  # "customer", "agent", "system"
    author_email = Column(String(255))
    content = Column(Text)
    is_internal = Column(String(10), default="false")  # internal notes
    created_at = Column(DateTime, server_default="now()")

class SLAPolicy(Base):
    __tablename__ = "sla_policies"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    priority = Column(Enum(Priority), unique=True)
    first_response_minutes = Column(Integer)
    resolution_minutes = Column(Integer)
    escalation_after_minutes = Column(Integer)

AI Ticket Classification

When a ticket arrives, classify it by category and priority, and generate a suggested response.

# services/classifier.py
import openai, json

SLA_DEFAULTS = {
    Priority.URGENT: {"response": 30, "resolution": 240},
    Priority.HIGH: {"response": 60, "resolution": 480},
    Priority.MEDIUM: {"response": 240, "resolution": 1440},
    Priority.LOW: {"response": 480, "resolution": 2880},
}

async def classify_ticket(ticket_id: str, db):
    ticket = db.query(SupportTicket).get(ticket_id)

    # Search for similar resolved tickets
    similar = await find_similar_tickets(ticket.description, db, limit=3)
    similar_context = "\n".join(
        [f"[{t.category}] {t.subject}: {t.ai_suggested_response}" for t in similar]
    )

    response = openai.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": f"""Classify this support ticket.

Similar resolved tickets for context:
{similar_context}

Return JSON with:
- category: one of [billing, technical, account, feature_request, bug_report]
- priority: one of [low, medium, high, urgent]
- tags: list of relevant tags
- suggested_response: a draft response the agent can send
- confidence: 0-1"""},
            {"role": "user", "content": f"Subject: {ticket.subject}\n\n{ticket.description}"},
        ],
        response_format={"type": "json_object"},
    )

    result = json.loads(response.choices[0].message.content)

    ticket.category = result["category"]
    ticket.priority = Priority(result["priority"])
    ticket.tags = result.get("tags", [])
    ticket.ai_suggested_response = result.get("suggested_response")

    # Set SLA deadline
    sla = SLA_DEFAULTS[ticket.priority]
    ticket.sla_deadline = datetime.utcnow() + timedelta(minutes=sla["resolution"])

    db.commit()
    return result

Skill-Based Assignment Engine

Assign tickets to the agent best suited for the category, with the lowest current workload.

# services/assignment.py
from sqlalchemy import func

async def assign_ticket(ticket_id: str, db):
    ticket = db.query(SupportTicket).get(ticket_id)

    # Map categories to required skills
    skill_map = {
        "billing": "billing",
        "technical": "technical",
        "account": "account",
        "bug_report": "technical",
        "feature_request": "account",
    }
    required_skill = skill_map.get(ticket.category, "general")

    # Find available agents with the required skill and lowest workload
    agents = db.query(
        SupportAgent,
        func.count(SupportTicket.id).label("current_tickets"),
    ).outerjoin(
        SupportTicket,
        (SupportTicket.assigned_to == SupportAgent.id) &
        (SupportTicket.status.in_([TicketStatus.ASSIGNED, TicketStatus.IN_PROGRESS]))
    ).filter(
        SupportAgent.skills.contains([required_skill]),
        SupportAgent.is_available == "true",
    ).group_by(SupportAgent.id).having(
        func.count(SupportTicket.id) < SupportAgent.max_tickets
    ).order_by("current_tickets").all()

    if agents:
        best_agent = agents[0][0]
        ticket.assigned_to = best_agent.id
        ticket.status = TicketStatus.ASSIGNED
        db.commit()
        await notify_agent(best_agent.email, ticket)
        return best_agent
    else:
        # No available agents — auto-escalate
        ticket.escalation_level = 1
        ticket.status = TicketStatus.ESCALATED
        db.commit()
        await notify_managers(ticket)
        return None

SLA Monitoring and Escalation

A background task checks for SLA breaches and escalates tickets automatically.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

# services/sla_monitor.py
from datetime import datetime, timedelta

async def check_sla_compliance():
    """Run every 5 minutes to check for SLA breaches."""
    now = datetime.utcnow()

    # Find tickets approaching or past SLA deadline
    at_risk = db.query(SupportTicket).filter(
        SupportTicket.status.in_([
            TicketStatus.NEW, TicketStatus.ASSIGNED, TicketStatus.IN_PROGRESS
        ]),
        SupportTicket.sla_deadline.isnot(None),
        SupportTicket.sla_deadline <= now + timedelta(minutes=30),
    ).all()

    for ticket in at_risk:
        minutes_remaining = (ticket.sla_deadline - now).total_seconds() / 60

        if minutes_remaining <= 0:
            # SLA breached
            ticket.escalation_level = max(ticket.escalation_level, 2)
            ticket.status = TicketStatus.ESCALATED
            await notify_managers(ticket, breach=True)
            add_system_comment(ticket.id, "SLA BREACHED. Auto-escalated to management.")

        elif minutes_remaining <= 30 and ticket.escalation_level == 0:
            # SLA at risk — first escalation
            ticket.escalation_level = 1
            await notify_agent_urgent(ticket)
            add_system_comment(
                ticket.id,
                f"SLA at risk. {int(minutes_remaining)} minutes remaining."
            )

    db.commit()

Ticket CRUD API

# routes/tickets.py
from fastapi import APIRouter

router = APIRouter(prefix="/tickets")

@router.post("/")
async def create_ticket(body: TicketCreate, db=Depends(get_db)):
    ticket = SupportTicket(
        subject=body.subject,
        description=body.description,
        customer_email=body.customer_email,
        source=body.source,
    )
    db.add(ticket)
    db.commit()

    # Async classification and assignment
    classification = await classify_ticket(str(ticket.id), db)
    agent = await assign_ticket(str(ticket.id), db)

    db.refresh(ticket)
    return {"ticket": ticket, "classification": classification}

@router.get("/{ticket_id}")
async def get_ticket(ticket_id: str, db=Depends(get_db)):
    ticket = db.query(SupportTicket).get(ticket_id)
    comments = db.query(TicketComment).filter(
        TicketComment.ticket_id == ticket_id
    ).order_by(TicketComment.created_at).all()
    return {"ticket": ticket, "comments": comments}

@router.patch("/{ticket_id}/resolve")
async def resolve_ticket(ticket_id: str, body: ResolveRequest, db=Depends(get_db)):
    ticket = db.query(SupportTicket).get(ticket_id)
    ticket.status = TicketStatus.RESOLVED
    ticket.resolved_at = datetime.utcnow()
    add_system_comment(ticket_id, f"Resolved by {body.agent_email}: {body.resolution_note}")
    db.commit()
    return {"status": "resolved"}

Reporting Dashboard API

# routes/reports.py
@router.get("/reports/overview")
async def reports_overview(days: int = 30, db=Depends(get_db)):
    since = datetime.utcnow() - timedelta(days=days)
    tickets = db.query(SupportTicket).filter(
        SupportTicket.created_at >= since
    ).all()

    resolved = [t for t in tickets if t.resolved_at]
    breached = [t for t in tickets if t.escalation_level >= 2]

    avg_resolution = None
    if resolved:
        deltas = [(t.resolved_at - t.created_at).total_seconds() / 3600 for t in resolved]
        avg_resolution = sum(deltas) / len(deltas)

    return {
        "total_tickets": len(tickets),
        "resolved": len(resolved),
        "open": len(tickets) - len(resolved),
        "sla_breach_count": len(breached),
        "sla_compliance_pct": round(
            (1 - len(breached) / max(len(tickets), 1)) * 100, 1
        ),
        "avg_resolution_hours": round(avg_resolution, 1) if avg_resolution else None,
        "by_category": count_by_field(tickets, "category"),
        "by_priority": count_by_field(tickets, "priority"),
    }

The complete help desk system demonstrates end-to-end AI integration in a business-critical application: from automatic classification and assignment through SLA enforcement to executive reporting. Each component is independently deployable and testable, and the architecture supports scaling by adding more support agents and increasing the background task frequency.

FAQ

How do I handle tickets that arrive via email?

Set up an inbound email webhook using SendGrid or Mailgun. When an email arrives at support@yourdomain.com, the webhook sends the sender, subject, and body to your /tickets endpoint. Parse the email body to extract the description, use the sender address as customer_email, and set the source to "email". Reply notifications are sent back via the same email service.

How do I prevent the AI from misclassifying urgent tickets as low priority?

Use keyword-based priority overrides as a safety net. If the ticket contains phrases like "system down", "data loss", "cannot login", or "security breach", force the priority to URGENT regardless of the AI classification. Log every override so you can tune the classifier to handle these cases natively over time.

How do I measure individual agent performance fairly?

Track metrics that the agent can control: average first response time, customer satisfaction rating, and resolution rate. Do not penalize agents for SLA breaches caused by assignment delays or ticket volume spikes. Compare each agent's metrics against tickets of similar category and priority to normalize for workload difficulty.


#CapstoneProject #HelpDesk #TicketManagement #SLATracking #Escalation #FullStackAI #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.