Systems Library / Customer Service / How to Build a Customer Sentiment Analysis for Tickets
Customer Service ticket management

How to Build a Customer Sentiment Analysis for Tickets

Analyze ticket sentiment to prioritize frustrated customers automatically.

Jay Banlasan

Jay Banlasan

The AI Systems Guy

Customer sentiment analysis on support tickets using ai catches frustrated customers before they churn. I build these as a layer on top of the ticket classification system. Every incoming message gets a sentiment score, and anything trending negative gets flagged for immediate attention.

The real value is tracking sentiment across a conversation, not just one message. A customer who starts calm and gets angrier with each reply is a different problem than someone who opens angry but calms down.

What You Need Before Starting

Step 1: Score Individual Messages

import anthropic
import json

client = anthropic.Anthropic()

def analyze_sentiment(text):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=100,
        messages=[{
            "role": "user",
            "content": f"""Analyze the sentiment of this customer support message.

Message: {text}

Respond with ONLY this JSON:
{{"score": 0.0, "label": "neutral", "indicators": ["specific phrases that indicate sentiment"]}}

Score range: -1.0 (very negative) to 1.0 (very positive).
Labels: very_negative, negative, neutral, positive, very_positive"""
        }]
    )
    return json.loads(response.content[0].text)

Step 2: Track Sentiment Over a Conversation

import sqlite3

def track_conversation_sentiment(ticket_id, message_text, message_number):
    sentiment = analyze_sentiment(message_text)

    conn = sqlite3.connect("sentiment.db")
    conn.execute("""
        INSERT INTO sentiment_log (ticket_id, message_number, score, label, indicators, analyzed_at)
        VALUES (?, ?, ?, ?, ?, datetime('now'))
    """, (ticket_id, message_number, sentiment["score"], sentiment["label"], json.dumps(sentiment["indicators"])))
    conn.commit()

    return sentiment

Step 3: Detect Sentiment Trends

The trend matters more than any single score:

def get_sentiment_trend(ticket_id):
    conn = sqlite3.connect("sentiment.db")
    scores = conn.execute(
        "SELECT score FROM sentiment_log WHERE ticket_id = ? ORDER BY message_number",
        (ticket_id,)
    ).fetchall()

    if len(scores) < 2:
        return {"trend": "stable", "direction": 0}

    recent = [s[0] for s in scores[-3:]]
    earlier = [s[0] for s in scores[:3]]

    avg_recent = sum(recent) / len(recent)
    avg_earlier = sum(earlier) / len(earlier)
    direction = avg_recent - avg_earlier

    if direction < -0.3:
        trend = "declining"
    elif direction > 0.3:
        trend = "improving"
    else:
        trend = "stable"

    return {"trend": trend, "direction": round(direction, 2), "current_avg": round(avg_recent, 2)}

Step 4: Trigger Escalation on Negative Trends

def check_escalation_needed(ticket_id):
    trend = get_sentiment_trend(ticket_id)

    if trend["trend"] == "declining" and trend["current_avg"] < -0.5:
        escalate_ticket(ticket_id, reason="sentiment_declining")
        return True

    conn = sqlite3.connect("sentiment.db")
    last_score = conn.execute(
        "SELECT score FROM sentiment_log WHERE ticket_id = ? ORDER BY message_number DESC LIMIT 1",
        (ticket_id,)
    ).fetchone()

    if last_score and last_score[0] < -0.7:
        escalate_ticket(ticket_id, reason="very_negative_message")
        return True

    return False

Step 5: Build a Sentiment Dashboard

Aggregate sentiment data for team-level insights:

def get_sentiment_dashboard(days=7):
    conn = sqlite3.connect("sentiment.db")
    from datetime import datetime, timedelta
    start = (datetime.now() - timedelta(days=days)).isoformat()

    avg = conn.execute(
        "SELECT AVG(score) FROM sentiment_log WHERE analyzed_at > ?", (start,)
    ).fetchone()[0]

    negative_count = conn.execute(
        "SELECT COUNT(DISTINCT ticket_id) FROM sentiment_log WHERE score < -0.5 AND analyzed_at > ?", (start,)
    ).fetchone()[0]

    declining = conn.execute("""
        SELECT ticket_id FROM sentiment_log WHERE analyzed_at > ?
        GROUP BY ticket_id HAVING COUNT(*) > 2
        AND AVG(CASE WHEN message_number > 2 THEN score END) < AVG(CASE WHEN message_number <= 2 THEN score END) - 0.3
    """, (start,)).fetchall()

    return {
        "avg_sentiment": round(avg or 0, 2),
        "negative_tickets": negative_count,
        "declining_conversations": len(declining)
    }

What to Build Next

Correlate sentiment data with resolution outcomes. Find out which agent responses improve sentiment and which make it worse. That data becomes training material for your support team.

Related Reading

Want this system built for your business?

Get a free assessment. We will map every system your business needs and show you the ROI.

Get Your Free Assessment

Related Systems