bldeaw commited on
Commit
ea2b6ec
·
1 Parent(s): 8ccd4b2

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 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, any]]] = None
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, any]]
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=8000)
 
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==0.104.1
2
- uvicorn[standard]==0.24.0
3
- pydantic==2.5.0
4
- pandas==2.1.3
5
- numpy==1.24.3
6
- scikit-learn>=1.3.0,<1.4.0
7
- xgboost>=2.1.0
8
- tensorflow==2.15.0
9
- shap==0.43.0
10
- joblib==1.3.2
11
- python-multipart==0.0.6
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)