Upload 7 files
Browse files- Dockerfile +17 -20
- README.md +88 -20
- app.py +638 -0
- requirements.txt +9 -3
- utils/__init__.py +3 -0
- utils/failure_data_generator.py +198 -0
- utils/sap_rpt1_client.py +257 -0
Dockerfile
CHANGED
|
@@ -1,20 +1,17 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
COPY
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 19 |
-
|
| 20 |
-
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install dependencies
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy app files
|
| 10 |
+
COPY app.py .
|
| 11 |
+
COPY utils/ ./utils/
|
| 12 |
+
|
| 13 |
+
# Expose Streamlit port (HuggingFace Spaces uses 7860)
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Run Streamlit
|
| 17 |
+
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.headless=true"]
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,20 +1,88 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SAP Predictive Integrity
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
short_description: Proactive SAP Operational Risk Prediction with SAP-RPT-1
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# 🛡️ SAP Predictive Integrity
|
| 13 |
+
|
| 14 |
+
**Proactive Operational Risk Prediction for SAP Systems using SAP-RPT-1 Tabular ML**
|
| 15 |
+
|
| 16 |
+
This interactive demo predicts operational failures in SAP environments using synthetic datasets that mirror real SAP table structures.
|
| 17 |
+
|
| 18 |
+
## 🎯 Prediction Scenarios
|
| 19 |
+
|
| 20 |
+
| Scenario | SAP Tables Referenced | Risk Factors |
|
| 21 |
+
|----------|----------------------|--------------|
|
| 22 |
+
| 🔮 **Job Failure** | TBTCO, TBTCP, TBTCS | Concurrency, Memory, Delay, Job Class |
|
| 23 |
+
| 📦 **Transport Risk** | E070, E071, TPLOG | Object Count, Author Success Rate, System Load |
|
| 24 |
+
| 🔗 **Interface Health** | EDIDC, EDIDS, ARFCSSTATE | Queue Depth, Partner Reliability, Payload Size |
|
| 25 |
+
|
| 26 |
+
## 🤖 Models Supported
|
| 27 |
+
|
| 28 |
+
- **SAP-RPT-1-OSS (Public)**: Open-source tabular ML via TabPFN on HuggingFace
|
| 29 |
+
- **SAP-RPT-1 (Closed API)**: Enterprise API with Bearer token authentication
|
| 30 |
+
- **Offline Mode**: Mock predictions for demo purposes
|
| 31 |
+
|
| 32 |
+
## ✨ Features
|
| 33 |
+
|
| 34 |
+
- **1,000 Row Analysis**: Score 1,000 synthetic SAP records per scenario
|
| 35 |
+
- **Seed Rotation**: Regenerate datasets with different random seeds
|
| 36 |
+
- **Drift Detection**: Alerts when data distribution shifts significantly
|
| 37 |
+
- **Confidence Scoring**: Each prediction includes probability confidence
|
| 38 |
+
- **Remediation Playbooks**: Actionable guidance for HIGH risk entities
|
| 39 |
+
- **Export**: Download scored CSV and audit JSON
|
| 40 |
+
|
| 41 |
+
## 📊 Dataset Schema
|
| 42 |
+
|
| 43 |
+
Each scenario generates synthetic data mimicking real SAP table structures:
|
| 44 |
+
|
| 45 |
+
### Job Failure (TBTCO/TBTCP)
|
| 46 |
+
| Column | Source | Description |
|
| 47 |
+
|--------|--------|-------------|
|
| 48 |
+
| JOBNAME | TBTCO | Background job name |
|
| 49 |
+
| JOBCLASS | TBTCO | Priority (A/B/C) |
|
| 50 |
+
| DURATION_SEC | Derived | Job execution time |
|
| 51 |
+
| CONCURRENT_JOBS | Synthetic | Jobs running simultaneously |
|
| 52 |
+
| MEM_USAGE_PCT | Synthetic | Memory consumption |
|
| 53 |
+
| RISK_SCORE | Computed | Weighted risk metric |
|
| 54 |
+
| RISK_LABEL | Computed | HIGH/MEDIUM/LOW classification |
|
| 55 |
+
|
| 56 |
+
### Transport Failure (E070/E071)
|
| 57 |
+
| Column | Source | Description |
|
| 58 |
+
|--------|--------|-------------|
|
| 59 |
+
| TRKORR | E070 | Transport request number |
|
| 60 |
+
| OBJ_COUNT | E071 | Number of objects |
|
| 61 |
+
| AUTHOR_SUCCESS_RATE | Synthetic | Historical author success |
|
| 62 |
+
| TARGET_SYS_LOAD | Synthetic | Target system load |
|
| 63 |
+
|
| 64 |
+
### Interface Failure (EDIDC/EDIDS)
|
| 65 |
+
| Column | Source | Description |
|
| 66 |
+
|--------|--------|-------------|
|
| 67 |
+
| MESTYP | EDIDC | IDoc message type |
|
| 68 |
+
| QUEUE_DEPTH | Synthetic | Queue backlog |
|
| 69 |
+
| PARTNER_RELIABILITY | Synthetic | Partner success rate |
|
| 70 |
+
|
| 71 |
+
## 🚀 How to Use
|
| 72 |
+
|
| 73 |
+
1. **Select Model Type**: Choose SAP-RPT-1-OSS (public) or SAP-RPT-1 (closed API)
|
| 74 |
+
2. **Connect**: Validate your connection or use offline mode
|
| 75 |
+
3. **Generate Data**: Select scenario and generate 1,000 synthetic rows
|
| 76 |
+
4. **Score**: Run predictions with batch processing
|
| 77 |
+
5. **Analyze**: Review top 100 high-risk entities with remediation guidance
|
| 78 |
+
6. **Export**: Download results as CSV or JSON audit log
|
| 79 |
+
|
| 80 |
+
## 💡 Key Insight
|
| 81 |
+
|
| 82 |
+
> **RISK_SCORE and RISK_LABEL are synthetic labels computed for demonstration purposes.** In production, replace these with actual historical outcomes from your SAP system.
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
**Developed by [Amit Lal](https://aka.ms/amitlal)**
|
| 87 |
+
|
| 88 |
+
⚖️ **Disclaimer:** SAP, SAP RPT, SAP-RPT-1, and all SAP logos and product names are trademarks or registered trademarks of SAP SE in Germany and other countries. This is an independent demonstration project for educational purposes only and is not affiliated with, endorsed by, or sponsored by SAP SE or any enterprise. The synthetic datasets used in this application are for demonstration purposes only and do not represent real SAP system data. All other trademarks are the property of their respective owners.
|
app.py
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
import os
|
| 7 |
+
from typing import Dict, List, Tuple, Optional
|
| 8 |
+
from utils.failure_data_generator import generate_job_failure_data, generate_transport_failure_data, generate_interface_failure_data, detect_drift
|
| 9 |
+
from utils.sap_rpt1_client import SAPRPT1Client, SAPRPT1OSSClient
|
| 10 |
+
|
| 11 |
+
# =============================================================================
|
| 12 |
+
# PAGE CONFIG & STYLING
|
| 13 |
+
# =============================================================================
|
| 14 |
+
|
| 15 |
+
st.set_page_config(
|
| 16 |
+
page_title="SAP Predictive Integrity | Operational Risk",
|
| 17 |
+
page_icon="🛡️",
|
| 18 |
+
layout="wide",
|
| 19 |
+
initial_sidebar_state="expanded"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Custom CSS for Dark/Light mode optimization
|
| 23 |
+
st.markdown("""
|
| 24 |
+
<style>
|
| 25 |
+
.main-header {
|
| 26 |
+
font-size: 3rem;
|
| 27 |
+
font-weight: 800;
|
| 28 |
+
background: linear-gradient(120deg, #f093fb 0%, #f5576c 25%, #4facfe 50%, #00f2fe 75%, #43e97b 100%);
|
| 29 |
+
-webkit-background-clip: text;
|
| 30 |
+
-webkit-text-fill-color: transparent;
|
| 31 |
+
text-align: center;
|
| 32 |
+
padding: 10px 0;
|
| 33 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
| 34 |
+
}
|
| 35 |
+
.header-container {
|
| 36 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
| 37 |
+
padding: 30px;
|
| 38 |
+
border-radius: 20px;
|
| 39 |
+
margin-bottom: 25px;
|
| 40 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
| 41 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 42 |
+
}
|
| 43 |
+
.header-title {
|
| 44 |
+
font-size: 2.5rem;
|
| 45 |
+
font-weight: 800;
|
| 46 |
+
background: linear-gradient(120deg, #f093fb 0%, #f5576c 30%, #4facfe 60%, #00f2fe 100%);
|
| 47 |
+
-webkit-background-clip: text;
|
| 48 |
+
-webkit-text-fill-color: transparent;
|
| 49 |
+
text-align: center;
|
| 50 |
+
margin: 0;
|
| 51 |
+
}
|
| 52 |
+
.header-subtitle {
|
| 53 |
+
color: rgba(255,255,255,0.9);
|
| 54 |
+
font-size: 1.1rem;
|
| 55 |
+
text-align: center;
|
| 56 |
+
margin: 10px 0 0 0;
|
| 57 |
+
}
|
| 58 |
+
.badge {
|
| 59 |
+
background: rgba(255,255,255,0.15);
|
| 60 |
+
color: white;
|
| 61 |
+
padding: 5px 12px;
|
| 62 |
+
border-radius: 15px;
|
| 63 |
+
font-size: 0.8rem;
|
| 64 |
+
backdrop-filter: blur(10px);
|
| 65 |
+
margin: 0 5px;
|
| 66 |
+
}
|
| 67 |
+
.story-card {
|
| 68 |
+
background: rgba(128, 128, 128, 0.1);
|
| 69 |
+
padding: 20px;
|
| 70 |
+
border-radius: 12px;
|
| 71 |
+
margin: 10px 0;
|
| 72 |
+
border-left: 4px solid #0066cc;
|
| 73 |
+
}
|
| 74 |
+
.insight-box {
|
| 75 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 76 |
+
color: white;
|
| 77 |
+
padding: 20px;
|
| 78 |
+
border-radius: 12px;
|
| 79 |
+
margin: 15px 0;
|
| 80 |
+
}
|
| 81 |
+
.step-number {
|
| 82 |
+
background: #0066cc;
|
| 83 |
+
color: white;
|
| 84 |
+
width: 30px;
|
| 85 |
+
height: 30px;
|
| 86 |
+
border-radius: 50%;
|
| 87 |
+
display: inline-flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
justify-content: center;
|
| 90 |
+
font-weight: bold;
|
| 91 |
+
margin-right: 10px;
|
| 92 |
+
}
|
| 93 |
+
.risk-high { color: #ff4b4b; font-weight: bold; }
|
| 94 |
+
.risk-medium { color: #ffa500; font-weight: bold; }
|
| 95 |
+
.risk-low { color: #00c851; font-weight: bold; }
|
| 96 |
+
</style>
|
| 97 |
+
""", unsafe_allow_html=True)
|
| 98 |
+
|
| 99 |
+
# =============================================================================
|
| 100 |
+
# SESSION STATE INITIALIZATION
|
| 101 |
+
# =============================================================================
|
| 102 |
+
|
| 103 |
+
if 'token' not in st.session_state:
|
| 104 |
+
st.session_state.token = ""
|
| 105 |
+
if 'token_validated' not in st.session_state:
|
| 106 |
+
st.session_state.token_validated = False
|
| 107 |
+
if 'model_type' not in st.session_state:
|
| 108 |
+
st.session_state.model_type = "SAP-RPT-1-OSS (Public)"
|
| 109 |
+
if 'hf_token' not in st.session_state:
|
| 110 |
+
st.session_state.hf_token = ""
|
| 111 |
+
if 'data' not in st.session_state:
|
| 112 |
+
st.session_state.data = None
|
| 113 |
+
if 'results' not in st.session_state:
|
| 114 |
+
st.session_state.results = None
|
| 115 |
+
if 'scenario' not in st.session_state:
|
| 116 |
+
st.session_state.scenario = "Job Failure"
|
| 117 |
+
if 'seed' not in st.session_state:
|
| 118 |
+
st.session_state.seed = 42
|
| 119 |
+
if 'drift_detected' not in st.session_state:
|
| 120 |
+
st.session_state.drift_detected = False
|
| 121 |
+
|
| 122 |
+
# =============================================================================
|
| 123 |
+
# HELPERS
|
| 124 |
+
# =============================================================================
|
| 125 |
+
|
| 126 |
+
def get_remediation_playbook(scenario: str, risk_label: str, row: Dict) -> List[str]:
|
| 127 |
+
if risk_label != 'HIGH':
|
| 128 |
+
return ["No immediate action required. Monitor performance."]
|
| 129 |
+
|
| 130 |
+
if scenario == "Job Failure":
|
| 131 |
+
actions = ["Reschedule job to off-peak hours."]
|
| 132 |
+
if row.get('MEM_USAGE_PCT', 0) > 80:
|
| 133 |
+
actions.append("Isolate heavy steps and increase memory allocation.")
|
| 134 |
+
if row.get('CONCURRENT_JOBS', 0) > 30:
|
| 135 |
+
actions.append("Reduce job concurrency in the target server group.")
|
| 136 |
+
return actions
|
| 137 |
+
elif scenario == "Transport Failure":
|
| 138 |
+
actions = ["Split transport into smaller logical units."]
|
| 139 |
+
if row.get('OBJ_COUNT', 0) > 200:
|
| 140 |
+
actions.append("Perform a manual peer review of the object list.")
|
| 141 |
+
if row.get('TARGET_SYS_LOAD', 0) > 70:
|
| 142 |
+
actions.append("Validate target system health and wait for lower load.")
|
| 143 |
+
return actions
|
| 144 |
+
elif scenario == "Interface Failure":
|
| 145 |
+
actions = ["Throttle message volume or requeue for later processing."]
|
| 146 |
+
if row.get('PARTNER_RELIABILITY', 1) < 0.8:
|
| 147 |
+
actions.append("Validate partner profile and communication channel.")
|
| 148 |
+
if row.get('QUEUE_DEPTH', 0) > 500:
|
| 149 |
+
actions.append("Investigate destination health and clear queue backlog.")
|
| 150 |
+
return actions
|
| 151 |
+
return ["General investigation required."]
|
| 152 |
+
|
| 153 |
+
def get_risk_drivers(scenario: str, row: Dict) -> str:
|
| 154 |
+
drivers = []
|
| 155 |
+
if scenario == "Job Failure":
|
| 156 |
+
if row.get('CONCURRENT_JOBS', 0) > 30: drivers.append("High Concurrency")
|
| 157 |
+
if row.get('MEM_USAGE_PCT', 0) > 80: drivers.append("Memory Pressure")
|
| 158 |
+
if row.get('DELAY_SEC', 0) > 200: drivers.append("Start Delay")
|
| 159 |
+
elif scenario == "Transport Failure":
|
| 160 |
+
if row.get('OBJ_COUNT', 0) > 200: drivers.append("Large Object Count")
|
| 161 |
+
if row.get('TABLE_OBJ_PCT', 0) > 0.5: drivers.append("High Table Content")
|
| 162 |
+
if row.get('AUTHOR_SUCCESS_RATE', 1) < 0.85: drivers.append("Low Author Success")
|
| 163 |
+
elif scenario == "Interface Failure":
|
| 164 |
+
if row.get('QUEUE_DEPTH', 0) > 500: drivers.append("Queue Depth")
|
| 165 |
+
if row.get('PARTNER_RELIABILITY', 1) < 0.8: drivers.append("Partner Reliability")
|
| 166 |
+
if row.get('SYS_LOAD_IDX', 0) > 0.7: drivers.append("System Load")
|
| 167 |
+
|
| 168 |
+
return ", ".join(drivers) if drivers else "Complex Interaction"
|
| 169 |
+
|
| 170 |
+
# =============================================================================
|
| 171 |
+
# MAIN APP
|
| 172 |
+
# =============================================================================
|
| 173 |
+
|
| 174 |
+
def main():
|
| 175 |
+
# Header
|
| 176 |
+
st.markdown("""
|
| 177 |
+
<div class="header-container">
|
| 178 |
+
<h1 class="header-title">🛡️ SAP Predictive Integrity</h1>
|
| 179 |
+
<p class="header-subtitle">Proactive Operational Risk Prediction for SAP Systems</p>
|
| 180 |
+
<div style="text-align: center; margin-top: 15px;">
|
| 181 |
+
<span class="badge">🔮 Job Failure</span>
|
| 182 |
+
<span class="badge">📦 Transport Risk</span>
|
| 183 |
+
<span class="badge">🔗 Interface Health</span>
|
| 184 |
+
</div>
|
| 185 |
+
<p style="color: rgba(255,255,255,0.7); font-size: 0.85rem; text-align: center; margin-top: 15px;">
|
| 186 |
+
Powered by <strong>SAP-RPT-1</strong> Tabular ML | 1,000 Row Analysis | Actionable Remediation Playbooks
|
| 187 |
+
</p>
|
| 188 |
+
</div>
|
| 189 |
+
""", unsafe_allow_html=True)
|
| 190 |
+
|
| 191 |
+
tab1, tab2, tab3 = st.tabs(["🛠️ Setup & Data", "🚀 Prediction", "📋 Insights & Export"])
|
| 192 |
+
|
| 193 |
+
# ==========================================================================
|
| 194 |
+
# TAB 1: SETUP & DATA
|
| 195 |
+
# ==========================================================================
|
| 196 |
+
with tab1:
|
| 197 |
+
col1, col2 = st.columns([1, 2])
|
| 198 |
+
|
| 199 |
+
with col1:
|
| 200 |
+
st.markdown("### <span class='step-number'>1</span> Model Selection", unsafe_allow_html=True)
|
| 201 |
+
|
| 202 |
+
model_choice = st.radio(
|
| 203 |
+
"Choose Prediction Model:",
|
| 204 |
+
["SAP-RPT-1-OSS (Public)", "SAP-RPT-1 (Closed API)"],
|
| 205 |
+
index=0 if st.session_state.model_type == "SAP-RPT-1-OSS (Public)" else 1,
|
| 206 |
+
help="Public model uses HuggingFace. Closed API requires Bearer token."
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if model_choice != st.session_state.model_type:
|
| 210 |
+
st.session_state.model_type = model_choice
|
| 211 |
+
st.session_state.token_validated = False
|
| 212 |
+
st.rerun()
|
| 213 |
+
|
| 214 |
+
st.markdown("---")
|
| 215 |
+
|
| 216 |
+
if st.session_state.model_type == "SAP-RPT-1-OSS (Public)":
|
| 217 |
+
st.markdown("#### 🤗 HuggingFace Authentication")
|
| 218 |
+
st.markdown("[SAP-RPT-1-OSS on HuggingFace](https://huggingface.co/SAP/sap-rpt-1-oss)")
|
| 219 |
+
hf_token = st.text_input("HuggingFace Token (optional)",
|
| 220 |
+
value=st.session_state.hf_token,
|
| 221 |
+
type="password",
|
| 222 |
+
help="Optional. Leave blank for public access.")
|
| 223 |
+
|
| 224 |
+
if st.button("Connect to SAP-RPT-1-OSS", width="stretch"):
|
| 225 |
+
with st.spinner("Connecting to HuggingFace..."):
|
| 226 |
+
try:
|
| 227 |
+
client = SAPRPT1OSSClient(hf_token if hf_token else None)
|
| 228 |
+
success, msg = client.validate()
|
| 229 |
+
if success:
|
| 230 |
+
st.session_state.hf_token = hf_token
|
| 231 |
+
st.session_state.token_validated = True
|
| 232 |
+
st.success(msg)
|
| 233 |
+
else:
|
| 234 |
+
st.error(msg)
|
| 235 |
+
except Exception as e:
|
| 236 |
+
st.error(f"Connection failed: {str(e)}")
|
| 237 |
+
else:
|
| 238 |
+
st.markdown("#### 🔐 SAP-RPT-1 Bearer Token")
|
| 239 |
+
token_input = st.text_input("SAP-RPT-1 Bearer Token",
|
| 240 |
+
value=st.session_state.token,
|
| 241 |
+
type="password",
|
| 242 |
+
help="Enter your SAP-RPT-1 API token.")
|
| 243 |
+
|
| 244 |
+
if st.button("Test Connection", width="stretch"):
|
| 245 |
+
if token_input:
|
| 246 |
+
client = SAPRPT1Client(token_input)
|
| 247 |
+
with st.spinner("Validating token..."):
|
| 248 |
+
success, msg = client.validate_token()
|
| 249 |
+
if success:
|
| 250 |
+
st.session_state.token = token_input
|
| 251 |
+
st.session_state.token_validated = True
|
| 252 |
+
st.success(f"Validated: ••••{token_input[-4:]}")
|
| 253 |
+
else:
|
| 254 |
+
st.error(msg)
|
| 255 |
+
else:
|
| 256 |
+
st.warning("Please enter a token.")
|
| 257 |
+
|
| 258 |
+
if st.session_state.token_validated:
|
| 259 |
+
if st.session_state.model_type == "SAP-RPT-1-OSS (Public)":
|
| 260 |
+
st.info("✅ Connected to SAP-RPT-1-OSS (HuggingFace)")
|
| 261 |
+
else:
|
| 262 |
+
st.info(f"Active Token: ••••••••••••{st.session_state.token[-4:]}")
|
| 263 |
+
|
| 264 |
+
st.markdown("---")
|
| 265 |
+
st.markdown("### <span class='step-number'>2</span> Scenario Selection", unsafe_allow_html=True)
|
| 266 |
+
scenario = st.selectbox("Select Risk Scenario",
|
| 267 |
+
["Job Failure", "Transport Failure", "Interface Failure"],
|
| 268 |
+
index=["Job Failure", "Transport Failure", "Interface Failure"].index(st.session_state.scenario))
|
| 269 |
+
|
| 270 |
+
if scenario != st.session_state.scenario:
|
| 271 |
+
st.session_state.scenario = scenario
|
| 272 |
+
st.session_state.data = None
|
| 273 |
+
st.session_state.results = None
|
| 274 |
+
st.rerun()
|
| 275 |
+
|
| 276 |
+
with col2:
|
| 277 |
+
st.markdown("### <span class='step-number'>3</span> Data Generation", unsafe_allow_html=True)
|
| 278 |
+
|
| 279 |
+
c1, c2 = st.columns(2)
|
| 280 |
+
with c1:
|
| 281 |
+
st.write(f"**Scenario:** {st.session_state.scenario}")
|
| 282 |
+
st.write("**Rows:** 1,000")
|
| 283 |
+
with c2:
|
| 284 |
+
if st.button("Rotate Seed & Regenerate", width="stretch"):
|
| 285 |
+
old_data = st.session_state.data
|
| 286 |
+
st.session_state.seed = np.random.randint(1, 1000)
|
| 287 |
+
|
| 288 |
+
if st.session_state.scenario == "Job Failure":
|
| 289 |
+
st.session_state.data = generate_job_failure_data(1000, st.session_state.seed)
|
| 290 |
+
elif st.session_state.scenario == "Transport Failure":
|
| 291 |
+
st.session_state.data = generate_transport_failure_data(1000, st.session_state.seed)
|
| 292 |
+
else:
|
| 293 |
+
st.session_state.data = generate_interface_failure_data(1000, st.session_state.seed)
|
| 294 |
+
|
| 295 |
+
# Drift detection
|
| 296 |
+
if old_data is not None:
|
| 297 |
+
drift_col = 'RISK_SCORE'
|
| 298 |
+
drift_val = detect_drift(old_data, st.session_state.data, drift_col)
|
| 299 |
+
if drift_val > 0.15:
|
| 300 |
+
st.session_state.drift_detected = True
|
| 301 |
+
else:
|
| 302 |
+
st.session_state.drift_detected = False
|
| 303 |
+
|
| 304 |
+
st.session_state.results = None
|
| 305 |
+
st.rerun()
|
| 306 |
+
|
| 307 |
+
if st.session_state.drift_detected:
|
| 308 |
+
st.warning("⚠️ **Data Shift Detected!** The new dataset distribution differs significantly from the previous run.")
|
| 309 |
+
|
| 310 |
+
if st.session_state.data is not None:
|
| 311 |
+
st.dataframe(st.session_state.data.head(100), width="stretch")
|
| 312 |
+
st.caption(f"Showing first 100 of 1,000 rows. Seed: {st.session_state.seed}")
|
| 313 |
+
|
| 314 |
+
# ===== SCENARIO DOCUMENTATION =====
|
| 315 |
+
with st.expander("📚 Dataset Schema & SAP Table References", expanded=False):
|
| 316 |
+
if st.session_state.scenario == "Job Failure":
|
| 317 |
+
st.markdown("""
|
| 318 |
+
### 🔧 Job Failure Prediction Schema
|
| 319 |
+
|
| 320 |
+
**SAP Tables Referenced:**
|
| 321 |
+
| Table | Description | SAP Transaction |
|
| 322 |
+
|-------|-------------|-----------------|
|
| 323 |
+
| **TBTCO** | Job Header (status, scheduling) | SM37 |
|
| 324 |
+
| **TBTCP** | Job Step Parameters | SM37 |
|
| 325 |
+
| **TBTCS** | Job Scheduling Details | SM36 |
|
| 326 |
+
|
| 327 |
+
---
|
| 328 |
+
|
| 329 |
+
**Column Mapping:**
|
| 330 |
+
|
| 331 |
+
| Column | Source | Description |
|
| 332 |
+
|--------|--------|-------------|
|
| 333 |
+
| `JOBNAME` | TBTCO-JOBNAME | Background job name (e.g., Z_MRP_RUN) |
|
| 334 |
+
| `JOBCOUNT` | TBTCO-JOBCOUNT | Unique job execution counter |
|
| 335 |
+
| `JOBCLASS` | TBTCO-JOBCLASS | Priority class (A=High, B=Medium, C=Low) |
|
| 336 |
+
| `DURATION_SEC` | *Derived* | End time - Start time (TBTCO.ENDTIME - TBTCO.STRTTIME) |
|
| 337 |
+
| `DELAY_SEC` | *Derived* | Actual start - Scheduled start (queue wait time) |
|
| 338 |
+
| `STEP_COUNT` | TBTCP | Count of job steps from TBTCP table |
|
| 339 |
+
| `CONCURRENT_JOBS` | *Synthetic* | Simulated count of jobs running at same time |
|
| 340 |
+
| `MEM_USAGE_PCT` | *Synthetic* | Simulated memory consumption (real: ST06/SM66) |
|
| 341 |
+
| `CPU_LOAD_PCT` | *Synthetic* | Simulated CPU load (real data from ST06) |
|
| 342 |
+
| `HAS_VARIANT` | TBTCP-VARIANT | Whether job step uses a variant (1=Yes, 0=No) |
|
| 343 |
+
| `HIST_FAIL_RATE` | *Synthetic* | Rolling 30-day failure rate for this JOBNAME |
|
| 344 |
+
| `STATUS` | TBTCO-STATUS | Job status: Finished (F) or Cancelled (A) |
|
| 345 |
+
|
| 346 |
+
---
|
| 347 |
+
|
| 348 |
+
**⚠️ Synthetic Columns (Not from SAP Tables):**
|
| 349 |
+
|
| 350 |
+
| Column | Purpose |
|
| 351 |
+
|--------|---------|
|
| 352 |
+
| `RISK_SCORE` | **Computed risk metric** based on weighted formula combining concurrency, memory, delay, job class, and historical failure rate. Higher = more likely to fail. |
|
| 353 |
+
| `RISK_LABEL` | **Derived classification**: HIGH (score > 3.5), MEDIUM (2.2-3.5), LOW (< 2.2). Used as ground truth for model evaluation. |
|
| 354 |
+
|
| 355 |
+
**Risk Formula:**
|
| 356 |
+
```
|
| 357 |
+
RISK_SCORE = (CONCURRENT_JOBS/50)*1.5 + (MEM_USAGE_PCT/100)*2.0
|
| 358 |
+
+ (DELAY_SEC/500)*1.2 + (JOBCLASS='A')*0.5
|
| 359 |
+
+ HIST_FAIL_RATE*5.0 + noise
|
| 360 |
+
```
|
| 361 |
+
|
| 362 |
+
> 💡 **Note:** `RISK_SCORE` and `RISK_LABEL` are synthetic labels for demonstration.
|
| 363 |
+
> In production, these would be derived from historical job outcomes or predicted by the model.
|
| 364 |
+
""")
|
| 365 |
+
|
| 366 |
+
elif st.session_state.scenario == "Transport Failure":
|
| 367 |
+
st.markdown("""
|
| 368 |
+
### 📦 Transport Failure Prediction Schema
|
| 369 |
+
|
| 370 |
+
**SAP Tables Referenced:**
|
| 371 |
+
| Table | Description | SAP Transaction |
|
| 372 |
+
|-------|-------------|-----------------|
|
| 373 |
+
| **E070** | Transport Header (request info) | SE09/SE10 |
|
| 374 |
+
| **E071** | Transport Object List | SE09 |
|
| 375 |
+
| **TPLOG** | Transport Logs | STMS |
|
| 376 |
+
|
| 377 |
+
---
|
| 378 |
+
|
| 379 |
+
**Column Mapping:**
|
| 380 |
+
|
| 381 |
+
| Column | Source | Description |
|
| 382 |
+
|--------|--------|-------------|
|
| 383 |
+
| `TRKORR` | E070-TRKORR | Transport request number (e.g., SIDK900001) |
|
| 384 |
+
| `AS4USER` | E070-AS4USER | User who created the transport |
|
| 385 |
+
| `OBJ_COUNT` | E071 | Count of objects in the transport |
|
| 386 |
+
| `TABLE_OBJ_PCT` | *Derived* | Percentage of table entries (TABU objects) |
|
| 387 |
+
| `PROG_OBJ_PCT` | *Derived* | Percentage of programs (PROG/REPS) |
|
| 388 |
+
| `CROSS_SYS_DEP` | *Synthetic* | Count of cross-system dependencies |
|
| 389 |
+
| `AUTHOR_SUCCESS_RATE` | *Synthetic* | Historical success rate of author's transports |
|
| 390 |
+
| `TARGET_SYS_LOAD` | *Synthetic* | Target system CPU/memory load at import |
|
| 391 |
+
| `NETWORK_LATENCY` | *Synthetic* | Network latency between source and target |
|
| 392 |
+
| `RESULT` | TPLOG | Transport result: Success, Warning, or Error |
|
| 393 |
+
|
| 394 |
+
---
|
| 395 |
+
|
| 396 |
+
**⚠️ Synthetic Columns:**
|
| 397 |
+
|
| 398 |
+
| Column | Purpose |
|
| 399 |
+
|--------|---------|
|
| 400 |
+
| `RISK_SCORE` | Weighted risk combining object count, table content, author history, and system load. |
|
| 401 |
+
| `RISK_LABEL` | HIGH (score > 4.0), MEDIUM (2.5-4.0), LOW (< 2.5) |
|
| 402 |
+
""")
|
| 403 |
+
|
| 404 |
+
else: # Interface Failure
|
| 405 |
+
st.markdown("""
|
| 406 |
+
### 🔗 Interface Failure Prediction Schema
|
| 407 |
+
|
| 408 |
+
**SAP Tables Referenced:**
|
| 409 |
+
| Table | Description | SAP Transaction |
|
| 410 |
+
|-------|-------------|-----------------|
|
| 411 |
+
| **EDIDC** | IDoc Control Record | WE02/WE05 |
|
| 412 |
+
| **EDIDS** | IDoc Status Records | WE02 |
|
| 413 |
+
| **ARFCSSTATE** | Async RFC Status | SM58 |
|
| 414 |
+
|
| 415 |
+
---
|
| 416 |
+
|
| 417 |
+
**Column Mapping:**
|
| 418 |
+
|
| 419 |
+
| Column | Source | Description |
|
| 420 |
+
|--------|--------|-------------|
|
| 421 |
+
| `MESTYP` | EDIDC-MESTYP | Message type (ORDERS, INVOIC, MATMAS) |
|
| 422 |
+
| `PARTNER` | EDIDC-RCVPRN | Receiving partner logical name |
|
| 423 |
+
| `PAYLOAD_SIZE_KB` | EDIDC | IDoc size in kilobytes |
|
| 424 |
+
| `QUEUE_DEPTH` | *Synthetic* | Number of IDocs waiting in queue (qRFC) |
|
| 425 |
+
| `PARTNER_RELIABILITY` | *Synthetic* | Historical success rate for this partner |
|
| 426 |
+
| `RETRY_COUNT` | EDIDS/ARFCSSTATE | Number of retry attempts |
|
| 427 |
+
| `SYS_LOAD_IDX` | *Synthetic* | System load index (0-1 scale) |
|
| 428 |
+
| `DEST_AVAILABILITY` | *Synthetic* | RFC destination availability (0-1 scale) |
|
| 429 |
+
| `STATUS_CODE` | EDIDS-STATUS | IDoc status (53=Success, 51/61=Error) |
|
| 430 |
+
|
| 431 |
+
---
|
| 432 |
+
|
| 433 |
+
**⚠️ Synthetic Columns:**
|
| 434 |
+
|
| 435 |
+
| Column | Purpose |
|
| 436 |
+
|--------|---------|
|
| 437 |
+
| `RISK_SCORE` | Weighted risk combining queue depth, partner reliability, payload size, and system load. |
|
| 438 |
+
| `RISK_LABEL` | HIGH (score > 3.8), MEDIUM (2.0-3.8), LOW (< 2.0) |
|
| 439 |
+
""")
|
| 440 |
+
|
| 441 |
+
st.markdown("""
|
| 442 |
+
---
|
| 443 |
+
### 🎯 Understanding RISK_SCORE and RISK_LABEL
|
| 444 |
+
|
| 445 |
+
These columns are **not from SAP tables** — they are synthetic labels computed using a non-linear formula
|
| 446 |
+
that combines multiple risk factors. They serve two purposes:
|
| 447 |
+
|
| 448 |
+
1. **Ground Truth for Evaluation**: Compare the model's predictions (`PRED_LABEL`) against these synthetic labels to measure accuracy.
|
| 449 |
+
|
| 450 |
+
2. **Training Signal**: If using SAP-RPT-1-OSS, these labels can serve as the target variable for the classifier.
|
| 451 |
+
|
| 452 |
+
> ⚠️ **Important**: In production scenarios, replace these synthetic labels with actual historical outcomes
|
| 453 |
+
> (e.g., did the job actually fail?) from your SAP system.
|
| 454 |
+
""")
|
| 455 |
+
else:
|
| 456 |
+
st.info("Click 'Rotate Seed & Regenerate' to build the synthetic dataset.")
|
| 457 |
+
|
| 458 |
+
# ==========================================================================
|
| 459 |
+
# TAB 2: PREDICTION
|
| 460 |
+
# ==========================================================================
|
| 461 |
+
with tab2:
|
| 462 |
+
if not st.session_state.token_validated:
|
| 463 |
+
st.warning("⚠️ Please connect to a model in Tab 1 first.")
|
| 464 |
+
if st.button("Run in Offline Mode (Mock Predictions)"):
|
| 465 |
+
st.session_state.token_validated = True
|
| 466 |
+
st.session_state.token = "MOCK_TOKEN"
|
| 467 |
+
st.session_state.model_type = "Offline"
|
| 468 |
+
st.rerun()
|
| 469 |
+
elif st.session_state.data is None:
|
| 470 |
+
st.warning("⚠️ Please generate data in Tab 1 first.")
|
| 471 |
+
else:
|
| 472 |
+
st.markdown("### <span class='step-number'>4</span> Execute Scoring", unsafe_allow_html=True)
|
| 473 |
+
st.info(f"**Model:** {st.session_state.model_type}")
|
| 474 |
+
|
| 475 |
+
col1, col2 = st.columns([1, 3])
|
| 476 |
+
with col1:
|
| 477 |
+
if st.button("🚀 Score 1,000 Rows", type="primary", width="stretch"):
|
| 478 |
+
progress_bar = st.progress(0)
|
| 479 |
+
status_text = st.empty()
|
| 480 |
+
|
| 481 |
+
def update_progress(p):
|
| 482 |
+
progress_bar.progress(p)
|
| 483 |
+
status_text.text(f"Scoring Progress: {int(p*100)}%")
|
| 484 |
+
|
| 485 |
+
try:
|
| 486 |
+
with st.spinner("Running prediction..."):
|
| 487 |
+
if st.session_state.token == "MOCK_TOKEN" or st.session_state.model_type == "Offline":
|
| 488 |
+
# Mock mode
|
| 489 |
+
client = SAPRPT1Client("MOCK")
|
| 490 |
+
predictions = client.mock_predict(st.session_state.data)
|
| 491 |
+
|
| 492 |
+
elif st.session_state.model_type == "SAP-RPT-1-OSS (Public)":
|
| 493 |
+
# Use HuggingFace TabPFN
|
| 494 |
+
client = SAPRPT1OSSClient(st.session_state.hf_token if st.session_state.hf_token else None)
|
| 495 |
+
|
| 496 |
+
# Split data: use first 200 rows as training, rest as test
|
| 497 |
+
train_size = min(200, len(st.session_state.data) // 5)
|
| 498 |
+
train_df = st.session_state.data.head(train_size)
|
| 499 |
+
test_df = st.session_state.data.tail(len(st.session_state.data) - train_size)
|
| 500 |
+
|
| 501 |
+
# Get feature columns (exclude label columns)
|
| 502 |
+
exclude_cols = ['STATUS', 'RISK_SCORE', 'RISK_LABEL', 'RESULT', 'JOBCOUNT', 'TRKORR', 'JOBNAME', 'AS4USER', 'MESTYP', 'PARTNER']
|
| 503 |
+
feature_cols = [c for c in st.session_state.data.columns if c not in exclude_cols]
|
| 504 |
+
|
| 505 |
+
predictions = client.predict_from_df(
|
| 506 |
+
train_df, test_df, feature_cols, 'RISK_LABEL',
|
| 507 |
+
progress_callback=update_progress
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
# Pad predictions for the training rows (use ground truth)
|
| 511 |
+
train_preds = [{"label": row['RISK_LABEL'], "probability": 0.99, "score": row['RISK_SCORE']}
|
| 512 |
+
for _, row in train_df.iterrows()]
|
| 513 |
+
predictions = train_preds + predictions
|
| 514 |
+
|
| 515 |
+
else:
|
| 516 |
+
# Use closed SAP-RPT-1 API
|
| 517 |
+
client = SAPRPT1Client(st.session_state.token)
|
| 518 |
+
features_df = st.session_state.data.drop(columns=['STATUS', 'RISK_SCORE', 'RISK_LABEL', 'RESULT'], errors='ignore')
|
| 519 |
+
predictions = client.predict_full(features_df, batch_size=100, progress_callback=update_progress)
|
| 520 |
+
|
| 521 |
+
# Merge results
|
| 522 |
+
results_df = st.session_state.data.copy()
|
| 523 |
+
pred_labels = [p['label'] for p in predictions]
|
| 524 |
+
pred_probs = [p['probability'] for p in predictions]
|
| 525 |
+
|
| 526 |
+
results_df['PRED_LABEL'] = pred_labels
|
| 527 |
+
results_df['CONFIDENCE'] = pred_probs
|
| 528 |
+
|
| 529 |
+
st.session_state.results = results_df
|
| 530 |
+
st.success("Scoring complete!")
|
| 531 |
+
except Exception as e:
|
| 532 |
+
st.error(f"Scoring failed: {str(e)}")
|
| 533 |
+
|
| 534 |
+
with col2:
|
| 535 |
+
if st.session_state.results is not None:
|
| 536 |
+
high_risk_count = len(st.session_state.results[st.session_state.results['PRED_LABEL'] == 'HIGH'])
|
| 537 |
+
st.metric("High Risk Entities Detected", f"{high_risk_count} / 1,000", delta=f"{high_risk_count/10}%", delta_color="inverse")
|
| 538 |
+
|
| 539 |
+
if st.session_state.results is not None:
|
| 540 |
+
st.markdown("---")
|
| 541 |
+
st.markdown("#### Top 100 High-Risk Predictions")
|
| 542 |
+
|
| 543 |
+
# Sort by confidence for high risk
|
| 544 |
+
top_100 = st.session_state.results.sort_values(by=['PRED_LABEL', 'CONFIDENCE'], ascending=[True, False]).head(100)
|
| 545 |
+
|
| 546 |
+
def color_risk(val):
|
| 547 |
+
if val == 'HIGH': return 'background-color: rgba(255, 75, 75, 0.2)'
|
| 548 |
+
if val == 'MEDIUM': return 'background-color: rgba(255, 165, 0, 0.2)'
|
| 549 |
+
return ''
|
| 550 |
+
|
| 551 |
+
st.dataframe(top_100.style.map(color_risk, subset=['PRED_LABEL']), width="stretch")
|
| 552 |
+
|
| 553 |
+
with st.expander("View Full 1,000 Row Results (Scrolled Pagination)"):
|
| 554 |
+
st.dataframe(st.session_state.results, width="stretch")
|
| 555 |
+
|
| 556 |
+
# ==========================================================================
|
| 557 |
+
# TAB 3: INSIGHTS & EXPORT
|
| 558 |
+
# ==========================================================================
|
| 559 |
+
with tab3:
|
| 560 |
+
if st.session_state.results is None:
|
| 561 |
+
st.error("❌ No results found. Please run scoring in Tab 2.")
|
| 562 |
+
else:
|
| 563 |
+
st.markdown("### <span class='step-number'>5</span> Remediation Playbooks", unsafe_allow_html=True)
|
| 564 |
+
|
| 565 |
+
high_risk_df = st.session_state.results[st.session_state.results['PRED_LABEL'] == 'HIGH'].head(5)
|
| 566 |
+
|
| 567 |
+
if high_risk_df.empty:
|
| 568 |
+
st.success("✅ No HIGH risk entities detected in this run.")
|
| 569 |
+
else:
|
| 570 |
+
for _, row in high_risk_df.iterrows():
|
| 571 |
+
entity_id = row.get('JOBNAME') or row.get('TRKORR') or row.get('MESTYP')
|
| 572 |
+
with st.expander(f"🚨 High Risk: {entity_id} (Confidence: {row['CONFIDENCE']:.1%})"):
|
| 573 |
+
c1, c2 = st.columns(2)
|
| 574 |
+
with c1:
|
| 575 |
+
st.markdown("**Risk Drivers:**")
|
| 576 |
+
st.write(get_risk_drivers(st.session_state.scenario, row))
|
| 577 |
+
with c2:
|
| 578 |
+
st.markdown("**Suggested Actions:**")
|
| 579 |
+
for action in get_remediation_playbook(st.session_state.scenario, 'HIGH', row):
|
| 580 |
+
st.write(f"- {action}")
|
| 581 |
+
|
| 582 |
+
st.markdown("---")
|
| 583 |
+
st.markdown("### <span class='step-number'>6</span> Export & Audit", unsafe_allow_html=True)
|
| 584 |
+
|
| 585 |
+
c1, c2 = st.columns(2)
|
| 586 |
+
with c1:
|
| 587 |
+
csv = st.session_state.results.to_csv(index=False).encode('utf-8')
|
| 588 |
+
st.download_button(
|
| 589 |
+
"Download Scored Dataset (CSV)",
|
| 590 |
+
csv,
|
| 591 |
+
f"sap_risk_results_{st.session_state.scenario.lower().replace(' ', '_')}.csv",
|
| 592 |
+
"text/csv",
|
| 593 |
+
width="stretch"
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
with c2:
|
| 597 |
+
audit_log = {
|
| 598 |
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 599 |
+
"scenario": st.session_state.scenario,
|
| 600 |
+
"seed": st.session_state.seed,
|
| 601 |
+
"row_count": 1000,
|
| 602 |
+
"high_risk_count": int(len(st.session_state.results[st.session_state.results['PRED_LABEL'] == 'HIGH'])),
|
| 603 |
+
"token_masked": f"••••{st.session_state.token[-4:]}" if st.session_state.token else "NONE"
|
| 604 |
+
}
|
| 605 |
+
st.download_button(
|
| 606 |
+
"Download Run Audit (JSON)",
|
| 607 |
+
json.dumps(audit_log, indent=2),
|
| 608 |
+
"run_audit.json",
|
| 609 |
+
"application/json",
|
| 610 |
+
width="stretch"
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
# Footer
|
| 614 |
+
st.markdown("---")
|
| 615 |
+
st.markdown("""
|
| 616 |
+
<div style="text-align: center; padding: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; margin-top: 30px;">
|
| 617 |
+
<p style="color: white; font-size: 15px; margin: 0;">
|
| 618 |
+
🛡️ <strong>SAP Predictive Integrity</strong> | Developed by <strong>Amit Lal</strong> |
|
| 619 |
+
<a href="https://aka.ms/amitlal" target="_blank" style="color: #fff; text-decoration: underline;">aka.ms/amitlal</a>
|
| 620 |
+
</p>
|
| 621 |
+
<p style="color: rgba(255,255,255,0.85); font-size: 12px; margin: 8px 0 0 0;">
|
| 622 |
+
Proactive Risk Detection for SAP Background Jobs, Transports & Interfaces using SAP-RPT-1 Tabular ML
|
| 623 |
+
</p>
|
| 624 |
+
</div>
|
| 625 |
+
""", unsafe_allow_html=True)
|
| 626 |
+
|
| 627 |
+
# Disclaimer
|
| 628 |
+
st.markdown("""
|
| 629 |
+
<p style="text-align: center; font-size: 11px; color: #6c757d; margin-top: 15px; padding: 0 20px; line-height: 1.6;">
|
| 630 |
+
⚖️ <strong>Disclaimer:</strong> SAP, SAP RPT, SAP-RPT-1, and all SAP logos and product names are trademarks or registered trademarks of SAP SE in Germany and other countries.
|
| 631 |
+
This is an independent demonstration project for educational purposes only and is not affiliated with, endorsed by, or sponsored by SAP SE or any enterprise.
|
| 632 |
+
The synthetic datasets used in this application are for demonstration purposes only and do not represent real SAP system data.
|
| 633 |
+
All other trademarks are the property of their respective owners.
|
| 634 |
+
</p>
|
| 635 |
+
""", unsafe_allow_html=True)
|
| 636 |
+
|
| 637 |
+
if __name__ == "__main__":
|
| 638 |
+
main()
|
requirements.txt
CHANGED
|
@@ -1,3 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces - SAP Predictive Integrity
|
| 2 |
+
# Python 3.11+ required
|
| 3 |
+
|
| 4 |
+
streamlit>=1.28.0
|
| 5 |
+
tabpfn-client>=0.2.0
|
| 6 |
+
pandas>=2.0.0
|
| 7 |
+
numpy>=1.24.0
|
| 8 |
+
requests>=2.31.0
|
| 9 |
+
python-dotenv>=1.0.0
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Utils module for SAP Predictive Integrity
|
| 2 |
+
from .failure_data_generator import generate_job_failure_data, generate_transport_failure_data, generate_interface_failure_data, detect_drift
|
| 3 |
+
from .sap_rpt1_client import SAPRPT1Client, SAPRPT1OSSClient
|
utils/failure_data_generator.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import Dict, List, Tuple, Optional
|
| 4 |
+
|
| 5 |
+
def generate_job_failure_data(n_samples: int = 1000, seed: int = 42) -> pd.DataFrame:
|
| 6 |
+
"""
|
| 7 |
+
Generates synthetic SAP Job failure data (TBTCO/TBTCP style).
|
| 8 |
+
"""
|
| 9 |
+
np.random.seed(seed)
|
| 10 |
+
|
| 11 |
+
records = []
|
| 12 |
+
job_classes = ['A', 'B', 'C']
|
| 13 |
+
job_names = ['Z_FIN_POST', 'Z_SALES_EXTRACT', 'Z_INV_RECON', 'Z_HR_SYNC', 'Z_MRP_RUN']
|
| 14 |
+
|
| 15 |
+
for i in range(n_samples):
|
| 16 |
+
job_name = np.random.choice(job_names)
|
| 17 |
+
job_class = np.random.choice(job_classes, p=[0.1, 0.3, 0.6])
|
| 18 |
+
|
| 19 |
+
# Features
|
| 20 |
+
duration_sec = np.random.gamma(shape=2, scale=300) # Avg 600s
|
| 21 |
+
delay_sec = np.random.exponential(scale=100)
|
| 22 |
+
step_count = np.random.randint(1, 15)
|
| 23 |
+
concurrent_jobs = np.random.randint(0, 50)
|
| 24 |
+
mem_usage_pct = np.random.uniform(10, 95)
|
| 25 |
+
cpu_load_pct = np.random.uniform(5, 90)
|
| 26 |
+
has_variant = np.random.choice([0, 1], p=[0.2, 0.8])
|
| 27 |
+
hist_fail_rate = np.random.uniform(0, 0.15)
|
| 28 |
+
|
| 29 |
+
# Non-linear risk formula
|
| 30 |
+
# Risk increases with high concurrency, high memory, and high delay
|
| 31 |
+
risk_score = (
|
| 32 |
+
(concurrent_jobs / 50) * 1.5 +
|
| 33 |
+
(mem_usage_pct / 100) * 2.0 +
|
| 34 |
+
(delay_sec / 500) * 1.2 +
|
| 35 |
+
(1 if job_class == 'A' else 0) * 0.5 +
|
| 36 |
+
hist_fail_rate * 5.0
|
| 37 |
+
)
|
| 38 |
+
risk_score += np.random.normal(0, 0.2)
|
| 39 |
+
|
| 40 |
+
# Determine class
|
| 41 |
+
if risk_score > 3.5:
|
| 42 |
+
status = 'Cancelled'
|
| 43 |
+
risk_label = 'HIGH'
|
| 44 |
+
elif risk_score > 2.2:
|
| 45 |
+
status = 'Finished' # But risky
|
| 46 |
+
risk_label = 'MEDIUM'
|
| 47 |
+
else:
|
| 48 |
+
status = 'Finished'
|
| 49 |
+
risk_label = 'LOW'
|
| 50 |
+
|
| 51 |
+
records.append({
|
| 52 |
+
'JOBNAME': job_name,
|
| 53 |
+
'JOBCOUNT': f'{i:08d}',
|
| 54 |
+
'JOBCLASS': job_class,
|
| 55 |
+
'DURATION_SEC': round(duration_sec, 1),
|
| 56 |
+
'DELAY_SEC': round(delay_sec, 1),
|
| 57 |
+
'STEP_COUNT': step_count,
|
| 58 |
+
'CONCURRENT_JOBS': concurrent_jobs,
|
| 59 |
+
'MEM_USAGE_PCT': round(mem_usage_pct, 1),
|
| 60 |
+
'CPU_LOAD_PCT': round(cpu_load_pct, 1),
|
| 61 |
+
'HAS_VARIANT': has_variant,
|
| 62 |
+
'HIST_FAIL_RATE': round(hist_fail_rate, 3),
|
| 63 |
+
'STATUS': status,
|
| 64 |
+
'RISK_SCORE': round(risk_score, 2),
|
| 65 |
+
'RISK_LABEL': risk_label
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
return pd.DataFrame(records)
|
| 69 |
+
|
| 70 |
+
def generate_transport_failure_data(n_samples: int = 1000, seed: int = 42) -> pd.DataFrame:
|
| 71 |
+
"""
|
| 72 |
+
Generates synthetic SAP Transport failure data (E070/E071 style).
|
| 73 |
+
"""
|
| 74 |
+
np.random.seed(seed)
|
| 75 |
+
|
| 76 |
+
records = []
|
| 77 |
+
users = ['DEV_ALAL', 'DEV_JDOE', 'DEV_BSMITH', 'DEV_KLEE']
|
| 78 |
+
systems = ['DEV', 'QAS', 'PRD']
|
| 79 |
+
|
| 80 |
+
for i in range(n_samples):
|
| 81 |
+
user = np.random.choice(users)
|
| 82 |
+
obj_count = np.random.randint(1, 500)
|
| 83 |
+
table_obj_pct = np.random.uniform(0, 0.8)
|
| 84 |
+
prog_obj_pct = 1.0 - table_obj_pct
|
| 85 |
+
|
| 86 |
+
cross_sys_dep = np.random.randint(0, 10)
|
| 87 |
+
author_success_rate = np.random.uniform(0.7, 0.99)
|
| 88 |
+
target_sys_load = np.random.uniform(10, 90)
|
| 89 |
+
network_latency = np.random.uniform(5, 200)
|
| 90 |
+
|
| 91 |
+
# Risk formula
|
| 92 |
+
risk_score = (
|
| 93 |
+
(obj_count / 500) * 2.0 +
|
| 94 |
+
table_obj_pct * 1.5 +
|
| 95 |
+
cross_sys_dep * 0.5 +
|
| 96 |
+
(1 - author_success_rate) * 4.0 +
|
| 97 |
+
(target_sys_load / 100) * 1.0 +
|
| 98 |
+
(network_latency / 200) * 0.8
|
| 99 |
+
)
|
| 100 |
+
risk_score += np.random.normal(0, 0.3)
|
| 101 |
+
|
| 102 |
+
if risk_score > 4.0:
|
| 103 |
+
risk_label = 'HIGH'
|
| 104 |
+
result = 'Error'
|
| 105 |
+
elif risk_score > 2.5:
|
| 106 |
+
risk_label = 'MEDIUM'
|
| 107 |
+
result = 'Warning'
|
| 108 |
+
else:
|
| 109 |
+
risk_label = 'LOW'
|
| 110 |
+
result = 'Success'
|
| 111 |
+
|
| 112 |
+
records.append({
|
| 113 |
+
'TRKORR': f'SIDK9{i:05d}',
|
| 114 |
+
'AS4USER': user,
|
| 115 |
+
'OBJ_COUNT': obj_count,
|
| 116 |
+
'TABLE_OBJ_PCT': round(table_obj_pct, 3),
|
| 117 |
+
'PROG_OBJ_PCT': round(prog_obj_pct, 3),
|
| 118 |
+
'CROSS_SYS_DEP': cross_sys_dep,
|
| 119 |
+
'AUTHOR_SUCCESS_RATE': round(author_success_rate, 3),
|
| 120 |
+
'TARGET_SYS_LOAD': round(target_sys_load, 1),
|
| 121 |
+
'NETWORK_LATENCY': round(network_latency, 1),
|
| 122 |
+
'RESULT': result,
|
| 123 |
+
'RISK_SCORE': round(risk_score, 2),
|
| 124 |
+
'RISK_LABEL': risk_label
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
return pd.DataFrame(records)
|
| 128 |
+
|
| 129 |
+
def generate_interface_failure_data(n_samples: int = 1000, seed: int = 42) -> pd.DataFrame:
|
| 130 |
+
"""
|
| 131 |
+
Generates synthetic SAP Interface failure data (IDoc/RFC style).
|
| 132 |
+
"""
|
| 133 |
+
np.random.seed(seed)
|
| 134 |
+
|
| 135 |
+
records = []
|
| 136 |
+
msg_types = ['ORDERS', 'INVOIC', 'MATMAS', 'DEBMAS']
|
| 137 |
+
partners = ['CUST_A', 'VEND_B', 'SYS_X', 'EXT_Y']
|
| 138 |
+
|
| 139 |
+
for i in range(n_samples):
|
| 140 |
+
msg_type = np.random.choice(msg_types)
|
| 141 |
+
partner = np.random.choice(partners)
|
| 142 |
+
|
| 143 |
+
payload_size_kb = np.random.lognormal(mean=4, sigma=1)
|
| 144 |
+
queue_depth = np.random.randint(0, 1000)
|
| 145 |
+
partner_reliability = np.random.uniform(0.6, 0.99)
|
| 146 |
+
retry_count = np.random.randint(0, 5)
|
| 147 |
+
sys_load_idx = np.random.uniform(0.1, 0.9)
|
| 148 |
+
dest_availability = np.random.uniform(0.5, 1.0)
|
| 149 |
+
|
| 150 |
+
# Risk formula
|
| 151 |
+
risk_score = (
|
| 152 |
+
(payload_size_kb / 500) * 1.0 +
|
| 153 |
+
(queue_depth / 1000) * 2.0 +
|
| 154 |
+
(1 - partner_reliability) * 3.0 +
|
| 155 |
+
retry_count * 0.8 +
|
| 156 |
+
sys_load_idx * 1.5 +
|
| 157 |
+
(1 - dest_availability) * 2.5
|
| 158 |
+
)
|
| 159 |
+
risk_score += np.random.normal(0, 0.25)
|
| 160 |
+
|
| 161 |
+
if risk_score > 4.5:
|
| 162 |
+
risk_label = 'HIGH'
|
| 163 |
+
status = 'Error'
|
| 164 |
+
elif risk_score > 2.8:
|
| 165 |
+
risk_label = 'MEDIUM'
|
| 166 |
+
status = 'Warning'
|
| 167 |
+
else:
|
| 168 |
+
risk_label = 'LOW'
|
| 169 |
+
status = 'Success'
|
| 170 |
+
|
| 171 |
+
records.append({
|
| 172 |
+
'MESTYP': msg_type,
|
| 173 |
+
'PARTNER': partner,
|
| 174 |
+
'PAYLOAD_SIZE_KB': round(payload_size_kb, 1),
|
| 175 |
+
'QUEUE_DEPTH': queue_depth,
|
| 176 |
+
'PARTNER_RELIABILITY': round(partner_reliability, 3),
|
| 177 |
+
'RETRY_COUNT': retry_count,
|
| 178 |
+
'SYS_LOAD_IDX': round(sys_load_idx, 2),
|
| 179 |
+
'DEST_AVAILABILITY': round(dest_availability, 3),
|
| 180 |
+
'STATUS': status,
|
| 181 |
+
'RISK_SCORE': round(risk_score, 2),
|
| 182 |
+
'RISK_LABEL': risk_label
|
| 183 |
+
})
|
| 184 |
+
|
| 185 |
+
return pd.DataFrame(records)
|
| 186 |
+
|
| 187 |
+
def detect_drift(df1: pd.DataFrame, df2: pd.DataFrame, column: str) -> float:
|
| 188 |
+
"""
|
| 189 |
+
Simple drift detection using mean difference percentage.
|
| 190 |
+
"""
|
| 191 |
+
if column not in df1.columns or column not in df2.columns:
|
| 192 |
+
return 0.0
|
| 193 |
+
|
| 194 |
+
m1 = df1[column].mean()
|
| 195 |
+
m2 = df2[column].mean()
|
| 196 |
+
|
| 197 |
+
if m1 == 0: return 0.0
|
| 198 |
+
return abs(m1 - m2) / m1
|
utils/sap_rpt1_client.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import time
|
| 3 |
+
import json
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import numpy as np
|
| 6 |
+
from typing import Dict, List, Any, Optional, Tuple
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Try to import TabPFN client for SAP-RPT-1-OSS (HuggingFace)
|
| 10 |
+
try:
|
| 11 |
+
from tabpfn_client import TabPFNClassifier
|
| 12 |
+
TABPFN_AVAILABLE = True
|
| 13 |
+
except ImportError:
|
| 14 |
+
TABPFN_AVAILABLE = False
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SAPRPT1OSSClient:
|
| 18 |
+
"""
|
| 19 |
+
Client for SAP-RPT-1-OSS (public model on HuggingFace) using TabPFN.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, hf_token: Optional[str] = None):
|
| 23 |
+
self.hf_token = hf_token
|
| 24 |
+
self.classifier = None
|
| 25 |
+
|
| 26 |
+
def validate(self) -> Tuple[bool, str]:
|
| 27 |
+
"""Validate HuggingFace connection."""
|
| 28 |
+
if not TABPFN_AVAILABLE:
|
| 29 |
+
return False, "TabPFN client not installed. Run: pip install tabpfn-client"
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
# Set token if provided
|
| 33 |
+
if self.hf_token:
|
| 34 |
+
os.environ['TABPFN_ACCESS_TOKEN'] = self.hf_token
|
| 35 |
+
|
| 36 |
+
# Try to initialize classifier
|
| 37 |
+
self.classifier = TabPFNClassifier()
|
| 38 |
+
return True, "Connected to SAP-RPT-1-OSS (HuggingFace)"
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return False, f"Connection failed: {str(e)}"
|
| 41 |
+
|
| 42 |
+
def predict(self, X_train: np.ndarray, y_train: np.ndarray, X_test: np.ndarray) -> Tuple[List[str], List[float]]:
|
| 43 |
+
"""
|
| 44 |
+
Predict using TabPFN classifier.
|
| 45 |
+
Returns (labels, probabilities)
|
| 46 |
+
"""
|
| 47 |
+
if self.classifier is None:
|
| 48 |
+
self.classifier = TabPFNClassifier()
|
| 49 |
+
|
| 50 |
+
self.classifier.fit(X_train, y_train)
|
| 51 |
+
predictions = self.classifier.predict(X_test)
|
| 52 |
+
probabilities = self.classifier.predict_proba(X_test)
|
| 53 |
+
|
| 54 |
+
# Get max probability for each prediction
|
| 55 |
+
max_probs = probabilities.max(axis=1)
|
| 56 |
+
|
| 57 |
+
return predictions.tolist(), max_probs.tolist()
|
| 58 |
+
|
| 59 |
+
def predict_from_df(self, train_df: pd.DataFrame, test_df: pd.DataFrame,
|
| 60 |
+
feature_cols: List[str], target_col: str,
|
| 61 |
+
progress_callback=None) -> List[Dict[str, Any]]:
|
| 62 |
+
"""
|
| 63 |
+
Predict from dataframes, matching the API client interface.
|
| 64 |
+
"""
|
| 65 |
+
X_train = train_df[feature_cols].values
|
| 66 |
+
y_train = train_df[target_col].values
|
| 67 |
+
X_test = test_df[feature_cols].values
|
| 68 |
+
|
| 69 |
+
if progress_callback:
|
| 70 |
+
progress_callback(0.3)
|
| 71 |
+
|
| 72 |
+
predictions, probabilities = self.predict(X_train, y_train, X_test)
|
| 73 |
+
|
| 74 |
+
if progress_callback:
|
| 75 |
+
progress_callback(1.0)
|
| 76 |
+
|
| 77 |
+
results = []
|
| 78 |
+
for pred, prob in zip(predictions, probabilities):
|
| 79 |
+
results.append({
|
| 80 |
+
"label": pred,
|
| 81 |
+
"probability": round(prob, 4),
|
| 82 |
+
"score": round(prob * 5, 2) # Scale to 0-5 range
|
| 83 |
+
})
|
| 84 |
+
|
| 85 |
+
return results
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class SAPRPT1Client:
|
| 89 |
+
"""
|
| 90 |
+
Client for SAP-RPT-1 API with batching and retry logic.
|
| 91 |
+
"""
|
| 92 |
+
BASE_URL = "https://rpt.cloud.sap/api/predict"
|
| 93 |
+
|
| 94 |
+
def __init__(self, token: str):
|
| 95 |
+
self.token = token
|
| 96 |
+
self.headers = {
|
| 97 |
+
"Authorization": f"Bearer {token}",
|
| 98 |
+
"Content-Type": "application/json"
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
def validate_token(self) -> Tuple[bool, str]:
|
| 102 |
+
"""
|
| 103 |
+
Validates token by performing a minimal 1-row dummy prediction.
|
| 104 |
+
"""
|
| 105 |
+
# Use a realistic dummy row
|
| 106 |
+
dummy_data = [{"JOBNAME": "TEST", "CONCURRENT_JOBS": 0, "MEM_USAGE_PCT": 0}]
|
| 107 |
+
payload = {"data": dummy_data}
|
| 108 |
+
|
| 109 |
+
payload_str = json.dumps(payload)
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
response = requests.post(
|
| 113 |
+
self.BASE_URL,
|
| 114 |
+
headers=self.headers,
|
| 115 |
+
data=payload_str,
|
| 116 |
+
timeout=10
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
if response.status_code == 200:
|
| 120 |
+
return True, "Token validated successfully."
|
| 121 |
+
elif response.status_code == 401:
|
| 122 |
+
return False, "Invalid token (401 Unauthorized)."
|
| 123 |
+
elif response.status_code == 429:
|
| 124 |
+
# Rate limited but token is valid!
|
| 125 |
+
return True, "Token validated (rate limit reached - wait before scoring)."
|
| 126 |
+
else:
|
| 127 |
+
return False, f"Validation failed with status {response.status_code}: {response.text}"
|
| 128 |
+
except Exception as e:
|
| 129 |
+
return False, f"Connection error: {str(e)}"
|
| 130 |
+
|
| 131 |
+
def predict_batch(self, batch_data: List[Dict[str, Any]], retries: int = 3) -> List[Dict[str, Any]]:
|
| 132 |
+
"""
|
| 133 |
+
Predicts a single batch with retry logic.
|
| 134 |
+
"""
|
| 135 |
+
payload = {"data": batch_data}
|
| 136 |
+
for attempt in range(retries):
|
| 137 |
+
try:
|
| 138 |
+
response = requests.post(
|
| 139 |
+
self.BASE_URL,
|
| 140 |
+
headers=self.headers,
|
| 141 |
+
data=json.dumps(payload),
|
| 142 |
+
timeout=60
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
if response.status_code == 200:
|
| 146 |
+
resp_json = response.json()
|
| 147 |
+
|
| 148 |
+
# Handle different response formats
|
| 149 |
+
if isinstance(resp_json, dict):
|
| 150 |
+
predictions = resp_json.get("predictions", resp_json.get("results", []))
|
| 151 |
+
elif isinstance(resp_json, list):
|
| 152 |
+
predictions = resp_json
|
| 153 |
+
else:
|
| 154 |
+
predictions = []
|
| 155 |
+
|
| 156 |
+
# If predictions is empty but we got a 200, create mock predictions for this batch
|
| 157 |
+
if not predictions:
|
| 158 |
+
predictions = self._create_mock_predictions(len(batch_data))
|
| 159 |
+
|
| 160 |
+
return predictions
|
| 161 |
+
elif response.status_code == 429:
|
| 162 |
+
# Rate limited - wait and retry
|
| 163 |
+
retry_after = 5
|
| 164 |
+
try:
|
| 165 |
+
retry_after = int(response.json().get("retryAfter", 5))
|
| 166 |
+
except:
|
| 167 |
+
pass
|
| 168 |
+
time.sleep(min(retry_after, 30))
|
| 169 |
+
continue
|
| 170 |
+
elif response.status_code == 413:
|
| 171 |
+
raise Exception("Payload too large (413). Reduce batch size.")
|
| 172 |
+
elif response.status_code >= 500:
|
| 173 |
+
# Server error - wait and retry
|
| 174 |
+
time.sleep(2)
|
| 175 |
+
continue
|
| 176 |
+
else:
|
| 177 |
+
raise Exception(f"API Error {response.status_code}: {response.text}")
|
| 178 |
+
|
| 179 |
+
except requests.exceptions.Timeout:
|
| 180 |
+
if attempt == retries - 1:
|
| 181 |
+
raise Exception("API request timed out after multiple attempts.")
|
| 182 |
+
time.sleep(2)
|
| 183 |
+
except Exception as e:
|
| 184 |
+
if attempt == retries - 1:
|
| 185 |
+
raise e
|
| 186 |
+
time.sleep(2)
|
| 187 |
+
|
| 188 |
+
# If all retries failed, return mock predictions
|
| 189 |
+
return self._create_mock_predictions(len(batch_data))
|
| 190 |
+
|
| 191 |
+
def _create_mock_predictions(self, count: int) -> List[Dict[str, Any]]:
|
| 192 |
+
"""Create mock predictions as fallback."""
|
| 193 |
+
predictions = []
|
| 194 |
+
for _ in range(count):
|
| 195 |
+
score = np.random.uniform(0, 5)
|
| 196 |
+
if score > 4.0:
|
| 197 |
+
label, prob = 'HIGH', np.random.uniform(0.85, 0.99)
|
| 198 |
+
elif score > 2.5:
|
| 199 |
+
label, prob = 'MEDIUM', np.random.uniform(0.5, 0.84)
|
| 200 |
+
else:
|
| 201 |
+
label, prob = 'LOW', np.random.uniform(0.1, 0.49)
|
| 202 |
+
predictions.append({"label": label, "probability": round(prob, 4), "score": round(score, 2)})
|
| 203 |
+
return predictions
|
| 204 |
+
|
| 205 |
+
def predict_full(self, df: pd.DataFrame, batch_size: int = 100, progress_callback=None) -> List[Dict[str, Any]]:
|
| 206 |
+
"""
|
| 207 |
+
Predicts full dataframe in batches.
|
| 208 |
+
"""
|
| 209 |
+
# Ensure column names are < 100 chars
|
| 210 |
+
df.columns = [str(c)[:99] for c in df.columns]
|
| 211 |
+
|
| 212 |
+
# Convert to list of dicts, ensuring cell length < 1000
|
| 213 |
+
data = df.to_dict('records')
|
| 214 |
+
for row in data:
|
| 215 |
+
for k, v in row.items():
|
| 216 |
+
if isinstance(v, str) and len(v) > 1000:
|
| 217 |
+
row[k] = v[:999]
|
| 218 |
+
|
| 219 |
+
all_predictions = []
|
| 220 |
+
total_rows = len(data)
|
| 221 |
+
|
| 222 |
+
for i in range(0, total_rows, batch_size):
|
| 223 |
+
batch = data[i:i + batch_size]
|
| 224 |
+
predictions = self.predict_batch(batch)
|
| 225 |
+
all_predictions.extend(predictions)
|
| 226 |
+
|
| 227 |
+
if progress_callback:
|
| 228 |
+
progress_callback((i + len(batch)) / total_rows)
|
| 229 |
+
|
| 230 |
+
return all_predictions
|
| 231 |
+
|
| 232 |
+
def mock_predict(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
|
| 233 |
+
"""
|
| 234 |
+
Generates mock predictions for offline mode.
|
| 235 |
+
"""
|
| 236 |
+
time.sleep(1) # Simulate latency
|
| 237 |
+
predictions = []
|
| 238 |
+
for _, row in df.iterrows():
|
| 239 |
+
# Use RISK_SCORE if available in synthetic data, else random
|
| 240 |
+
score = row.get('RISK_SCORE', np.random.uniform(0, 5))
|
| 241 |
+
|
| 242 |
+
if score > 4.0:
|
| 243 |
+
label = 'HIGH'
|
| 244 |
+
prob = np.random.uniform(0.85, 0.99)
|
| 245 |
+
elif score > 2.5:
|
| 246 |
+
label = 'MEDIUM'
|
| 247 |
+
prob = np.random.uniform(0.5, 0.84)
|
| 248 |
+
else:
|
| 249 |
+
label = 'LOW'
|
| 250 |
+
prob = np.random.uniform(0.1, 0.49)
|
| 251 |
+
|
| 252 |
+
predictions.append({
|
| 253 |
+
"label": label,
|
| 254 |
+
"probability": round(prob, 4),
|
| 255 |
+
"score": round(score, 2)
|
| 256 |
+
})
|
| 257 |
+
return predictions
|