Skip to main content
The triage system uses machine learning to automatically classify support tickets into categories and priority levels. This enables intelligent routing and context-aware answer generation.

Architecture

Two separate logistic regression models handle classification:
  • Category Model - Predicts support domain (Billing, Authentication, etc.)
  • Priority Model - Predicts urgency level (Low, Medium, High)
Both models use TF-IDF features extracted from ticket subject and body text.
The models are trained offline and loaded once at service initialization for fast inference.

Model Training

Training happens in src/ml/train.py using a supervised learning pipeline.

Pipeline Construction

def build_pipeline() -> Pipeline:
    """
    Construct a text classification pipeline.
    """
    return Pipeline(
        [
            ("features", build_feature_union()),
            ("clf", LogisticRegression(max_iter=500, class_weight="balanced")),
        ]
    )
The class_weight="balanced" parameter ensures the model handles class imbalance effectively.

Data Preprocessing

Text is normalized before feature extraction:
def load_dataset(
    train_csv: str,
    df_override: Optional[pd.DataFrame] = None,
) -> pd.DataFrame:
    """
    Load the training dataset from disk or use an injected DataFrame.
    """
    df = df_override.copy() if df_override is not None else pd.read_csv(train_csv)

    # Text normalization
    df["subject"] = df["subject"].apply(preprocess_text)
    df["body"] = df["body"].apply(preprocess_text)

    return df

Train/Validation Split

Stratified splitting ensures balanced representation:
def split_train_val(
    X: pd.DataFrame,
    y_category: pd.Series,
    y_priority: pd.Series,
    test_size: float = 0.2,
    random_state: int = 42,
) -> Tuple:
    """
    Perform a train/validation split with optional stratification.
    """
    stratify = y_category if y_category.value_counts().min() >= 2 else None

    if stratify is None:
        warnings.warn(
            "Dataset too small for stratified split; using non-stratified split."
        )

    return train_test_split(
        X,
        y_category,
        y_priority,
        test_size=test_size,
        random_state=random_state,
        stratify=stratify,
    )

Edge Case Handling

The training pipeline handles single-class datasets gracefully:
class ConstantPredictor:
    """
    Fallback predictor used when the training data contains only one class.
    """

    def __init__(self, label):
        self.label = label

    def predict(self, X):
        return [self.label] * len(X)

    def predict_proba(self, X):
        return np.ones((len(X), 1))


def train_or_fallback(pipeline: Pipeline, X, y):
    """
    Train a pipeline or fall back to a constant predictor if only one class exists.
    """
    if y.nunique() >= 2:
        pipeline.fit(X, y)
        return pipeline

    return ConstantPredictor(y.iloc[0])
Constant predictors are used automatically when training data lacks class diversity.

Inference

The TriageModel class handles runtime predictions:
class TriageModel:
    """
    ML model for triaging support tickets:
      - predicts category and priority
      - returns confidence scores
    """

    def __init__(
        self,
        category_model_path: str = "artifacts/category_model.joblib",
        priority_model_path: str = "artifacts/priority_model.joblib",
    ):
        """
        Load pre-trained ML models from disk.
        """
        self.category_model_path = Path(category_model_path)
        self.priority_model_path = Path(priority_model_path)

        # Load models once during initialization
        self.category_model = self._load_model(self.category_model_path)
        self.priority_model = self._load_model(self.priority_model_path)

    @staticmethod
    def _load_model(path: Path):
        if not path.exists():
            raise FileNotFoundError(f"ML model not found: {path}")
        return joblib.load(path)

Prediction with Confidence

def predict(self, subject: str, body: str) -> Dict:
    """
    Predict category and priority from ticket subject and body.

    Returns:
        Dict with:
            - category: predicted category
            - priority: predicted priority
            - confidence: dict with category & priority probabilities
    """
    # Preprocess inputs
    subject_clean = preprocess_text(subject)
    body_clean = preprocess_text(body)

    X = pd.DataFrame([{"subject": subject_clean, "body": body_clean}])

    # Predict labels
    category = self.category_model.predict(X)[0]
    priority = self.priority_model.predict(X)[0]

    # Predict confidence scores
    cat_conf = max(self.category_model.predict_proba(X)[0])
    pri_conf = max(self.priority_model.predict_proba(X)[0])

    return {
        "category": category,
        "priority": priority,
        "confidence": {"category": float(cat_conf), "priority": float(pri_conf)},
    }
Confidence scores are derived from the maximum probability across all classes.

Evaluation Metrics

The training script computes standard classification metrics:
def compute_metrics(
    y_true_cat,
    y_pred_cat,
    y_true_pri,
    y_pred_pri,
) -> Dict[str, float]:
    """
    Compute validation metrics for both tasks.
    """
    return {
        "category_macro_f1": float(f1_score(y_true_cat, y_pred_cat, average="macro")),
        "priority_f1": float(f1_score(y_true_pri, y_pred_pri, average="weighted")),
        "priority_recall": float(
            recall_score(y_true_pri, y_pred_pri, average="weighted")
        ),
    }

Visualization

Confusion matrices are automatically generated for both models:
def plot_confusion_matrix(
    y_true,
    y_pred,
    labels,
    title: str,
    cmap: str,
    save_path: str,
):
    """
    Plot and save a confusion matrix.
    """
    cm = confusion_matrix(y_true, y_pred, labels=labels)

    plt.figure(figsize=(8, 6))
    sns.heatmap(
        cm,
        annot=True,
        fmt="d",
        cmap=cmap,
        xticklabels=labels,
        yticklabels=labels,
    )
    plt.xlabel("Predicted")
    plt.ylabel("True")
    plt.title(title)
    plt.savefig(save_path, bbox_inches="tight")
    plt.close()

Model Artifacts

Trained models are saved to artifacts/ as .joblib files for fast loading.

Confidence Thresholds

The RAG pipeline uses confidence scores to flag uncertain predictions:
CATEGORY_CONF_THRESHOLD = 0.5
PRIORITY_CONF_THRESHOLD = 0.5

needs_human_review = (
    confidence.get("category", 0) < CATEGORY_CONF_THRESHOLD
    or confidence.get("priority", 0) < PRIORITY_CONF_THRESHOLD
)
Tickets with low confidence scores are automatically flagged for human review.

RAG Pipeline

See how triage predictions guide retrieval

Structured Outputs

Understand review flags and next steps

Build docs developers (and LLMs) love