Spaces:
Sleeping
Sleeping
Deploy to Hugging Face Spaces: Add application files and dependencies
Browse files- Add FastAPI application (app.py)
- Add preprocessing module (preprocessing.py)
- Add model utilities (model_utils.py)
- Add Dockerfile for HF Spaces deployment
- Add requirements.txt
- Add deployment documentation
- Add configuration files
- GIT_COMMANDS.md +193 -0
- SETUP.md +128 -0
- app.py +4 -4
- deploy.ps1 +93 -0
- deploy.sh +96 -0
- docker-compose.yml +30 -0
- n8n_workflow_examples.md +336 -0
- n8n_workflow_job_monitoring.json +224 -0
- requirements.txt +11 -12
- test_api.py +79 -0
- train_anomaly.py +183 -0
- train_job_failure.py +257 -0
GIT_COMMANDS.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# คำสั่ง Git สำหรับ Deploy ไปยัง Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
## วิธีที่ 1: ใช้ Script (แนะนำ)
|
| 4 |
+
|
| 5 |
+
### Windows (PowerShell)
|
| 6 |
+
```powershell
|
| 7 |
+
.\deploy.ps1
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
### Linux/Mac (Bash)
|
| 11 |
+
```bash
|
| 12 |
+
chmod +x deploy.sh
|
| 13 |
+
./deploy.sh
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## วิธีที่ 2: รันคำสั่ง Git แบบ Manual
|
| 19 |
+
|
| 20 |
+
### Step 1: ลบ Git Lock File (ถ้ามี)
|
| 21 |
+
```powershell
|
| 22 |
+
# PowerShell
|
| 23 |
+
Remove-Item .git\index.lock -Force -ErrorAction SilentlyContinue
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
# Bash
|
| 28 |
+
rm -f .git/index.lock
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Step 2: Add ไฟล์ Core Application
|
| 32 |
+
```bash
|
| 33 |
+
git add app.py
|
| 34 |
+
git add preprocessing.py
|
| 35 |
+
git add model_utils.py
|
| 36 |
+
git add requirements.txt
|
| 37 |
+
git add Dockerfile
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### Step 3: Add Configuration Files
|
| 41 |
+
```bash
|
| 42 |
+
git add .dockerignore
|
| 43 |
+
git add .gitignore
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Step 4: Add Documentation
|
| 47 |
+
```bash
|
| 48 |
+
git add README_HF_SPACE.md
|
| 49 |
+
git add DEPLOYMENT.md
|
| 50 |
+
git add DEPLOYMENT_SUMMARY.md
|
| 51 |
+
git add README.md
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### Step 5: Add Models Directory (ถ้าต้องการ)
|
| 55 |
+
```bash
|
| 56 |
+
# ถ้า models/ ไม่อยู่ใน .gitignore
|
| 57 |
+
git add models/
|
| 58 |
+
|
| 59 |
+
# ถ้า models/ อยู่ใน .gitignore แต่ต้องการ commit
|
| 60 |
+
git add -f models/
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### Step 6: ตรวจสอบ Files ที่จะ Commit
|
| 64 |
+
```bash
|
| 65 |
+
git status --short
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Step 7: Commit
|
| 69 |
+
```bash
|
| 70 |
+
git commit -m "Deploy to Hugging Face Spaces: Add application files and dependencies
|
| 71 |
+
|
| 72 |
+
- Add FastAPI application (app.py)
|
| 73 |
+
- Add preprocessing module (preprocessing.py)
|
| 74 |
+
- Add model utilities (model_utils.py)
|
| 75 |
+
- Add Dockerfile for HF Spaces deployment
|
| 76 |
+
- Add requirements.txt
|
| 77 |
+
- Add deployment documentation
|
| 78 |
+
- Add configuration files"
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### Step 8: Push
|
| 82 |
+
```bash
|
| 83 |
+
git push
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
หรือ
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
git push origin main
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## วิธีที่ 3: คำสั่งเดียว (Quick)
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
# Add ไฟล์ทั้งหมดที่จำเป็น
|
| 98 |
+
git add app.py preprocessing.py model_utils.py requirements.txt Dockerfile .dockerignore .gitignore README_HF_SPACE.md DEPLOYMENT.md DEPLOYMENT_SUMMARY.md README.md
|
| 99 |
+
|
| 100 |
+
# Add models (ถ้าต้องการ)
|
| 101 |
+
git add models/ || git add -f models/
|
| 102 |
+
|
| 103 |
+
# Commit
|
| 104 |
+
git commit -m "Deploy to Hugging Face Spaces: Add application files and dependencies"
|
| 105 |
+
|
| 106 |
+
# Push
|
| 107 |
+
git push
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
## ไฟล์ที่ต้อง Commit
|
| 113 |
+
|
| 114 |
+
### ✅ ไฟล์ที่จำเป็น (Required)
|
| 115 |
+
- `app.py` - FastAPI application
|
| 116 |
+
- `preprocessing.py` - Preprocessing module
|
| 117 |
+
- `model_utils.py` - Model utilities
|
| 118 |
+
- `requirements.txt` - Python dependencies
|
| 119 |
+
- `Dockerfile` - Docker configuration
|
| 120 |
+
|
| 121 |
+
### ✅ ไฟล์ Configuration
|
| 122 |
+
- `.dockerignore` - Docker ignore rules
|
| 123 |
+
- `.gitignore` - Git ignore rules
|
| 124 |
+
|
| 125 |
+
### ✅ ไฟล์ Documentation
|
| 126 |
+
- `README_HF_SPACE.md` - README for HF Space
|
| 127 |
+
- `DEPLOYMENT.md` - Deployment guide
|
| 128 |
+
- `DEPLOYMENT_SUMMARY.md` - Deployment summary
|
| 129 |
+
- `README.md` - Main README
|
| 130 |
+
|
| 131 |
+
### ⚠️ Models Directory
|
| 132 |
+
- `models/` - Model files (ต้อง commit ถ้าต้องการใช้ local models)
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## Troubleshooting
|
| 137 |
+
|
| 138 |
+
### ปัญหา: Git Lock File
|
| 139 |
+
```bash
|
| 140 |
+
# ลบ lock file
|
| 141 |
+
Remove-Item .git\index.lock -Force # PowerShell
|
| 142 |
+
rm -f .git/index.lock # Bash
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
### ปัญหา: Authentication Required
|
| 146 |
+
```bash
|
| 147 |
+
# ตั้งค่า credential
|
| 148 |
+
git config --global credential.helper store
|
| 149 |
+
|
| 150 |
+
# หรือใช้ token
|
| 151 |
+
git remote set-url origin https://[token]@huggingface.co/spaces/[username]/[space-name]
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### ปัญหา: Models ไม่ถูก Commit
|
| 155 |
+
```bash
|
| 156 |
+
# Force add models directory
|
| 157 |
+
git add -f models/
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### ปัญหา: Remote URL ไม่ถูกต้อง
|
| 161 |
+
```bash
|
| 162 |
+
# ตรวจสอบ remote URL
|
| 163 |
+
git remote -v
|
| 164 |
+
|
| 165 |
+
# ตั้งค่า remote URL (ถ้าจำเป็น)
|
| 166 |
+
git remote set-url origin https://huggingface.co/spaces/[username]/[space-name]
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## Checklist
|
| 172 |
+
|
| 173 |
+
- [ ] ลบ git lock file (ถ้ามี)
|
| 174 |
+
- [ ] Add ไฟล์ core application
|
| 175 |
+
- [ ] Add configuration files
|
| 176 |
+
- [ ] Add documentation files
|
| 177 |
+
- [ ] Add models directory (ถ้าต้องการ)
|
| 178 |
+
- [ ] ตรวจสอบ git status
|
| 179 |
+
- [ ] Commit พร้อม message
|
| 180 |
+
- [ ] Push ไปยัง remote
|
| 181 |
+
- [ ] ตรวจสอบ build logs ใน Hugging Face Spaces
|
| 182 |
+
|
| 183 |
+
---
|
| 184 |
+
|
| 185 |
+
## หมายเหตุ
|
| 186 |
+
|
| 187 |
+
1. **Models Directory**: ถ้า `models/` อยู่ใน `.gitignore` ต้องใช้ `git add -f models/` เพื่อ force add
|
| 188 |
+
|
| 189 |
+
2. **Authentication**: Hugging Face Spaces ต้องใช้ token หรือ credential สำหรับ push
|
| 190 |
+
|
| 191 |
+
3. **Build Time**: หลัง push แล้ว Hugging Face Spaces จะ rebuild อัตโนมัติ (~5-15 นาที)
|
| 192 |
+
|
| 193 |
+
4. **File Size**: ตรวจสอบขนาด models ถ้าใหญ่เกิน 1GB อาจต้องใช้ Git LFS
|
SETUP.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Setup Instructions
|
| 2 |
+
|
| 3 |
+
## 1. Install Dependencies
|
| 4 |
+
|
| 5 |
+
### Option A: Using pip (recommended)
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
pip install -r requirements.txt
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
### Option B: Using virtual environment (recommended for isolation)
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
# Create virtual environment
|
| 15 |
+
python -m venv venv
|
| 16 |
+
|
| 17 |
+
# Activate virtual environment
|
| 18 |
+
# On Windows (PowerShell):
|
| 19 |
+
.\venv\Scripts\Activate.ps1
|
| 20 |
+
# On Windows (CMD):
|
| 21 |
+
venv\Scripts\activate.bat
|
| 22 |
+
# On Linux/Mac:
|
| 23 |
+
source venv/bin/activate
|
| 24 |
+
|
| 25 |
+
# Install dependencies
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## 2. Prepare Data
|
| 30 |
+
|
| 31 |
+
Place your CSV files in the `data/` directory:
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
mkdir -p data
|
| 35 |
+
# Copy your CSV files to data/
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## 3. Train Models
|
| 39 |
+
|
| 40 |
+
### Train Job Failure Prediction Model
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
# Option 1: Use all CSV files in data/ directory (recommended)
|
| 44 |
+
python train_job_failure.py
|
| 45 |
+
|
| 46 |
+
# Option 2: Specify files explicitly
|
| 47 |
+
python train_job_failure.py data/true_export_report_20260120.csv data/true_export_report_20260121.csv
|
| 48 |
+
|
| 49 |
+
# Option 3: Use glob pattern (works in PowerShell and bash)
|
| 50 |
+
python train_job_failure.py data/*.csv
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Train Anomaly Detection Model
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
# Option 1: Use all CSV files in data/ directory (recommended)
|
| 57 |
+
python train_anomaly.py
|
| 58 |
+
|
| 59 |
+
# Option 2: Specify files explicitly
|
| 60 |
+
python train_anomaly.py data/true_export_report_20260120.csv data/true_export_report_20260121.csv
|
| 61 |
+
|
| 62 |
+
# Option 3: Use glob pattern
|
| 63 |
+
python train_anomaly.py data/*.csv
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## 4. Verify Models
|
| 67 |
+
|
| 68 |
+
After training, check that models were created:
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
ls models/
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
You should see:
|
| 75 |
+
- `job_fail_pipeline_cpu.joblib`
|
| 76 |
+
- `anomaly_autoencoder_cpu.keras`
|
| 77 |
+
- `anomaly_scaler.joblib`
|
| 78 |
+
- `feature_schema.json`
|
| 79 |
+
- `shap_background.npy`
|
| 80 |
+
- `anomaly_features.joblib`
|
| 81 |
+
- `anomaly_threshold.joblib`
|
| 82 |
+
|
| 83 |
+
## 5. Run the Service
|
| 84 |
+
|
| 85 |
+
### Local Development
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
uvicorn app:app --host 0.0.0.0 --port 8000
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Docker
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
docker-compose up --build
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## Troubleshooting
|
| 98 |
+
|
| 99 |
+
### "ModuleNotFoundError: No module named 'tensorflow'"
|
| 100 |
+
|
| 101 |
+
Install dependencies:
|
| 102 |
+
```bash
|
| 103 |
+
pip install -r requirements.txt
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### "No data files found!"
|
| 107 |
+
|
| 108 |
+
1. Check that CSV files exist in `data/` directory:
|
| 109 |
+
```bash
|
| 110 |
+
ls data/
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
2. Use explicit file paths:
|
| 114 |
+
```bash
|
| 115 |
+
python train_job_failure.py data/true_export_report_20260120.csv
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### PowerShell glob pattern not working
|
| 119 |
+
|
| 120 |
+
The scripts now handle glob patterns internally. You can use:
|
| 121 |
+
```powershell
|
| 122 |
+
python train_job_failure.py data/*.csv
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
Or just run without arguments to use all CSV files in `data/`:
|
| 126 |
+
```powershell
|
| 127 |
+
python train_job_failure.py
|
| 128 |
+
```
|
app.py
CHANGED
|
@@ -4,7 +4,7 @@ FastAPI application for Job Failure Prediction and Anomaly Detection.
|
|
| 4 |
from fastapi import FastAPI, HTTPException
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
from pydantic import BaseModel, Field
|
| 7 |
-
from typing import Optional, Dict, List
|
| 8 |
import pandas as pd
|
| 9 |
from datetime import datetime
|
| 10 |
import os
|
|
@@ -75,7 +75,7 @@ class JobFailureResponse(BaseModel):
|
|
| 75 |
"""Response model for job failure prediction."""
|
| 76 |
fail_probability: float
|
| 77 |
risk_level: str
|
| 78 |
-
top_drivers: Optional[List[Dict[str,
|
| 79 |
recommended_actions: Optional[List[str]] = None
|
| 80 |
error: Optional[str] = None
|
| 81 |
|
|
@@ -96,7 +96,7 @@ class AnomalyResponse(BaseModel):
|
|
| 96 |
reconstruction_error: float
|
| 97 |
is_anomaly: bool
|
| 98 |
threshold: float
|
| 99 |
-
top_drivers: List[Dict[str,
|
| 100 |
error: Optional[str] = None
|
| 101 |
|
| 102 |
|
|
@@ -188,4 +188,4 @@ async def root():
|
|
| 188 |
|
| 189 |
if __name__ == "__main__":
|
| 190 |
import uvicorn
|
| 191 |
-
uvicorn.run(app, host="0.0.0.0", port=
|
|
|
|
| 4 |
from fastapi import FastAPI, HTTPException
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
from pydantic import BaseModel, Field
|
| 7 |
+
from typing import Optional, Dict, List, Any
|
| 8 |
import pandas as pd
|
| 9 |
from datetime import datetime
|
| 10 |
import os
|
|
|
|
| 75 |
"""Response model for job failure prediction."""
|
| 76 |
fail_probability: float
|
| 77 |
risk_level: str
|
| 78 |
+
top_drivers: Optional[List[Dict[str, Any]]] = None
|
| 79 |
recommended_actions: Optional[List[str]] = None
|
| 80 |
error: Optional[str] = None
|
| 81 |
|
|
|
|
| 96 |
reconstruction_error: float
|
| 97 |
is_anomaly: bool
|
| 98 |
threshold: float
|
| 99 |
+
top_drivers: List[Dict[str, Any]]
|
| 100 |
error: Optional[str] = None
|
| 101 |
|
| 102 |
|
|
|
|
| 188 |
|
| 189 |
if __name__ == "__main__":
|
| 190 |
import uvicorn
|
| 191 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
deploy.ps1
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git deployment script for Hugging Face Spaces
|
| 2 |
+
# This script adds, commits, and pushes all necessary files
|
| 3 |
+
|
| 4 |
+
Write-Host "=== Preparing files for Hugging Face Spaces deployment ===" -ForegroundColor Cyan
|
| 5 |
+
|
| 6 |
+
# Change to project directory
|
| 7 |
+
Set-Location $PSScriptRoot
|
| 8 |
+
|
| 9 |
+
# Step 1: Remove git lock file if exists (in case of previous failed operations)
|
| 10 |
+
if (Test-Path .git\index.lock) {
|
| 11 |
+
Write-Host "Removing git lock file..." -ForegroundColor Yellow
|
| 12 |
+
Remove-Item .git\index.lock -Force -ErrorAction SilentlyContinue
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Step 2: Add core application files (required for API to work)
|
| 16 |
+
Write-Host "`nAdding core application files..." -ForegroundColor Green
|
| 17 |
+
git add app.py
|
| 18 |
+
git add preprocessing.py
|
| 19 |
+
git add model_utils.py
|
| 20 |
+
git add requirements.txt
|
| 21 |
+
git add Dockerfile
|
| 22 |
+
|
| 23 |
+
# Step 3: Add configuration files
|
| 24 |
+
Write-Host "Adding configuration files..." -ForegroundColor Green
|
| 25 |
+
git add .dockerignore
|
| 26 |
+
git add .gitignore
|
| 27 |
+
|
| 28 |
+
# Step 4: Add documentation files
|
| 29 |
+
Write-Host "Adding documentation files..." -ForegroundColor Green
|
| 30 |
+
git add README_HF_SPACE.md
|
| 31 |
+
git add DEPLOYMENT.md
|
| 32 |
+
git add DEPLOYMENT_SUMMARY.md
|
| 33 |
+
git add README.md
|
| 34 |
+
|
| 35 |
+
# Step 5: Add models directory (if exists and not in .gitignore)
|
| 36 |
+
if (Test-Path models) {
|
| 37 |
+
Write-Host "Checking models directory..." -ForegroundColor Yellow
|
| 38 |
+
# Check if models/ is in .gitignore
|
| 39 |
+
$gitignoreContent = Get-Content .gitignore -ErrorAction SilentlyContinue
|
| 40 |
+
if ($gitignoreContent -notmatch "^models/") {
|
| 41 |
+
Write-Host "Adding models directory..." -ForegroundColor Green
|
| 42 |
+
git add models/
|
| 43 |
+
} else {
|
| 44 |
+
Write-Host "WARNING: models/ is in .gitignore. You may need to force add it:" -ForegroundColor Yellow
|
| 45 |
+
Write-Host " git add -f models/" -ForegroundColor Yellow
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Step 6: Show status
|
| 50 |
+
Write-Host "`n=== Files staged for commit ===" -ForegroundColor Cyan
|
| 51 |
+
git status --short
|
| 52 |
+
|
| 53 |
+
# Step 7: Commit
|
| 54 |
+
Write-Host "`n=== Committing changes ===" -ForegroundColor Cyan
|
| 55 |
+
$commitMessage = "Deploy to Hugging Face Spaces: Add application files and dependencies
|
| 56 |
+
|
| 57 |
+
- Add FastAPI application (app.py)
|
| 58 |
+
- Add preprocessing module (preprocessing.py)
|
| 59 |
+
- Add model utilities (model_utils.py)
|
| 60 |
+
- Add Dockerfile for HF Spaces deployment
|
| 61 |
+
- Add requirements.txt
|
| 62 |
+
- Add deployment documentation
|
| 63 |
+
- Add configuration files"
|
| 64 |
+
|
| 65 |
+
git commit -m $commitMessage
|
| 66 |
+
|
| 67 |
+
if ($LASTEXITCODE -eq 0) {
|
| 68 |
+
Write-Host "`n=== Commit successful! ===" -ForegroundColor Green
|
| 69 |
+
|
| 70 |
+
# Step 8: Push
|
| 71 |
+
Write-Host "`n=== Pushing to remote ===" -ForegroundColor Cyan
|
| 72 |
+
Write-Host "Note: You may need to authenticate for push" -ForegroundColor Yellow
|
| 73 |
+
git push
|
| 74 |
+
|
| 75 |
+
if ($LASTEXITCODE -eq 0) {
|
| 76 |
+
Write-Host "`n=== Deployment files pushed successfully! ===" -ForegroundColor Green
|
| 77 |
+
Write-Host "Next steps:" -ForegroundColor Cyan
|
| 78 |
+
Write-Host "1. Go to your Hugging Face Space" -ForegroundColor White
|
| 79 |
+
Write-Host "2. Wait for automatic rebuild" -ForegroundColor White
|
| 80 |
+
Write-Host "3. Check logs if build fails" -ForegroundColor White
|
| 81 |
+
} else {
|
| 82 |
+
Write-Host "`n=== Push failed ===" -ForegroundColor Red
|
| 83 |
+
Write-Host "You may need to:" -ForegroundColor Yellow
|
| 84 |
+
Write-Host "1. Set up authentication (git credential or token)" -ForegroundColor White
|
| 85 |
+
Write-Host "2. Check remote URL: git remote -v" -ForegroundColor White
|
| 86 |
+
Write-Host "3. Try: git push origin main" -ForegroundColor White
|
| 87 |
+
}
|
| 88 |
+
} else {
|
| 89 |
+
Write-Host "`n=== Commit failed ===" -ForegroundColor Red
|
| 90 |
+
Write-Host "Check git status and resolve any conflicts" -ForegroundColor Yellow
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
Write-Host "`n=== Script completed ===" -ForegroundColor Cyan
|
deploy.sh
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Git deployment script for Hugging Face Spaces
|
| 3 |
+
# This script adds, commits, and pushes all necessary files
|
| 4 |
+
|
| 5 |
+
echo "=== Preparing files for Hugging Face Spaces deployment ==="
|
| 6 |
+
|
| 7 |
+
# Step 1: Remove git lock file if exists
|
| 8 |
+
if [ -f .git/index.lock ]; then
|
| 9 |
+
echo "Removing git lock file..."
|
| 10 |
+
rm -f .git/index.lock
|
| 11 |
+
fi
|
| 12 |
+
|
| 13 |
+
# Step 2: Add core application files (required for API to work)
|
| 14 |
+
echo ""
|
| 15 |
+
echo "Adding core application files..."
|
| 16 |
+
git add app.py
|
| 17 |
+
git add preprocessing.py
|
| 18 |
+
git add model_utils.py
|
| 19 |
+
git add requirements.txt
|
| 20 |
+
git add Dockerfile
|
| 21 |
+
|
| 22 |
+
# Step 3: Add configuration files
|
| 23 |
+
echo "Adding configuration files..."
|
| 24 |
+
git add .dockerignore
|
| 25 |
+
git add .gitignore
|
| 26 |
+
|
| 27 |
+
# Step 4: Add documentation files
|
| 28 |
+
echo "Adding documentation files..."
|
| 29 |
+
git add README_HF_SPACE.md
|
| 30 |
+
git add DEPLOYMENT.md
|
| 31 |
+
git add DEPLOYMENT_SUMMARY.md
|
| 32 |
+
git add README.md
|
| 33 |
+
|
| 34 |
+
# Step 5: Add models directory (if exists and not in .gitignore)
|
| 35 |
+
if [ -d models ]; then
|
| 36 |
+
echo "Checking models directory..."
|
| 37 |
+
if ! grep -q "^models/" .gitignore 2>/dev/null; then
|
| 38 |
+
echo "Adding models directory..."
|
| 39 |
+
git add models/
|
| 40 |
+
else
|
| 41 |
+
echo "WARNING: models/ is in .gitignore. You may need to force add it:"
|
| 42 |
+
echo " git add -f models/"
|
| 43 |
+
fi
|
| 44 |
+
fi
|
| 45 |
+
|
| 46 |
+
# Step 6: Show status
|
| 47 |
+
echo ""
|
| 48 |
+
echo "=== Files staged for commit ==="
|
| 49 |
+
git status --short
|
| 50 |
+
|
| 51 |
+
# Step 7: Commit
|
| 52 |
+
echo ""
|
| 53 |
+
echo "=== Committing changes ==="
|
| 54 |
+
git commit -m "Deploy to Hugging Face Spaces: Add application files and dependencies
|
| 55 |
+
|
| 56 |
+
- Add FastAPI application (app.py)
|
| 57 |
+
- Add preprocessing module (preprocessing.py)
|
| 58 |
+
- Add model utilities (model_utils.py)
|
| 59 |
+
- Add Dockerfile for HF Spaces deployment
|
| 60 |
+
- Add requirements.txt
|
| 61 |
+
- Add deployment documentation
|
| 62 |
+
- Add configuration files"
|
| 63 |
+
|
| 64 |
+
if [ $? -eq 0 ]; then
|
| 65 |
+
echo ""
|
| 66 |
+
echo "=== Commit successful! ==="
|
| 67 |
+
|
| 68 |
+
# Step 8: Push
|
| 69 |
+
echo ""
|
| 70 |
+
echo "=== Pushing to remote ==="
|
| 71 |
+
echo "Note: You may need to authenticate for push"
|
| 72 |
+
git push
|
| 73 |
+
|
| 74 |
+
if [ $? -eq 0 ]; then
|
| 75 |
+
echo ""
|
| 76 |
+
echo "=== Deployment files pushed successfully! ==="
|
| 77 |
+
echo "Next steps:"
|
| 78 |
+
echo "1. Go to your Hugging Face Space"
|
| 79 |
+
echo "2. Wait for automatic rebuild"
|
| 80 |
+
echo "3. Check logs if build fails"
|
| 81 |
+
else
|
| 82 |
+
echo ""
|
| 83 |
+
echo "=== Push failed ==="
|
| 84 |
+
echo "You may need to:"
|
| 85 |
+
echo "1. Set up authentication (git credential or token)"
|
| 86 |
+
echo "2. Check remote URL: git remote -v"
|
| 87 |
+
echo "3. Try: git push origin main"
|
| 88 |
+
fi
|
| 89 |
+
else
|
| 90 |
+
echo ""
|
| 91 |
+
echo "=== Commit failed ==="
|
| 92 |
+
echo "Check git status and resolve any conflicts"
|
| 93 |
+
fi
|
| 94 |
+
|
| 95 |
+
echo ""
|
| 96 |
+
echo "=== Script completed ==="
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
job-ml:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
container_name: job-ml-service
|
| 9 |
+
ports:
|
| 10 |
+
- "8000:8000"
|
| 11 |
+
volumes:
|
| 12 |
+
# Mount models directory so models persist and can be updated
|
| 13 |
+
- ./models:/app/models
|
| 14 |
+
# Mount data directory for training data (optional)
|
| 15 |
+
- ./data:/app/data
|
| 16 |
+
environment:
|
| 17 |
+
- PYTHONUNBUFFERED=1
|
| 18 |
+
restart: unless-stopped
|
| 19 |
+
healthcheck:
|
| 20 |
+
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
|
| 21 |
+
interval: 30s
|
| 22 |
+
timeout: 10s
|
| 23 |
+
retries: 3
|
| 24 |
+
start_period: 10s
|
| 25 |
+
networks:
|
| 26 |
+
- ml-network
|
| 27 |
+
|
| 28 |
+
networks:
|
| 29 |
+
ml-network:
|
| 30 |
+
driver: bridge
|
n8n_workflow_examples.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# n8n Workflow Examples for Job Failure Prediction
|
| 2 |
+
|
| 3 |
+
This document provides examples for integrating the ML service with n8n workflows.
|
| 4 |
+
|
| 5 |
+
## Service Endpoints
|
| 6 |
+
|
| 7 |
+
- **Base URL**: `http://job-ml:8000` (internal) or `http://localhost:8000` (local)
|
| 8 |
+
- **Health Check**: `GET /health`
|
| 9 |
+
- **Job Failure Prediction**: `POST /predict/job-fail`
|
| 10 |
+
- **Anomaly Detection**: `POST /detect/anomaly`
|
| 11 |
+
|
| 12 |
+
## Example 1: Job Failure Prediction Workflow
|
| 13 |
+
|
| 14 |
+
### Workflow Structure
|
| 15 |
+
1. **Trigger**: Database query / Webhook / Schedule
|
| 16 |
+
2. **HTTP Request**: Call prediction endpoint
|
| 17 |
+
3. **IF Node**: Check risk level
|
| 18 |
+
4. **Action**: Send alert (Slack / PagerDuty / Email)
|
| 19 |
+
|
| 20 |
+
### HTTP Request Node Configuration
|
| 21 |
+
|
| 22 |
+
**Method**: `POST`
|
| 23 |
+
**URL**: `http://job-ml:8000/predict/job-fail`
|
| 24 |
+
**Headers**:
|
| 25 |
+
```
|
| 26 |
+
Content-Type: application/json
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
**Body (JSON)**:
|
| 30 |
+
```json
|
| 31 |
+
{
|
| 32 |
+
"zone": "{{ $json.zone }}",
|
| 33 |
+
"job_nm": "{{ $json.job_nm }}",
|
| 34 |
+
"tasksgroup_nm": "{{ $json.tasksgroup_nm }}",
|
| 35 |
+
"job_start_time": "{{ $json.job_start_time }}",
|
| 36 |
+
"duration": "{{ $json.duration }}",
|
| 37 |
+
"duration_sec": {{ $json.duration_sec }},
|
| 38 |
+
"status": "{{ $json.status }}",
|
| 39 |
+
"err_msg": "{{ $json.err_msg }}",
|
| 40 |
+
"zeppelin": "{{ $json.zeppelin }}",
|
| 41 |
+
"explain": true
|
| 42 |
+
}
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### IF Node Conditions
|
| 46 |
+
|
| 47 |
+
**WARNING Condition** (Medium Risk):
|
| 48 |
+
```javascript
|
| 49 |
+
{{ $json.fail_probability >= 0.5 && $json.fail_probability < 0.8 }}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**CRITICAL Condition** (High Risk):
|
| 53 |
+
```javascript
|
| 54 |
+
{{ $json.fail_probability >= 0.8 }}
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
**Combined Alert Condition** (WARNING or CRITICAL):
|
| 58 |
+
```javascript
|
| 59 |
+
{{ $json.fail_probability >= 0.5 }}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### Example Response Handling
|
| 63 |
+
|
| 64 |
+
The response will look like:
|
| 65 |
+
```json
|
| 66 |
+
{
|
| 67 |
+
"fail_probability": 0.79,
|
| 68 |
+
"risk_level": "MEDIUM",
|
| 69 |
+
"top_drivers": [
|
| 70 |
+
{
|
| 71 |
+
"feature": "failure_rate_7",
|
| 72 |
+
"shap_value": 0.30,
|
| 73 |
+
"effect": "increase"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"feature": "duration_zscore",
|
| 77 |
+
"shap_value": 0.18,
|
| 78 |
+
"effect": "increase"
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
"recommended_actions": [
|
| 82 |
+
"Monitor upstream dependencies and recent job history",
|
| 83 |
+
"Check for resource constraints or data volume spikes"
|
| 84 |
+
]
|
| 85 |
+
}
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Slack Alert Example
|
| 89 |
+
|
| 90 |
+
**Slack Node Configuration**:
|
| 91 |
+
- **Channel**: `#job-alerts`
|
| 92 |
+
- **Text**:
|
| 93 |
+
```
|
| 94 |
+
🚨 Job Failure Alert
|
| 95 |
+
|
| 96 |
+
Job: {{ $json.job_nm }}
|
| 97 |
+
Zone: {{ $json.zone }}
|
| 98 |
+
Risk Level: *{{ $('HTTP Request').item.json.risk_level }}*
|
| 99 |
+
Failure Probability: {{ $('HTTP Request').item.json.fail_probability * 100 }}%
|
| 100 |
+
|
| 101 |
+
Top Risk Factors:
|
| 102 |
+
{{ $('HTTP Request').item.json.top_drivers.map(d => `• ${d.feature}: ${d.effect}`).join('\n') }}
|
| 103 |
+
|
| 104 |
+
Recommended Actions:
|
| 105 |
+
{{ $('HTTP Request').item.json.recommended_actions.map(a => `• ${a}`).join('\n') }}
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## Example 2: Anomaly Detection Workflow
|
| 111 |
+
|
| 112 |
+
### HTTP Request Node Configuration
|
| 113 |
+
|
| 114 |
+
**Method**: `POST`
|
| 115 |
+
**URL**: `http://job-ml:8000/detect/anomaly`
|
| 116 |
+
**Headers**:
|
| 117 |
+
```
|
| 118 |
+
Content-Type: application/json
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
**Body (JSON)**:
|
| 122 |
+
```json
|
| 123 |
+
{
|
| 124 |
+
"features": {
|
| 125 |
+
"duration_sec": {{ $json.duration_sec }},
|
| 126 |
+
"duration_zscore": {{ $json.duration_zscore }},
|
| 127 |
+
"avg_duration_7": {{ $json.avg_duration_7 }},
|
| 128 |
+
"failure_rate_7": {{ $json.failure_rate_7 }},
|
| 129 |
+
"err_msg_len": {{ $json.err_msg_len || 0 }},
|
| 130 |
+
"hour_sin": {{ $json.hour_sin || 0 }},
|
| 131 |
+
"hour_cos": {{ $json.hour_cos || 0 }}
|
| 132 |
+
},
|
| 133 |
+
"threshold": 0.01
|
| 134 |
+
}
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### IF Node Condition
|
| 138 |
+
|
| 139 |
+
**Anomaly Detected**:
|
| 140 |
+
```javascript
|
| 141 |
+
{{ $json.is_anomaly === true }}
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
**High Severity Anomaly** (reconstruction error > 3x threshold):
|
| 145 |
+
```javascript
|
| 146 |
+
{{ $json.is_anomaly === true && $json.reconstruction_error > ($json.threshold * 3) }}
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Example Response
|
| 150 |
+
|
| 151 |
+
```json
|
| 152 |
+
{
|
| 153 |
+
"reconstruction_error": 0.0235,
|
| 154 |
+
"is_anomaly": true,
|
| 155 |
+
"threshold": 0.01,
|
| 156 |
+
"top_drivers": [
|
| 157 |
+
{
|
| 158 |
+
"feature": "duration_zscore",
|
| 159 |
+
"error": 0.0142
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"feature": "duration_sec",
|
| 163 |
+
"error": 0.0068
|
| 164 |
+
}
|
| 165 |
+
]
|
| 166 |
+
}
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## Example 3: Combined Workflow (Prediction + Anomaly)
|
| 172 |
+
|
| 173 |
+
### Workflow Structure
|
| 174 |
+
1. **Trigger**: Job completion event
|
| 175 |
+
2. **HTTP Request 1**: Job failure prediction
|
| 176 |
+
3. **HTTP Request 2**: Anomaly detection
|
| 177 |
+
4. **IF Node**: Combined alert logic
|
| 178 |
+
5. **Action**: Send alert
|
| 179 |
+
|
| 180 |
+
### Combined Alert Condition
|
| 181 |
+
|
| 182 |
+
**Alert if EITHER prediction is risky OR anomaly is detected**:
|
| 183 |
+
```javascript
|
| 184 |
+
{{
|
| 185 |
+
($('Predict').item.json.fail_probability >= 0.5) ||
|
| 186 |
+
($('Anomaly').item.json.is_anomaly === true)
|
| 187 |
+
}}
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
**High Priority Alert** (both conditions):
|
| 191 |
+
```javascript
|
| 192 |
+
{{
|
| 193 |
+
($('Predict').item.json.fail_probability >= 0.8) ||
|
| 194 |
+
($('Anomaly').item.json.is_anomaly === true && $('Anomaly').item.json.reconstruction_error > ($('Anomaly').item.json.threshold * 3))
|
| 195 |
+
}}
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## Example 4: Scheduled Monitoring Workflow
|
| 201 |
+
|
| 202 |
+
### Workflow Structure
|
| 203 |
+
1. **Schedule Trigger**: Every 15 minutes
|
| 204 |
+
2. **Database Query**: Get recent job runs
|
| 205 |
+
3. **Loop**: For each job
|
| 206 |
+
- HTTP Request: Predict failure
|
| 207 |
+
- HTTP Request: Detect anomaly
|
| 208 |
+
- IF: Check conditions
|
| 209 |
+
- Action: Alert if needed
|
| 210 |
+
|
| 211 |
+
### Code Node for Batch Processing
|
| 212 |
+
|
| 213 |
+
```javascript
|
| 214 |
+
// Process multiple jobs
|
| 215 |
+
const jobs = $input.all();
|
| 216 |
+
const results = [];
|
| 217 |
+
|
| 218 |
+
for (const job of jobs) {
|
| 219 |
+
// Call prediction API
|
| 220 |
+
const predictResponse = await $http.post('http://job-ml:8000/predict/job-fail', {
|
| 221 |
+
zone: job.json.zone,
|
| 222 |
+
job_nm: job.json.job_nm,
|
| 223 |
+
job_start_time: job.json.job_start_time,
|
| 224 |
+
duration_sec: job.json.duration_sec,
|
| 225 |
+
status: job.json.status,
|
| 226 |
+
err_msg: job.json.err_msg,
|
| 227 |
+
explain: true
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
// Call anomaly API
|
| 231 |
+
const anomalyResponse = await $http.post('http://job-ml:8000/detect/anomaly', {
|
| 232 |
+
features: {
|
| 233 |
+
duration_sec: job.json.duration_sec,
|
| 234 |
+
duration_zscore: job.json.duration_zscore || 0,
|
| 235 |
+
err_msg_len: (job.json.err_msg || '').length
|
| 236 |
+
}
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
results.push({
|
| 240 |
+
job: job.json.job_nm,
|
| 241 |
+
prediction: predictResponse.data,
|
| 242 |
+
anomaly: anomalyResponse.data,
|
| 243 |
+
should_alert: predictResponse.data.fail_probability >= 0.5 || anomalyResponse.data.is_anomaly
|
| 244 |
+
});
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
return results.map(r => ({ json: r }));
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
## Alert Severity Levels
|
| 253 |
+
|
| 254 |
+
### INFO
|
| 255 |
+
- `fail_probability < 0.3` and no anomaly
|
| 256 |
+
- Normal operations
|
| 257 |
+
|
| 258 |
+
### LOW
|
| 259 |
+
- `0.3 <= fail_probability < 0.5`
|
| 260 |
+
- Minor deviations detected
|
| 261 |
+
|
| 262 |
+
### WARNING (MEDIUM)
|
| 263 |
+
- `0.5 <= fail_probability < 0.8`
|
| 264 |
+
- OR `is_anomaly === true` with `reconstruction_error < threshold * 3`
|
| 265 |
+
- Requires monitoring
|
| 266 |
+
|
| 267 |
+
### CRITICAL
|
| 268 |
+
- `fail_probability >= 0.8`
|
| 269 |
+
- OR `is_anomaly === true` with `reconstruction_error >= threshold * 3`
|
| 270 |
+
- Immediate action required
|
| 271 |
+
|
| 272 |
+
---
|
| 273 |
+
|
| 274 |
+
## Error Handling
|
| 275 |
+
|
| 276 |
+
### HTTP Request Error Handling
|
| 277 |
+
|
| 278 |
+
In n8n, configure the HTTP Request node to:
|
| 279 |
+
- **Continue on Error**: Enabled
|
| 280 |
+
- **Response Format**: JSON
|
| 281 |
+
|
| 282 |
+
Add an IF node after HTTP Request to check for errors:
|
| 283 |
+
```javascript
|
| 284 |
+
{{ $json.error !== undefined && $json.error !== null }}
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
### Retry Logic
|
| 288 |
+
|
| 289 |
+
For critical predictions, add a retry mechanism:
|
| 290 |
+
1. HTTP Request node
|
| 291 |
+
2. IF node: Check if response has error
|
| 292 |
+
3. Wait node: 5 seconds
|
| 293 |
+
4. HTTP Request node: Retry (max 3 times)
|
| 294 |
+
|
| 295 |
+
---
|
| 296 |
+
|
| 297 |
+
## Testing the Workflow
|
| 298 |
+
|
| 299 |
+
### Test Payload (Job Failure Prediction)
|
| 300 |
+
|
| 301 |
+
```json
|
| 302 |
+
{
|
| 303 |
+
"zone": "prod",
|
| 304 |
+
"job_nm": "daily_export_customer",
|
| 305 |
+
"tasksgroup_nm": "export_group",
|
| 306 |
+
"job_start_time": "2026-01-21T01:00:00",
|
| 307 |
+
"duration": "01:30:00",
|
| 308 |
+
"duration_sec": 5400,
|
| 309 |
+
"status": "SUCCESS",
|
| 310 |
+
"err_msg": "",
|
| 311 |
+
"zeppelin": null
|
| 312 |
+
}
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
### Test Payload (Anomaly Detection)
|
| 316 |
+
|
| 317 |
+
```json
|
| 318 |
+
{
|
| 319 |
+
"features": {
|
| 320 |
+
"duration_sec": 5400,
|
| 321 |
+
"duration_zscore": 1.6,
|
| 322 |
+
"avg_duration_7": 3000,
|
| 323 |
+
"failure_rate_7": 0.15,
|
| 324 |
+
"err_msg_len": 0,
|
| 325 |
+
"hour_sin": 0.2588,
|
| 326 |
+
"hour_cos": 0.9659
|
| 327 |
+
},
|
| 328 |
+
"threshold": 0.01
|
| 329 |
+
}
|
| 330 |
+
```
|
| 331 |
+
|
| 332 |
+
---
|
| 333 |
+
|
| 334 |
+
## n8n Workflow JSON Export
|
| 335 |
+
|
| 336 |
+
See `n8n_workflow_job_monitoring.json` for a complete importable workflow.
|
n8n_workflow_job_monitoring.json
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Job Failure Monitoring",
|
| 3 |
+
"nodes": [
|
| 4 |
+
{
|
| 5 |
+
"parameters": {
|
| 6 |
+
"rule": {
|
| 7 |
+
"interval": [
|
| 8 |
+
{
|
| 9 |
+
"field": "minutes",
|
| 10 |
+
"minutesInterval": 15
|
| 11 |
+
}
|
| 12 |
+
]
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"id": "schedule-trigger",
|
| 16 |
+
"name": "Schedule Trigger",
|
| 17 |
+
"type": "n8n-nodes-base.scheduleTrigger",
|
| 18 |
+
"typeVersion": 1.1,
|
| 19 |
+
"position": [250, 300]
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"parameters": {
|
| 23 |
+
"method": "POST",
|
| 24 |
+
"url": "http://job-ml:8000/predict/job-fail",
|
| 25 |
+
"authentication": "none",
|
| 26 |
+
"sendHeaders": true,
|
| 27 |
+
"headerParameters": {
|
| 28 |
+
"parameters": [
|
| 29 |
+
{
|
| 30 |
+
"name": "Content-Type",
|
| 31 |
+
"value": "application/json"
|
| 32 |
+
}
|
| 33 |
+
]
|
| 34 |
+
},
|
| 35 |
+
"sendBody": true,
|
| 36 |
+
"bodyParameters": {
|
| 37 |
+
"parameters": []
|
| 38 |
+
},
|
| 39 |
+
"specifyBody": "json",
|
| 40 |
+
"jsonBody": "={\n \"zone\": \"{{ $json.zone }}\",\n \"job_nm\": \"{{ $json.job_nm }}\",\n \"tasksgroup_nm\": \"{{ $json.tasksgroup_nm }}\",\n \"job_start_time\": \"{{ $json.job_start_time }}\",\n \"duration\": \"{{ $json.duration }}\",\n \"duration_sec\": {{ $json.duration_sec }},\n \"status\": \"{{ $json.status }}\",\n \"err_msg\": \"{{ $json.err_msg }}\",\n \"zeppelin\": \"{{ $json.zeppelin }}\",\n \"explain\": true\n}",
|
| 41 |
+
"options": {}
|
| 42 |
+
},
|
| 43 |
+
"id": "predict-http",
|
| 44 |
+
"name": "Predict Job Failure",
|
| 45 |
+
"type": "n8n-nodes-base.httpRequest",
|
| 46 |
+
"typeVersion": 4.1,
|
| 47 |
+
"position": [450, 300]
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"parameters": {
|
| 51 |
+
"conditions": {
|
| 52 |
+
"options": {
|
| 53 |
+
"caseSensitive": true,
|
| 54 |
+
"leftValue": "",
|
| 55 |
+
"typeValidation": "strict"
|
| 56 |
+
},
|
| 57 |
+
"conditions": [
|
| 58 |
+
{
|
| 59 |
+
"id": "risk-check",
|
| 60 |
+
"leftValue": "={{ $json.fail_probability }}",
|
| 61 |
+
"rightValue": 0.5,
|
| 62 |
+
"operator": {
|
| 63 |
+
"type": "number",
|
| 64 |
+
"operation": "gte"
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"combinator": "and"
|
| 69 |
+
},
|
| 70 |
+
"options": {}
|
| 71 |
+
},
|
| 72 |
+
"id": "risk-if",
|
| 73 |
+
"name": "Check Risk Level",
|
| 74 |
+
"type": "n8n-nodes-base.if",
|
| 75 |
+
"typeVersion": 2,
|
| 76 |
+
"position": [650, 300]
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"parameters": {
|
| 80 |
+
"method": "POST",
|
| 81 |
+
"url": "http://job-ml:8000/detect/anomaly",
|
| 82 |
+
"authentication": "none",
|
| 83 |
+
"sendHeaders": true,
|
| 84 |
+
"headerParameters": {
|
| 85 |
+
"parameters": [
|
| 86 |
+
{
|
| 87 |
+
"name": "Content-Type",
|
| 88 |
+
"value": "application/json"
|
| 89 |
+
}
|
| 90 |
+
]
|
| 91 |
+
},
|
| 92 |
+
"sendBody": true,
|
| 93 |
+
"specifyBody": "json",
|
| 94 |
+
"jsonBody": "={\n \"features\": {\n \"duration_sec\": {{ $json.duration_sec }},\n \"duration_zscore\": {{ $json.duration_zscore || 0 }},\n \"err_msg_len\": {{ ($json.err_msg || '').length }}\n },\n \"threshold\": 0.01\n}",
|
| 95 |
+
"options": {}
|
| 96 |
+
},
|
| 97 |
+
"id": "anomaly-http",
|
| 98 |
+
"name": "Detect Anomaly",
|
| 99 |
+
"type": "n8n-nodes-base.httpRequest",
|
| 100 |
+
"typeVersion": 4.1,
|
| 101 |
+
"position": [650, 200]
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"parameters": {
|
| 105 |
+
"conditions": {
|
| 106 |
+
"options": {
|
| 107 |
+
"caseSensitive": true,
|
| 108 |
+
"leftValue": "",
|
| 109 |
+
"typeValidation": "strict"
|
| 110 |
+
},
|
| 111 |
+
"conditions": [
|
| 112 |
+
{
|
| 113 |
+
"id": "anomaly-check",
|
| 114 |
+
"leftValue": "={{ $json.is_anomaly }}",
|
| 115 |
+
"rightValue": true,
|
| 116 |
+
"operator": {
|
| 117 |
+
"type": "boolean",
|
| 118 |
+
"operation": "true"
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
],
|
| 122 |
+
"combinator": "and"
|
| 123 |
+
},
|
| 124 |
+
"options": {}
|
| 125 |
+
},
|
| 126 |
+
"id": "anomaly-if",
|
| 127 |
+
"name": "Check Anomaly",
|
| 128 |
+
"type": "n8n-nodes-base.if",
|
| 129 |
+
"typeVersion": 2,
|
| 130 |
+
"position": [850, 200]
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"parameters": {
|
| 134 |
+
"channel": "#job-alerts",
|
| 135 |
+
"text": "=🚨 Job Failure Alert\n\nJob: {{ $('Predict Job Failure').item.json.job_nm }}\nZone: {{ $('Predict Job Failure').item.json.zone }}\nRisk Level: *{{ $('Predict Job Failure').item.json.risk_level }}*\nFailure Probability: {{ Math.round($('Predict Job Failure').item.json.fail_probability * 100) }}%\n\nTop Risk Factors:\n{{ $('Predict Job Failure').item.json.top_drivers.map(d => `• ${d.feature}: ${d.effect}`).join('\\n') }}\n\nRecommended Actions:\n{{ $('Predict Job Failure').item.json.recommended_actions.map(a => `• ${a}`).join('\\n') }}",
|
| 136 |
+
"otherOptions": {}
|
| 137 |
+
},
|
| 138 |
+
"id": "slack-alert",
|
| 139 |
+
"name": "Send Slack Alert",
|
| 140 |
+
"type": "n8n-nodes-base.slack",
|
| 141 |
+
"typeVersion": 2.1,
|
| 142 |
+
"position": [1050, 300],
|
| 143 |
+
"credentials": {
|
| 144 |
+
"slackApi": {
|
| 145 |
+
"id": "slack-credentials",
|
| 146 |
+
"name": "Slack API"
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
],
|
| 151 |
+
"connections": {
|
| 152 |
+
"Schedule Trigger": {
|
| 153 |
+
"main": [
|
| 154 |
+
[
|
| 155 |
+
{
|
| 156 |
+
"node": "Predict Job Failure",
|
| 157 |
+
"type": "main",
|
| 158 |
+
"index": 0
|
| 159 |
+
}
|
| 160 |
+
]
|
| 161 |
+
]
|
| 162 |
+
},
|
| 163 |
+
"Predict Job Failure": {
|
| 164 |
+
"main": [
|
| 165 |
+
[
|
| 166 |
+
{
|
| 167 |
+
"node": "Check Risk Level",
|
| 168 |
+
"type": "main",
|
| 169 |
+
"index": 0
|
| 170 |
+
}
|
| 171 |
+
]
|
| 172 |
+
]
|
| 173 |
+
},
|
| 174 |
+
"Check Risk Level": {
|
| 175 |
+
"main": [
|
| 176 |
+
[
|
| 177 |
+
{
|
| 178 |
+
"node": "Send Slack Alert",
|
| 179 |
+
"type": "main",
|
| 180 |
+
"index": 0
|
| 181 |
+
}
|
| 182 |
+
],
|
| 183 |
+
[
|
| 184 |
+
{
|
| 185 |
+
"node": "Detect Anomaly",
|
| 186 |
+
"type": "main",
|
| 187 |
+
"index": 0
|
| 188 |
+
}
|
| 189 |
+
]
|
| 190 |
+
]
|
| 191 |
+
},
|
| 192 |
+
"Detect Anomaly": {
|
| 193 |
+
"main": [
|
| 194 |
+
[
|
| 195 |
+
{
|
| 196 |
+
"node": "Check Anomaly",
|
| 197 |
+
"type": "main",
|
| 198 |
+
"index": 0
|
| 199 |
+
}
|
| 200 |
+
]
|
| 201 |
+
]
|
| 202 |
+
},
|
| 203 |
+
"Check Anomaly": {
|
| 204 |
+
"main": [
|
| 205 |
+
[
|
| 206 |
+
{
|
| 207 |
+
"node": "Send Slack Alert",
|
| 208 |
+
"type": "main",
|
| 209 |
+
"index": 0
|
| 210 |
+
}
|
| 211 |
+
]
|
| 212 |
+
]
|
| 213 |
+
}
|
| 214 |
+
},
|
| 215 |
+
"pinData": {},
|
| 216 |
+
"settings": {
|
| 217 |
+
"executionOrder": "v1"
|
| 218 |
+
},
|
| 219 |
+
"staticData": null,
|
| 220 |
+
"tags": [],
|
| 221 |
+
"triggerCount": 1,
|
| 222 |
+
"updatedAt": "2026-01-25T00:00:00.000Z",
|
| 223 |
+
"versionId": "1"
|
| 224 |
+
}
|
requirements.txt
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
-
fastapi
|
| 2 |
-
uvicorn[standard]
|
| 3 |
-
pydantic
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
requests==2.31.0
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
pydantic
|
| 4 |
+
numpy
|
| 5 |
+
scikit-learn
|
| 6 |
+
xgboost
|
| 7 |
+
tensorflow
|
| 8 |
+
shap
|
| 9 |
+
joblib
|
| 10 |
+
python-multipart
|
| 11 |
+
requests
|
|
|
test_api.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple test script for API endpoints.
|
| 3 |
+
Run this after starting the service to verify endpoints work correctly.
|
| 4 |
+
"""
|
| 5 |
+
import requests
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
BASE_URL = "http://localhost:8000"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_health():
|
| 12 |
+
"""Test health endpoint."""
|
| 13 |
+
print("Testing /health endpoint...")
|
| 14 |
+
response = requests.get(f"{BASE_URL}/health")
|
| 15 |
+
print(f"Status: {response.status_code}")
|
| 16 |
+
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
| 17 |
+
print()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_predict():
|
| 21 |
+
"""Test job failure prediction endpoint."""
|
| 22 |
+
print("Testing /predict/job-fail endpoint...")
|
| 23 |
+
payload = {
|
| 24 |
+
"zone": "prod",
|
| 25 |
+
"job_nm": "daily_export_customer",
|
| 26 |
+
"tasksgroup_nm": "export_group",
|
| 27 |
+
"job_start_time": "2026-01-21T01:00:00",
|
| 28 |
+
"duration": "01:30:00",
|
| 29 |
+
"duration_sec": 5400,
|
| 30 |
+
"status": "SUCCESS",
|
| 31 |
+
"err_msg": "",
|
| 32 |
+
"zeppelin": None,
|
| 33 |
+
"explain": True
|
| 34 |
+
}
|
| 35 |
+
response = requests.post(
|
| 36 |
+
f"{BASE_URL}/predict/job-fail",
|
| 37 |
+
json=payload,
|
| 38 |
+
headers={"Content-Type": "application/json"}
|
| 39 |
+
)
|
| 40 |
+
print(f"Status: {response.status_code}")
|
| 41 |
+
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
| 42 |
+
print()
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_anomaly():
|
| 46 |
+
"""Test anomaly detection endpoint."""
|
| 47 |
+
print("Testing /detect/anomaly endpoint...")
|
| 48 |
+
payload = {
|
| 49 |
+
"features": {
|
| 50 |
+
"duration_sec": 5400,
|
| 51 |
+
"duration_zscore": 1.6,
|
| 52 |
+
"avg_duration_7": 3000,
|
| 53 |
+
"failure_rate_7": 0.15,
|
| 54 |
+
"err_msg_len": 0,
|
| 55 |
+
"hour_sin": 0.2588,
|
| 56 |
+
"hour_cos": 0.9659
|
| 57 |
+
},
|
| 58 |
+
"threshold": 0.01
|
| 59 |
+
}
|
| 60 |
+
response = requests.post(
|
| 61 |
+
f"{BASE_URL}/detect/anomaly",
|
| 62 |
+
json=payload,
|
| 63 |
+
headers={"Content-Type": "application/json"}
|
| 64 |
+
)
|
| 65 |
+
print(f"Status: {response.status_code}")
|
| 66 |
+
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
| 67 |
+
print()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
try:
|
| 72 |
+
test_health()
|
| 73 |
+
test_predict()
|
| 74 |
+
test_anomaly()
|
| 75 |
+
print("All tests completed!")
|
| 76 |
+
except requests.exceptions.ConnectionError:
|
| 77 |
+
print("Error: Could not connect to API. Make sure the service is running on http://localhost:8000")
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"Error: {e}")
|
train_anomaly.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training script for Anomaly Detection using Autoencoder.
|
| 3 |
+
"""
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import numpy as np
|
| 6 |
+
from sklearn.preprocessing import StandardScaler
|
| 7 |
+
from tensorflow import keras
|
| 8 |
+
from tensorflow.keras import Model, Input
|
| 9 |
+
from tensorflow.keras.layers import Dense
|
| 10 |
+
import joblib
|
| 11 |
+
import os
|
| 12 |
+
from preprocessing import engineer_features, get_feature_columns
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def train_anomaly_detector(
|
| 16 |
+
train_csv_paths: list,
|
| 17 |
+
output_dir: str = 'models',
|
| 18 |
+
encoding_dim: int = 32,
|
| 19 |
+
epochs: int = 50,
|
| 20 |
+
batch_size: int = 256,
|
| 21 |
+
random_state: int = 42
|
| 22 |
+
):
|
| 23 |
+
"""
|
| 24 |
+
Train autoencoder for anomaly detection.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
train_csv_paths: List of CSV file paths to load
|
| 28 |
+
output_dir: Directory to save model artifacts
|
| 29 |
+
encoding_dim: Dimension of encoding layer
|
| 30 |
+
epochs: Training epochs
|
| 31 |
+
batch_size: Batch size
|
| 32 |
+
random_state: Random seed
|
| 33 |
+
"""
|
| 34 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 35 |
+
np.random.seed(random_state)
|
| 36 |
+
|
| 37 |
+
# Load and combine data
|
| 38 |
+
print("Loading data...")
|
| 39 |
+
dfs = []
|
| 40 |
+
for csv_path in train_csv_paths:
|
| 41 |
+
if os.path.exists(csv_path):
|
| 42 |
+
df = pd.read_csv(csv_path)
|
| 43 |
+
dfs.append(df)
|
| 44 |
+
print(f" Loaded {len(df)} rows from {csv_path}")
|
| 45 |
+
else:
|
| 46 |
+
print(f" Warning: {csv_path} not found, skipping")
|
| 47 |
+
|
| 48 |
+
if not dfs:
|
| 49 |
+
raise ValueError("No data files found!")
|
| 50 |
+
|
| 51 |
+
df = pd.concat(dfs, ignore_index=True)
|
| 52 |
+
print(f"Total rows: {len(df)}")
|
| 53 |
+
|
| 54 |
+
# Engineer features
|
| 55 |
+
print("Engineering features...")
|
| 56 |
+
df = engineer_features(df)
|
| 57 |
+
|
| 58 |
+
# Get numeric features for autoencoder
|
| 59 |
+
feature_cols = get_feature_columns()
|
| 60 |
+
num_cols = feature_cols['anomaly_numeric']
|
| 61 |
+
|
| 62 |
+
# Ensure all columns exist
|
| 63 |
+
for col in num_cols:
|
| 64 |
+
if col not in df.columns:
|
| 65 |
+
df[col] = 0.0
|
| 66 |
+
|
| 67 |
+
X = df[num_cols].copy()
|
| 68 |
+
X = X.fillna(0.0)
|
| 69 |
+
|
| 70 |
+
print(f"Anomaly features: {num_cols}")
|
| 71 |
+
print(f"Feature matrix shape: {X.shape}")
|
| 72 |
+
|
| 73 |
+
# Scale features
|
| 74 |
+
print("Scaling features...")
|
| 75 |
+
scaler = StandardScaler()
|
| 76 |
+
X_scaled = scaler.fit_transform(X)
|
| 77 |
+
|
| 78 |
+
# Build autoencoder
|
| 79 |
+
print("Building autoencoder...")
|
| 80 |
+
input_dim = X_scaled.shape[1]
|
| 81 |
+
|
| 82 |
+
input_layer = Input(shape=(input_dim,))
|
| 83 |
+
encoded = Dense(64, activation='relu')(input_layer)
|
| 84 |
+
encoded = Dense(encoding_dim, activation='relu')(encoded)
|
| 85 |
+
decoded = Dense(64, activation='relu')(encoded)
|
| 86 |
+
decoded = Dense(input_dim, activation='linear')(decoded)
|
| 87 |
+
|
| 88 |
+
autoencoder = Model(input_layer, decoded)
|
| 89 |
+
autoencoder.compile(optimizer='adam', loss='mse')
|
| 90 |
+
|
| 91 |
+
print(f"Autoencoder architecture:")
|
| 92 |
+
autoencoder.summary()
|
| 93 |
+
|
| 94 |
+
# Train
|
| 95 |
+
print("Training autoencoder...")
|
| 96 |
+
history = autoencoder.fit(
|
| 97 |
+
X_scaled, X_scaled,
|
| 98 |
+
epochs=epochs,
|
| 99 |
+
batch_size=batch_size,
|
| 100 |
+
validation_split=0.1,
|
| 101 |
+
verbose=1
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Compute reconstruction errors
|
| 105 |
+
print("Computing reconstruction errors...")
|
| 106 |
+
X_pred = autoencoder.predict(X_scaled, verbose=0)
|
| 107 |
+
recon_errors = np.mean((X_scaled - X_pred) ** 2, axis=1)
|
| 108 |
+
|
| 109 |
+
df['recon_error'] = recon_errors
|
| 110 |
+
|
| 111 |
+
# Compute per-job thresholds (97th percentile)
|
| 112 |
+
df['anom_threshold_job'] = df.groupby('job_nm')['recon_error'].transform(
|
| 113 |
+
lambda s: np.percentile(s, 97) if len(s) > 0 else np.percentile(recon_errors, 97)
|
| 114 |
+
)
|
| 115 |
+
df['is_anomaly_job'] = (df['recon_error'] > df['anom_threshold_job']).astype(int)
|
| 116 |
+
|
| 117 |
+
print(f"\nReconstruction error stats:")
|
| 118 |
+
print(f" Mean: {recon_errors.mean():.6f}")
|
| 119 |
+
print(f" Std: {recon_errors.std():.6f}")
|
| 120 |
+
print(f" Min: {recon_errors.min():.6f}")
|
| 121 |
+
print(f" Max: {recon_errors.max():.6f}")
|
| 122 |
+
print(f" 97th percentile: {np.percentile(recon_errors, 97):.6f}")
|
| 123 |
+
print(f" Anomalies detected: {df['is_anomaly_job'].sum()} ({df['is_anomaly_job'].mean()*100:.2f}%)")
|
| 124 |
+
|
| 125 |
+
# Save model and artifacts
|
| 126 |
+
model_path = os.path.join(output_dir, 'anomaly_autoencoder_cpu.keras')
|
| 127 |
+
autoencoder.save(model_path)
|
| 128 |
+
print(f"\nAutoencoder saved to {model_path}")
|
| 129 |
+
|
| 130 |
+
scaler_path = os.path.join(output_dir, 'anomaly_scaler.joblib')
|
| 131 |
+
joblib.dump(scaler, scaler_path)
|
| 132 |
+
print(f"Scaler saved to {scaler_path}")
|
| 133 |
+
|
| 134 |
+
feature_list_path = os.path.join(output_dir, 'anomaly_features.joblib')
|
| 135 |
+
joblib.dump(num_cols, feature_list_path)
|
| 136 |
+
print(f"Feature list saved to {feature_list_path}")
|
| 137 |
+
|
| 138 |
+
# Save global threshold (97th percentile)
|
| 139 |
+
global_threshold = np.percentile(recon_errors, 97)
|
| 140 |
+
threshold_path = os.path.join(output_dir, 'anomaly_threshold.joblib')
|
| 141 |
+
joblib.dump(global_threshold, threshold_path)
|
| 142 |
+
print(f"Global threshold saved to {threshold_path} (value: {global_threshold:.6f})")
|
| 143 |
+
|
| 144 |
+
return autoencoder, scaler, X_scaled, recon_errors
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
if __name__ == '__main__':
|
| 148 |
+
import sys
|
| 149 |
+
import glob
|
| 150 |
+
|
| 151 |
+
# Default CSV paths (can be overridden via command line)
|
| 152 |
+
if len(sys.argv) > 1:
|
| 153 |
+
# Handle glob patterns (works in both PowerShell and bash)
|
| 154 |
+
csv_paths = []
|
| 155 |
+
for arg in sys.argv[1:]:
|
| 156 |
+
# Expand glob patterns
|
| 157 |
+
expanded = glob.glob(arg)
|
| 158 |
+
if expanded:
|
| 159 |
+
csv_paths.extend(expanded)
|
| 160 |
+
else:
|
| 161 |
+
# If no match, use as-is (might be a specific file)
|
| 162 |
+
csv_paths.append(arg)
|
| 163 |
+
else:
|
| 164 |
+
# Default: all CSV files in data directory
|
| 165 |
+
csv_paths = glob.glob('data/*.csv')
|
| 166 |
+
if not csv_paths:
|
| 167 |
+
csv_paths = [
|
| 168 |
+
'data/true_export_report_20260120.csv',
|
| 169 |
+
'data/true_export_report_20260121.csv'
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
if not csv_paths:
|
| 173 |
+
print("Error: No CSV files found!")
|
| 174 |
+
print("Usage: python train_anomaly.py [file1.csv] [file2.csv] ...")
|
| 175 |
+
print(" or: python train_anomaly.py data/*.csv")
|
| 176 |
+
sys.exit(1)
|
| 177 |
+
|
| 178 |
+
print(f"Training with {len(csv_paths)} file(s):")
|
| 179 |
+
for path in csv_paths:
|
| 180 |
+
print(f" - {path}")
|
| 181 |
+
print()
|
| 182 |
+
|
| 183 |
+
train_anomaly_detector(csv_paths)
|
train_job_failure.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training script for Job Failure Prediction using XGBoost.
|
| 3 |
+
"""
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import numpy as np
|
| 6 |
+
from sklearn.compose import ColumnTransformer
|
| 7 |
+
from sklearn.pipeline import Pipeline
|
| 8 |
+
from sklearn.preprocessing import StandardScaler, OneHotEncoder
|
| 9 |
+
from sklearn.impute import SimpleImputer
|
| 10 |
+
from sklearn.metrics import (
|
| 11 |
+
classification_report, confusion_matrix, roc_auc_score,
|
| 12 |
+
precision_recall_fscore_support
|
| 13 |
+
)
|
| 14 |
+
from xgboost import XGBClassifier
|
| 15 |
+
import joblib
|
| 16 |
+
import os
|
| 17 |
+
from preprocessing import engineer_features, get_feature_columns, save_feature_schema
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def train_job_failure_model(
|
| 21 |
+
train_csv_paths: list,
|
| 22 |
+
output_dir: str = 'models',
|
| 23 |
+
test_size: float = 0.2,
|
| 24 |
+
random_state: int = 42
|
| 25 |
+
):
|
| 26 |
+
"""
|
| 27 |
+
Train XGBoost model for job failure prediction.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
train_csv_paths: List of CSV file paths to load
|
| 31 |
+
output_dir: Directory to save model artifacts
|
| 32 |
+
test_size: Fraction of data to use for testing (time-based split)
|
| 33 |
+
random_state: Random seed
|
| 34 |
+
"""
|
| 35 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 36 |
+
|
| 37 |
+
# Load and combine data
|
| 38 |
+
print("Loading data...")
|
| 39 |
+
dfs = []
|
| 40 |
+
for csv_path in train_csv_paths:
|
| 41 |
+
if os.path.exists(csv_path):
|
| 42 |
+
df = pd.read_csv(csv_path)
|
| 43 |
+
dfs.append(df)
|
| 44 |
+
print(f" Loaded {len(df)} rows from {csv_path}")
|
| 45 |
+
else:
|
| 46 |
+
print(f" Warning: {csv_path} not found, skipping")
|
| 47 |
+
|
| 48 |
+
if not dfs:
|
| 49 |
+
raise ValueError("No data files found!")
|
| 50 |
+
|
| 51 |
+
df = pd.concat(dfs, ignore_index=True)
|
| 52 |
+
print(f"Total rows: {len(df)}")
|
| 53 |
+
|
| 54 |
+
# Engineer features
|
| 55 |
+
print("Engineering features...")
|
| 56 |
+
df = engineer_features(df)
|
| 57 |
+
|
| 58 |
+
# Diagnostic: Check class distribution over time
|
| 59 |
+
if 'job_start_time' in df.columns and df['job_start_time'].notna().any():
|
| 60 |
+
df['job_start_time'] = pd.to_datetime(df['job_start_time'], errors='coerce')
|
| 61 |
+
df_sorted = df.sort_values('job_start_time')
|
| 62 |
+
print("\nClass distribution over time (first 10% vs last 10%):")
|
| 63 |
+
first_10pct = int(len(df_sorted) * 0.1)
|
| 64 |
+
last_10pct = int(len(df_sorted) * 0.9)
|
| 65 |
+
print(f" First 10%: {df_sorted.iloc[:first_10pct]['is_failed'].value_counts().to_dict()}")
|
| 66 |
+
print(f" Last 10%: {df_sorted.iloc[last_10pct:]['is_failed'].value_counts().to_dict()}")
|
| 67 |
+
|
| 68 |
+
# Save feature schema
|
| 69 |
+
schema_path = os.path.join(output_dir, 'feature_schema.json')
|
| 70 |
+
save_feature_schema(schema_path)
|
| 71 |
+
|
| 72 |
+
# Get feature columns
|
| 73 |
+
feature_cols = get_feature_columns()
|
| 74 |
+
num_cols = feature_cols['numeric']
|
| 75 |
+
cat_cols = feature_cols['categorical']
|
| 76 |
+
|
| 77 |
+
# Prepare features and target
|
| 78 |
+
# Use current run failure as target (can be extended to future prediction)
|
| 79 |
+
df = df.sort_values(['job_nm', 'job_start_time']).reset_index(drop=True)
|
| 80 |
+
|
| 81 |
+
# Ensure all feature columns exist
|
| 82 |
+
for col in num_cols + cat_cols:
|
| 83 |
+
if col not in df.columns:
|
| 84 |
+
if col in num_cols:
|
| 85 |
+
df[col] = 0.0
|
| 86 |
+
else:
|
| 87 |
+
df[col] = ''
|
| 88 |
+
|
| 89 |
+
X = df[num_cols + cat_cols].copy()
|
| 90 |
+
y = df['is_failed'].copy()
|
| 91 |
+
|
| 92 |
+
print(f"Features: {len(num_cols)} numeric, {len(cat_cols)} categorical")
|
| 93 |
+
print(f"Target distribution (full dataset): {y.value_counts().to_dict()}")
|
| 94 |
+
|
| 95 |
+
# Diagnostic: Check status column values
|
| 96 |
+
if 'status' in df.columns:
|
| 97 |
+
print(f"Status column values: {df['status'].value_counts().to_dict()}")
|
| 98 |
+
print(f"Status column unique values: {df['status'].unique().tolist()}")
|
| 99 |
+
|
| 100 |
+
# Check if we have both classes
|
| 101 |
+
unique_classes = y.unique()
|
| 102 |
+
if len(unique_classes) < 2:
|
| 103 |
+
raise ValueError(
|
| 104 |
+
f"Dataset contains only one class: {unique_classes}. "
|
| 105 |
+
"XGBoost requires at least 2 classes for binary classification. "
|
| 106 |
+
"Please check your data - all jobs may have the same status."
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Time-based split (no shuffle)
|
| 110 |
+
cut_idx = int(len(df) * (1 - test_size))
|
| 111 |
+
X_train = X.iloc[:cut_idx]
|
| 112 |
+
X_test = X.iloc[cut_idx:]
|
| 113 |
+
y_train = y.iloc[:cut_idx]
|
| 114 |
+
y_test = y.iloc[cut_idx:]
|
| 115 |
+
|
| 116 |
+
print(f"Train size: {len(X_train)}, Test size: {len(X_test)}")
|
| 117 |
+
print(f"Train target distribution: {y_train.value_counts().to_dict()}")
|
| 118 |
+
print(f"Test target distribution: {y_test.value_counts().to_dict()}")
|
| 119 |
+
|
| 120 |
+
# Validate training set has both classes
|
| 121 |
+
train_unique_classes = y_train.unique()
|
| 122 |
+
if len(train_unique_classes) < 2:
|
| 123 |
+
print(f"\nWARNING: Training set contains only class(es): {train_unique_classes}")
|
| 124 |
+
print("This may be due to time-based split. Trying stratified approach...")
|
| 125 |
+
|
| 126 |
+
# Try stratified split instead
|
| 127 |
+
from sklearn.model_selection import train_test_split
|
| 128 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 129 |
+
X, y,
|
| 130 |
+
test_size=test_size,
|
| 131 |
+
stratify=y,
|
| 132 |
+
random_state=random_state
|
| 133 |
+
)
|
| 134 |
+
print(f"Using stratified split instead:")
|
| 135 |
+
print(f"Train size: {len(X_train)}, Test size: {len(X_test)}")
|
| 136 |
+
print(f"Train target distribution: {y_train.value_counts().to_dict()}")
|
| 137 |
+
print(f"Test target distribution: {y_test.value_counts().to_dict()}")
|
| 138 |
+
|
| 139 |
+
# Final check
|
| 140 |
+
if len(y_train.unique()) < 2:
|
| 141 |
+
raise ValueError(
|
| 142 |
+
f"Even with stratified split, training set has only one class: {y_train.unique()}. "
|
| 143 |
+
"This suggests severe class imbalance. Consider collecting more diverse data."
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# Build preprocessing pipeline
|
| 147 |
+
print("Building preprocessing pipeline...")
|
| 148 |
+
preprocess = ColumnTransformer([
|
| 149 |
+
('num', Pipeline([
|
| 150 |
+
('impute', SimpleImputer(strategy='constant', fill_value=0)),
|
| 151 |
+
('scale', StandardScaler())
|
| 152 |
+
]), num_cols),
|
| 153 |
+
('cat', OneHotEncoder(handle_unknown='ignore', sparse_output=False), cat_cols)
|
| 154 |
+
])
|
| 155 |
+
|
| 156 |
+
# Build full pipeline
|
| 157 |
+
xgb = XGBClassifier(
|
| 158 |
+
tree_method='hist',
|
| 159 |
+
n_jobs=-1,
|
| 160 |
+
random_state=random_state,
|
| 161 |
+
eval_metric='logloss'
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
pipe = Pipeline([
|
| 165 |
+
('preprocess', preprocess),
|
| 166 |
+
('model', xgb)
|
| 167 |
+
])
|
| 168 |
+
|
| 169 |
+
# Train
|
| 170 |
+
print("Training model...")
|
| 171 |
+
pipe.fit(X_train, y_train)
|
| 172 |
+
|
| 173 |
+
# Evaluate
|
| 174 |
+
print("Evaluating model...")
|
| 175 |
+
try:
|
| 176 |
+
y_pred = pipe.predict(X_test)
|
| 177 |
+
y_proba = pipe.predict_proba(X_test)[:, 1]
|
| 178 |
+
except AttributeError as e:
|
| 179 |
+
if '__sklearn_tags__' in str(e):
|
| 180 |
+
# Workaround for XGBoost/scikit-learn compatibility issue
|
| 181 |
+
print("Warning: Compatibility issue detected. Using direct model prediction...")
|
| 182 |
+
# Get the preprocessed test data
|
| 183 |
+
X_test_processed = pipe.named_steps['preprocess'].transform(X_test)
|
| 184 |
+
# Use the model directly
|
| 185 |
+
xgb_model = pipe.named_steps['model']
|
| 186 |
+
y_pred = xgb_model.predict(X_test_processed)
|
| 187 |
+
y_proba = xgb_model.predict_proba(X_test_processed)[:, 1]
|
| 188 |
+
else:
|
| 189 |
+
raise
|
| 190 |
+
|
| 191 |
+
print("\n=== Classification Report ===")
|
| 192 |
+
print(classification_report(y_test, y_pred))
|
| 193 |
+
|
| 194 |
+
print("\n=== Confusion Matrix ===")
|
| 195 |
+
print(confusion_matrix(y_test, y_pred))
|
| 196 |
+
|
| 197 |
+
if len(np.unique(y_test)) > 1:
|
| 198 |
+
auc = roc_auc_score(y_test, y_proba)
|
| 199 |
+
print(f"\nAUC-ROC: {auc:.4f}")
|
| 200 |
+
|
| 201 |
+
precision, recall, f1, _ = precision_recall_fscore_support(
|
| 202 |
+
y_test, y_pred, average='binary', zero_division=0
|
| 203 |
+
)
|
| 204 |
+
print(f"Precision: {precision:.4f}, Recall: {recall:.4f}, F1: {f1:.4f}")
|
| 205 |
+
|
| 206 |
+
# Save model
|
| 207 |
+
model_path = os.path.join(output_dir, 'job_fail_pipeline_cpu.joblib')
|
| 208 |
+
joblib.dump(pipe, model_path)
|
| 209 |
+
print(f"\nModel saved to {model_path}")
|
| 210 |
+
|
| 211 |
+
# Save background sample for SHAP (small sample for efficiency)
|
| 212 |
+
X_train_sample = X_train.sample(min(100, len(X_train)), random_state=random_state)
|
| 213 |
+
X_train_encoded = preprocess.transform(X_train_sample)
|
| 214 |
+
background_path = os.path.join(output_dir, 'shap_background.npy')
|
| 215 |
+
np.save(background_path, X_train_encoded)
|
| 216 |
+
print(f"SHAP background sample saved to {background_path}")
|
| 217 |
+
|
| 218 |
+
return pipe, X_test, y_test
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
if __name__ == '__main__':
|
| 222 |
+
import sys
|
| 223 |
+
import glob
|
| 224 |
+
|
| 225 |
+
# Default CSV paths (can be overridden via command line)
|
| 226 |
+
if len(sys.argv) > 1:
|
| 227 |
+
# Handle glob patterns (works in both PowerShell and bash)
|
| 228 |
+
csv_paths = []
|
| 229 |
+
for arg in sys.argv[1:]:
|
| 230 |
+
# Expand glob patterns
|
| 231 |
+
expanded = glob.glob(arg)
|
| 232 |
+
if expanded:
|
| 233 |
+
csv_paths.extend(expanded)
|
| 234 |
+
else:
|
| 235 |
+
# If no match, use as-is (might be a specific file)
|
| 236 |
+
csv_paths.append(arg)
|
| 237 |
+
else:
|
| 238 |
+
# Default: all CSV files in data directory
|
| 239 |
+
csv_paths = glob.glob('data/*.csv')
|
| 240 |
+
if not csv_paths:
|
| 241 |
+
csv_paths = [
|
| 242 |
+
'data/true_export_report_20260120.csv',
|
| 243 |
+
'data/true_export_report_20260121.csv'
|
| 244 |
+
]
|
| 245 |
+
|
| 246 |
+
if not csv_paths:
|
| 247 |
+
print("Error: No CSV files found!")
|
| 248 |
+
print("Usage: python train_job_failure.py [file1.csv] [file2.csv] ...")
|
| 249 |
+
print(" or: python train_job_failure.py data/*.csv")
|
| 250 |
+
sys.exit(1)
|
| 251 |
+
|
| 252 |
+
print(f"Training with {len(csv_paths)} file(s):")
|
| 253 |
+
for path in csv_paths:
|
| 254 |
+
print(f" - {path}")
|
| 255 |
+
print()
|
| 256 |
+
|
| 257 |
+
train_job_failure_model(csv_paths)
|