Adisri99 commited on
Commit
e8cca46
·
verified ·
1 Parent(s): 41a5cfa

Upload 25 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ ENV PYTHONDONTWRITEBYTECODE=1
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV PORT=7860
5
+ WORKDIR /app
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+ COPY . .
9
+ EXPOSE 7860
10
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Space Risk Intelligence API
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Space Risk Intelligence API
app/__init__.py ADDED
File without changes
app/analytics.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ from app.repository import list_high_risk_pairs, latest_runs
3
+
4
+ def shell_analytics(db, limit: int = 10):
5
+ rows = list_high_risk_pairs(db, limit=500)
6
+ buckets = defaultdict(lambda: {"shell_key": "", "pair_count": 0, "avg_final_score": 0.0, "max_final_score": 0.0, "high_or_critical": 0})
7
+ for r in rows:
8
+ key = r.shell_key or "unknown"
9
+ b = buckets[key]
10
+ b["shell_key"] = key
11
+ b["pair_count"] += 1
12
+ b["avg_final_score"] += r.final_score
13
+ b["max_final_score"] = max(b["max_final_score"], r.final_score)
14
+ if r.risk_label in ("high", "critical"):
15
+ b["high_or_critical"] += 1
16
+ out = []
17
+ for b in buckets.values():
18
+ b["avg_final_score"] = b["avg_final_score"] / max(1, b["pair_count"])
19
+ out.append(b)
20
+ out.sort(key=lambda x: x["avg_final_score"], reverse=True)
21
+ return out[:limit]
22
+
23
+ def diagnostics_summary(db):
24
+ rows = list_high_risk_pairs(db, limit=500)
25
+ runs = latest_runs(db, limit=10)
26
+ labels = {"low": 0, "medium": 0, "high": 0, "critical": 0}
27
+ if not rows:
28
+ return {
29
+ "pair_count": 0,
30
+ "avg_final_score": 0.0,
31
+ "max_final_score": 0.0,
32
+ "high_plus_critical": 0,
33
+ "label_distribution": labels,
34
+ "recent_runs": len(runs),
35
+ }
36
+ scores = [r.final_score for r in rows]
37
+ for r in rows:
38
+ labels[r.risk_label] = labels.get(r.risk_label, 0) + 1
39
+ return {
40
+ "pair_count": len(rows),
41
+ "avg_final_score": sum(scores) / len(scores),
42
+ "max_final_score": max(scores),
43
+ "high_plus_critical": labels["high"] + labels["critical"],
44
+ "label_distribution": labels,
45
+ "recent_runs": len(runs),
46
+ }
app/config.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+ class Settings(BaseSettings):
4
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
5
+ APP_NAME: str = "Space Risk Intelligence API"
6
+ APP_ENV: str = "dev"
7
+ DATABASE_URL: str = "sqlite:///./space_risk.db"
8
+ CELESTRAK_URL: str = "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=json"
9
+ ALLOWED_ORIGINS: str = "http://localhost:5173,http://localhost:3000"
10
+ TOP_K_ALERTS: int = 25
11
+ MAX_OBJECTS_PER_RUN: int = 600
12
+ MAX_CANDIDATE_PAIRS: int = 2500
13
+
14
+ settings = Settings()
app/database.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import DeclarativeBase, sessionmaker
3
+ from app.config import settings
4
+
5
+ connect_args = {"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {}
6
+ engine = create_engine(settings.DATABASE_URL, connect_args=connect_args)
7
+ SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
8
+
9
+ class Base(DeclarativeBase):
10
+ pass
11
+
12
+ def get_db():
13
+ db = SessionLocal()
14
+ try:
15
+ yield db
16
+ finally:
17
+ db.close()
app/explanations.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def build_top_factors(features, anomaly_score, final_score):
2
+ factors = []
3
+ if features.get("close_approach_proxy", 0) > 0.5:
4
+ factors.append("small orbital separation proxy")
5
+ if features.get("same_shell", 0) >= 1:
6
+ factors.append("same orbital shell")
7
+ if features.get("graph_local_density", 0) > 0.2:
8
+ factors.append("dense interaction neighborhood")
9
+ if features.get("recurrence_count", 0) >= 3:
10
+ factors.append("repeated appearance across scoring windows")
11
+ if features.get("trend_delta_score", 0) > 0.1:
12
+ factors.append("risk trend increasing over time")
13
+ if anomaly_score > 0.6:
14
+ factors.append("unusual conjunction pattern")
15
+ if final_score > 0.9:
16
+ factors.append("high blended system score")
17
+ return factors[:5] or ["general risk elevation from orbital similarity"]
18
+
19
+ def analyst_summary(features, top_factors, final_score):
20
+ text = "This pair is prioritized because " + ", ".join(top_factors[:3]) + "." if top_factors else "This pair is prioritized because multiple similarity signals are elevated."
21
+ if features.get("recurrence_count", 0) >= 3:
22
+ text += " The pair has appeared repeatedly in recent scoring windows."
23
+ if features.get("graph_local_density", 0) > 0.2:
24
+ text += " The surrounding interaction neighborhood is congested."
25
+ if final_score > 0.9:
26
+ text += " This pair should be reviewed first."
27
+ return text
28
+
29
+ def structured_explanation(features, top_factors, final_score, action):
30
+ return {
31
+ "why_risky": top_factors[:3],
32
+ "what_changed": "Risk increased recently." if features.get("trend_delta_score", 0) > 0.1 else "No major recent change detected.",
33
+ "context": "Pair sits in a dense local interaction neighborhood." if features.get("graph_local_density", 0) > 0.2 else "Pair context is not highly congested.",
34
+ "recommended_action": action,
35
+ "final_score": final_score,
36
+ }
37
+
38
+ def recommended_action(label):
39
+ return {
40
+ "critical": "immediate analyst review",
41
+ "high": "prioritize analyst review",
42
+ "medium": "monitor and rescore on next cycle",
43
+ }.get(label, "low priority monitoring")
app/feature_engineering.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.utils import safe_float, safe_int
2
+
3
+ FEATURE_COLUMNS = [
4
+ "delta_mean_motion","delta_inclination","delta_eccentricity","delta_raan","delta_bstar",
5
+ "launch_year_gap","same_object_type","same_shell","shell_density_proxy","close_approach_proxy",
6
+ "persistence_proxy","recurrence_count","trend_delta_score","score_volatility_proxy",
7
+ "graph_degree_sum","graph_common_neighbors","graph_jaccard","graph_local_density",
8
+ ]
9
+
10
+ FEATURE_LABELS = {
11
+ "delta_mean_motion": "Mean motion difference",
12
+ "delta_inclination": "Inclination difference",
13
+ "delta_eccentricity": "Eccentricity difference",
14
+ "delta_raan": "RAAN difference",
15
+ "delta_bstar": "BSTAR difference",
16
+ "launch_year_gap": "Launch year gap",
17
+ "same_object_type": "Same object type",
18
+ "same_shell": "Same orbital shell",
19
+ "shell_density_proxy": "Shell density proxy",
20
+ "close_approach_proxy": "Close approach proxy",
21
+ "persistence_proxy": "Persistence proxy",
22
+ "recurrence_count": "Recurrence count",
23
+ "trend_delta_score": "Trend delta",
24
+ "score_volatility_proxy": "Score volatility",
25
+ "graph_degree_sum": "Graph degree sum",
26
+ "graph_common_neighbors": "Common neighbors",
27
+ "graph_jaccard": "Graph jaccard",
28
+ "graph_local_density": "Graph local density",
29
+ }
30
+
31
+ def normalize_object(raw):
32
+ name = raw.get("OBJECT_NAME") or raw.get("object_name") or "UNKNOWN"
33
+ norad = raw.get("NORAD_CAT_ID") or raw.get("norad_cat_id")
34
+ intl = raw.get("OBJECT_ID") or raw.get("object_id") or ""
35
+ launch_year = int(intl[:4]) if len(intl) >= 4 and intl[:4].isdigit() else None
36
+ return {
37
+ "object_id": str(norad or name),
38
+ "norad_cat_id": safe_int(norad, 0) or None,
39
+ "object_name": name,
40
+ "object_type": raw.get("OBJECT_TYPE") or raw.get("object_type"),
41
+ "mean_motion": safe_float(raw.get("MEAN_MOTION") or raw.get("mean_motion")),
42
+ "inclination": safe_float(raw.get("INCLINATION") or raw.get("inclination")),
43
+ "eccentricity": safe_float(raw.get("ECCENTRICITY") or raw.get("eccentricity")),
44
+ "raan": safe_float(raw.get("RA_OF_ASC_NODE") or raw.get("raan")),
45
+ "bstar": safe_float(raw.get("BSTAR") or raw.get("bstar")),
46
+ "launch_year": launch_year,
47
+ }
48
+
49
+ def orbital_shell_key(obj):
50
+ mm = safe_float(obj.get("mean_motion"))
51
+ inc = safe_float(obj.get("inclination"))
52
+ ecc = safe_float(obj.get("eccentricity"))
53
+ return f"mm:{int(mm)}|inc:{int(inc//5)*5}|ecc:{int(ecc*1000)//10}"
54
+
55
+ def base_pair_features(a, b):
56
+ mm1, mm2 = safe_float(a.get("mean_motion")), safe_float(b.get("mean_motion"))
57
+ inc1, inc2 = safe_float(a.get("inclination")), safe_float(b.get("inclination"))
58
+ ecc1, ecc2 = safe_float(a.get("eccentricity")), safe_float(b.get("eccentricity"))
59
+ raan1, raan2 = safe_float(a.get("raan")), safe_float(b.get("raan"))
60
+ b1, b2 = safe_float(a.get("bstar")), safe_float(b.get("bstar"))
61
+ ly1, ly2 = safe_int(a.get("launch_year")), safe_int(b.get("launch_year"))
62
+ same_type = 1 if (a.get("object_type") or "") == (b.get("object_type") or "") else 0
63
+ same_shell = 1 if orbital_shell_key(a) == orbital_shell_key(b) else 0
64
+ delta_mm = abs(mm1 - mm2)
65
+ delta_inc = abs(inc1 - inc2)
66
+ delta_ecc = abs(ecc1 - ecc2)
67
+ delta_raan = abs(raan1 - raan2)
68
+ delta_bstar = abs(b1 - b2)
69
+ launch_gap = abs(ly1 - ly2) if ly1 and ly2 else 25
70
+ shell_density_proxy = max(0.0, 10.0 - delta_mm) + max(0.0, 8.0 - delta_inc / 2.0)
71
+ close_approach_proxy = 1.0 / (1.0 + delta_mm + delta_inc / 10.0 + delta_ecc * 50.0 + delta_raan / 60.0)
72
+ persistence_proxy = 1.0 if same_shell else 0.25
73
+ return {
74
+ "delta_mean_motion": delta_mm,
75
+ "delta_inclination": delta_inc,
76
+ "delta_eccentricity": delta_ecc,
77
+ "delta_raan": delta_raan,
78
+ "delta_bstar": delta_bstar,
79
+ "launch_year_gap": float(launch_gap),
80
+ "same_object_type": float(same_type),
81
+ "same_shell": float(same_shell),
82
+ "shell_density_proxy": float(shell_density_proxy),
83
+ "close_approach_proxy": float(close_approach_proxy),
84
+ "persistence_proxy": float(persistence_proxy),
85
+ }
86
+
87
+ def combine_features(a, b, trend, graph):
88
+ return {**base_pair_features(a, b), **trend, **graph}
app/graph_features.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import networkx as nx
2
+
3
+ def build_graph(candidate_pairs):
4
+ g = nx.Graph()
5
+ for a, b in candidate_pairs:
6
+ g.add_edge(a, b)
7
+ return g
8
+
9
+ def pair_graph_features(g, a, b):
10
+ degree_sum = float(g.degree(a) + g.degree(b))
11
+ common = len(list(nx.common_neighbors(g, a, b))) if a in g and b in g else 0
12
+ na = set(g.neighbors(a)) if a in g else set()
13
+ nb = set(g.neighbors(b)) if b in g else set()
14
+ union = len(na | nb)
15
+ inter = len(na & nb)
16
+ jaccard = float(inter / union) if union else 0.0
17
+ nodes = set([a, b]) | na | nb
18
+ sub = g.subgraph(nodes)
19
+ possible = max(1, len(nodes) * (len(nodes) - 1) / 2)
20
+ density = float(sub.number_of_edges() / possible)
21
+ return {
22
+ "graph_degree_sum": degree_sum,
23
+ "graph_common_neighbors": float(common),
24
+ "graph_jaccard": jaccard,
25
+ "graph_local_density": density,
26
+ }
app/main.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.config import settings
4
+ from app.database import Base, engine
5
+ from app.routers.health import router as health_router
6
+ from app.routers.objects import router as objects_router
7
+ from app.routers.pairs import router as pairs_router
8
+ from app.routers.admin import router as admin_router
9
+ from app.routers.analytics import router as analytics_router
10
+ from app.scripts.bootstrap_demo import bootstrap_if_needed
11
+
12
+ Base.metadata.create_all(bind=engine)
13
+ bootstrap_if_needed()
14
+
15
+ app = FastAPI(title=settings.APP_NAME, docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json")
16
+ origins = [x.strip() for x in settings.ALLOWED_ORIGINS.split(",") if x.strip()]
17
+ app.add_middleware(CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
18
+
19
+ @app.get("/")
20
+ def root():
21
+ return {"status": "ok", "message": "Space Risk Intelligence API is running", "docs": "/docs", "health": "/health"}
22
+
23
+ app.include_router(health_router)
24
+ app.include_router(objects_router)
25
+ app.include_router(pairs_router)
26
+ app.include_router(admin_router)
27
+ app.include_router(analytics_router)
app/ml.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ import joblib
4
+ import numpy as np
5
+ from sklearn.ensemble import IsolationForest
6
+ from xgboost import XGBClassifier
7
+ from app.feature_engineering import FEATURE_COLUMNS, FEATURE_LABELS
8
+
9
+ BASE_DIR = Path(__file__).resolve().parents[1]
10
+ MODEL_DIR = BASE_DIR / "models"
11
+ MODEL_DIR.mkdir(exist_ok=True)
12
+ BASELINE_PATH = MODEL_DIR / "baseline_model.joblib"
13
+ ANOMALY_PATH = MODEL_DIR / "anomaly_model.joblib"
14
+ FEATURE_COLUMNS_PATH = MODEL_DIR / "feature_columns.json"
15
+
16
+ def train_models(X, y):
17
+ clf = XGBClassifier(
18
+ n_estimators=140, max_depth=5, learning_rate=0.06,
19
+ subsample=0.9, colsample_bytree=0.9, eval_metric="logloss",
20
+ random_state=42
21
+ )
22
+ clf.fit(X, y)
23
+ anomaly = IsolationForest(n_estimators=180, contamination=0.08, random_state=42)
24
+ anomaly.fit(X)
25
+ joblib.dump(clf, BASELINE_PATH)
26
+ joblib.dump(anomaly, ANOMALY_PATH)
27
+ FEATURE_COLUMNS_PATH.write_text(json.dumps(FEATURE_COLUMNS), encoding="utf-8")
28
+ return str(BASELINE_PATH)
29
+
30
+ def predict_local(feature_vector):
31
+ clf = joblib.load(BASELINE_PATH)
32
+ anomaly = joblib.load(ANOMALY_PATH)
33
+ x = np.array([feature_vector])
34
+ risk = float(clf.predict_proba(x)[0][1])
35
+ raw = float(anomaly.decision_function(x)[0])
36
+ anomaly_score = float(max(0.0, min(1.0, 1.0 - ((raw + 0.5) / 1.0))))
37
+ final = 0.72 * risk + 0.18 * anomaly_score + 0.10 * min(1.0, feature_vector[10] if len(feature_vector) > 10 else 0.0)
38
+ return risk, anomaly_score, float(max(0.0, min(1.0, final)))
39
+
40
+ def feature_importance():
41
+ clf = joblib.load(BASELINE_PATH)
42
+ vals = clf.feature_importances_.tolist()
43
+ out = []
44
+ for name, value in sorted(zip(FEATURE_COLUMNS, vals), key=lambda x: x[1], reverse=True):
45
+ out.append({"feature": name, "label": FEATURE_LABELS.get(name, name), "importance": float(value)})
46
+ return out
app/models.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from sqlalchemy import String, Float, Integer, DateTime, Text, Boolean
3
+ from sqlalchemy.orm import Mapped, mapped_column
4
+ from app.database import Base
5
+
6
+ class SpaceObject(Base):
7
+ __tablename__ = "space_objects"
8
+ object_id: Mapped[str] = mapped_column(String(64), primary_key=True)
9
+ norad_cat_id: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True)
10
+ object_name: Mapped[str] = mapped_column(String(255), index=True)
11
+ object_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
12
+ mean_motion: Mapped[float | None] = mapped_column(Float, nullable=True)
13
+ inclination: Mapped[float | None] = mapped_column(Float, nullable=True)
14
+ eccentricity: Mapped[float | None] = mapped_column(Float, nullable=True)
15
+ raan: Mapped[float | None] = mapped_column(Float, nullable=True)
16
+ bstar: Mapped[float | None] = mapped_column(Float, nullable=True)
17
+ launch_year: Mapped[int | None] = mapped_column(Integer, nullable=True)
18
+ inserted_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
19
+
20
+ class PairScore(Base):
21
+ __tablename__ = "pair_scores"
22
+ pair_id: Mapped[str] = mapped_column(String(128), primary_key=True)
23
+ primary_object_id: Mapped[str] = mapped_column(String(64), index=True)
24
+ secondary_object_id: Mapped[str] = mapped_column(String(64), index=True)
25
+ latest_run_id: Mapped[str] = mapped_column(String(64), index=True)
26
+ risk_score: Mapped[float] = mapped_column(Float, index=True)
27
+ anomaly_score: Mapped[float] = mapped_column(Float, index=True)
28
+ final_score: Mapped[float] = mapped_column(Float, index=True)
29
+ risk_label: Mapped[str] = mapped_column(String(32), index=True)
30
+ recurrence_count: Mapped[int] = mapped_column(Integer, default=1)
31
+ trend_delta_24h: Mapped[float | None] = mapped_column(Float, nullable=True)
32
+ shell_key: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
33
+ top_factors_json: Mapped[str] = mapped_column(Text)
34
+ feature_payload_json: Mapped[str] = mapped_column(Text)
35
+ updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
36
+
37
+ class PairScoreHistory(Base):
38
+ __tablename__ = "pair_score_history"
39
+ history_id: Mapped[str] = mapped_column(String(128), primary_key=True)
40
+ pair_id: Mapped[str] = mapped_column(String(128), index=True)
41
+ run_id: Mapped[str] = mapped_column(String(64), index=True)
42
+ risk_score: Mapped[float] = mapped_column(Float)
43
+ anomaly_score: Mapped[float] = mapped_column(Float)
44
+ final_score: Mapped[float] = mapped_column(Float)
45
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
46
+
47
+ class ScoringRun(Base):
48
+ __tablename__ = "scoring_runs"
49
+ run_id: Mapped[str] = mapped_column(String(64), primary_key=True)
50
+ source: Mapped[str] = mapped_column(String(64))
51
+ object_count: Mapped[int] = mapped_column(Integer, default=0)
52
+ candidate_pair_count: Mapped[int] = mapped_column(Integer, default=0)
53
+ scored_pair_count: Mapped[int] = mapped_column(Integer, default=0)
54
+ completed: Mapped[bool] = mapped_column(Boolean, default=False)
55
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
app/repository.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy.orm import Session
2
+ from sqlalchemy import select, desc, or_
3
+ from app.models import SpaceObject, PairScore, PairScoreHistory, ScoringRun
4
+
5
+ def upsert_space_object(db: Session, payload):
6
+ obj = db.get(SpaceObject, payload["object_id"])
7
+ if obj:
8
+ for k, v in payload.items():
9
+ setattr(obj, k, v)
10
+ db.add(obj)
11
+ else:
12
+ db.add(SpaceObject(**payload))
13
+
14
+ def list_objects(db: Session, limit=100):
15
+ return db.scalars(select(SpaceObject).limit(limit)).all()
16
+
17
+ def get_object(db: Session, object_id):
18
+ return db.get(SpaceObject, object_id)
19
+
20
+ def save_pair_score(db: Session, payload):
21
+ row = db.get(PairScore, payload["pair_id"])
22
+ if row:
23
+ for k, v in payload.items():
24
+ setattr(row, k, v)
25
+ db.add(row)
26
+ else:
27
+ db.add(PairScore(**payload))
28
+
29
+ def insert_pair_history(db: Session, payload):
30
+ db.add(PairScoreHistory(**payload))
31
+
32
+ def list_high_risk_pairs(db: Session, limit=50):
33
+ return db.scalars(select(PairScore).order_by(desc(PairScore.final_score)).limit(limit)).all()
34
+
35
+ def get_pair(db: Session, pair_id):
36
+ return db.get(PairScore, pair_id)
37
+
38
+ def get_pair_history(db: Session, pair_id, limit=20):
39
+ return db.scalars(select(PairScoreHistory).where(PairScoreHistory.pair_id == pair_id).order_by(desc(PairScoreHistory.created_at)).limit(limit)).all()
40
+
41
+ def create_run(db: Session, payload):
42
+ db.add(ScoringRun(**payload))
43
+
44
+ def latest_runs(db: Session, limit=10):
45
+ return db.scalars(select(ScoringRun).order_by(desc(ScoringRun.created_at)).limit(limit)).all()
46
+
47
+ def get_run(db: Session, run_id):
48
+ return db.get(ScoringRun, run_id)
49
+
50
+ def object_pairs(db: Session, object_id: str, limit=25):
51
+ stmt = select(PairScore).where(or_(PairScore.primary_object_id == object_id, PairScore.secondary_object_id == object_id)).order_by(desc(PairScore.final_score)).limit(limit)
52
+ return db.scalars(stmt).all()
53
+
54
+ def pairs_in_same_shell(db: Session, shell_key: str, exclude_pair_id: str, limit=20):
55
+ stmt = select(PairScore).where(PairScore.shell_key == shell_key, PairScore.pair_id != exclude_pair_id).order_by(desc(PairScore.final_score)).limit(limit)
56
+ return db.scalars(stmt).all()
app/routers/__init__.py ADDED
File without changes
app/routers/admin.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import APIRouter, Depends
3
+ from app.database import get_db
4
+ from app.feature_engineering import normalize_object
5
+ from app.ingestion import fetch_celestrak_json
6
+ from app.repository import list_high_risk_pairs, latest_runs, get_run
7
+ from app.services import scoring_cycle, demo_objects
8
+ from app.scripts.train_baseline import run_training
9
+
10
+ router = APIRouter(prefix="/api/v1", tags=["admin"])
11
+
12
+ @router.post("/ingest/celestrak")
13
+ def ingest_celestrak(db=Depends(get_db)):
14
+ return {"status": "ok", **scoring_cycle(db, [normalize_object(x) for x in fetch_celestrak_json()], source="celestrak")}
15
+
16
+ @router.post("/score/demo-cycle")
17
+ def score_demo_cycle(db=Depends(get_db)):
18
+ return {"status": "ok", **scoring_cycle(db, demo_objects(db), source="demo")}
19
+
20
+ @router.post("/train/baseline")
21
+ def train_baseline():
22
+ model_path, rows, metrics = run_training()
23
+ return {"status": "ok", "model_path": model_path, "rows_used": rows, "metrics": metrics}
24
+
25
+ @router.get("/alerts/live")
26
+ def alerts_live(limit: int = 25, db=Depends(get_db)):
27
+ rows = list_high_risk_pairs(db, limit)
28
+ return [{"pair_id": r.pair_id, "final_score": r.final_score, "risk_label": r.risk_label, "top_factors": json.loads(r.top_factors_json)} for r in rows]
29
+
30
+ @router.get("/runs")
31
+ def runs(db=Depends(get_db)):
32
+ return [{"run_id": r.run_id, "source": r.source, "object_count": r.object_count, "candidate_pair_count": r.candidate_pair_count, "scored_pair_count": r.scored_pair_count, "completed": r.completed, "created_at": r.created_at} for r in latest_runs(db, 10)]
33
+
34
+ @router.get("/runs/{run_id}")
35
+ def run_detail(run_id: str, db=Depends(get_db)):
36
+ r = get_run(db, run_id)
37
+ if not r:
38
+ return {"detail": "Run not found"}
39
+ rows = [x for x in list_high_risk_pairs(db, 200) if x.latest_run_id == run_id]
40
+ labels = {"low": 0, "medium": 0, "high": 0, "critical": 0}
41
+ for x in rows:
42
+ labels[x.risk_label] = labels.get(x.risk_label, 0) + 1
43
+ return {
44
+ "run_id": r.run_id,
45
+ "source": r.source,
46
+ "object_count": r.object_count,
47
+ "candidate_pair_count": r.candidate_pair_count,
48
+ "scored_pair_count": r.scored_pair_count,
49
+ "completed": r.completed,
50
+ "created_at": r.created_at,
51
+ "label_distribution": labels,
52
+ "top_pairs": [{"pair_id": x.pair_id, "final_score": x.final_score, "risk_label": x.risk_label} for x in sorted(rows, key=lambda z: z.final_score, reverse=True)[:20]],
53
+ }
app/routers/analytics.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends
2
+ from app.database import get_db
3
+ from app.analytics import shell_analytics, diagnostics_summary
4
+ from app.ml import feature_importance
5
+
6
+ router = APIRouter(prefix="/api/v1", tags=["analytics"])
7
+
8
+ @router.get("/analytics/shells")
9
+ def get_shell_analytics(limit: int = 10, db=Depends(get_db)):
10
+ return shell_analytics(db, limit)
11
+
12
+ @router.get("/diagnostics/summary")
13
+ def get_diagnostics_summary(db=Depends(get_db)):
14
+ return diagnostics_summary(db)
15
+
16
+ @router.get("/model/feature-importance")
17
+ def get_feature_importance():
18
+ return feature_importance()
app/routers/health.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from app.config import settings
3
+
4
+ router = APIRouter()
5
+
6
+ @router.get("/health")
7
+ def health():
8
+ return {"status": "ok", "app": settings.APP_NAME}
app/routers/objects.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from app.database import get_db
4
+ from app.repository import list_objects, get_object, object_pairs
5
+
6
+ router = APIRouter(prefix="/api/v1/objects", tags=["objects"])
7
+
8
+ @router.get("")
9
+ def get_objects(limit: int = 100, db=Depends(get_db)):
10
+ return list_objects(db, limit)
11
+
12
+ @router.get("/{object_id}")
13
+ def get_object_detail(object_id: str, db=Depends(get_db)):
14
+ obj = get_object(db, object_id)
15
+ if not obj:
16
+ raise HTTPException(status_code=404, detail="Object not found")
17
+ return obj
18
+
19
+ @router.get("/{object_id}/pairs")
20
+ def get_object_related_pairs(object_id: str, limit: int = 25, db=Depends(get_db)):
21
+ rows = object_pairs(db, object_id, limit)
22
+ return [{"pair_id": r.pair_id, "final_score": r.final_score, "risk_label": r.risk_label, "top_factors": json.loads(r.top_factors_json)} for r in rows]
app/routers/pairs.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from app.database import get_db
4
+ from app.feature_engineering import normalize_object
5
+ from app.services import score_pair
6
+ from app.repository import list_high_risk_pairs, get_pair, get_pair_history, pairs_in_same_shell, object_pairs
7
+
8
+ router = APIRouter(prefix="/api/v1", tags=["pairs"])
9
+
10
+ @router.post("/score/pair")
11
+ def score_pair_route(payload: dict, db=Depends(get_db)):
12
+ return score_pair(db, normalize_object(payload["primary"]), normalize_object(payload["secondary"]))
13
+
14
+ @router.get("/pairs/high-risk")
15
+ def high_risk(limit: int = 50, db=Depends(get_db)):
16
+ rows = list_high_risk_pairs(db, limit)
17
+ return [{"pair_id": r.pair_id, "risk_score": r.risk_score, "anomaly_score": r.anomaly_score, "final_score": r.final_score, "risk_label": r.risk_label, "recurrence_count": r.recurrence_count, "trend_delta_24h": r.trend_delta_24h, "shell_key": r.shell_key, "top_factors": json.loads(r.top_factors_json)} for r in rows]
18
+
19
+ @router.get("/pairs/{pair_id}")
20
+ def pair_detail(pair_id: str, db=Depends(get_db)):
21
+ row = get_pair(db, pair_id)
22
+ if not row:
23
+ raise HTTPException(status_code=404, detail="Pair not found")
24
+ payload = json.loads(row.feature_payload_json)
25
+ return {
26
+ "pair_id": row.pair_id,
27
+ "primary_object_id": row.primary_object_id,
28
+ "secondary_object_id": row.secondary_object_id,
29
+ "risk_score": row.risk_score,
30
+ "anomaly_score": row.anomaly_score,
31
+ "final_score": row.final_score,
32
+ "risk_label": row.risk_label,
33
+ "recurrence_count": row.recurrence_count,
34
+ "trend_delta_24h": row.trend_delta_24h,
35
+ "shell_key": row.shell_key,
36
+ "top_factors": json.loads(row.top_factors_json),
37
+ "features": payload,
38
+ "analyst_summary": payload.get("analyst_summary", ""),
39
+ "structured_explanation": payload.get("structured_explanation", {}),
40
+ }
41
+
42
+ @router.get("/pairs/{pair_id}/history")
43
+ def pair_history(pair_id: str, limit: int = 20, db=Depends(get_db)):
44
+ rows = get_pair_history(db, pair_id, limit)
45
+ return [{"history_id": r.history_id, "run_id": r.run_id, "risk_score": r.risk_score, "anomaly_score": r.anomaly_score, "final_score": r.final_score, "created_at": r.created_at} for r in rows]
46
+
47
+ @router.get("/pairs/{pair_id}/neighbors")
48
+ def pair_neighbors(pair_id: str, limit: int = 20, db=Depends(get_db)):
49
+ row = get_pair(db, pair_id)
50
+ if not row:
51
+ raise HTTPException(status_code=404, detail="Pair not found")
52
+ shell_rows = pairs_in_same_shell(db, row.shell_key or "unknown", row.pair_id, limit)
53
+ object_related = object_pairs(db, row.primary_object_id, limit) + object_pairs(db, row.secondary_object_id, limit)
54
+ seen = {pair_id}
55
+ related = []
56
+ for r in shell_rows + object_related:
57
+ if r.pair_id in seen:
58
+ continue
59
+ seen.add(r.pair_id)
60
+ related.append({"pair_id": r.pair_id, "final_score": r.final_score, "risk_label": r.risk_label, "top_factors": json.loads(r.top_factors_json)})
61
+ if len(related) >= limit:
62
+ break
63
+ return related
app/scripts/bootstrap_demo.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from app.database import Base, engine, SessionLocal
3
+ from app.repository import list_objects, latest_runs
4
+ from app.scripts.seed_synthetic import main as seed_main
5
+ from app.scripts.train_baseline import run_training
6
+ from app.services import scoring_cycle, demo_objects
7
+
8
+ def bootstrap_if_needed():
9
+ Base.metadata.create_all(bind=engine)
10
+ model_path = Path(__file__).resolve().parents[2] / "models" / "baseline_model.joblib"
11
+ db = SessionLocal()
12
+ try:
13
+ has_objects = bool(list_objects(db, 5))
14
+ finally:
15
+ db.close()
16
+ if not has_objects:
17
+ seed_main()
18
+ if not model_path.exists():
19
+ run_training()
20
+ db = SessionLocal()
21
+ try:
22
+ if not latest_runs(db, 1):
23
+ scoring_cycle(db, demo_objects(db), source="bootstrap-demo")
24
+ finally:
25
+ db.close()
26
+
27
+ if __name__ == "__main__":
28
+ bootstrap_if_needed()
app/scripts/seed_synthetic.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from app.database import Base, engine, SessionLocal
3
+ from app.repository import upsert_space_object
4
+
5
+ def synthetic_objects(n=300):
6
+ items = []
7
+ for i in range(n):
8
+ shell = random.choice([(15.2, 53), (14.9, 98), (13.8, 74), (2.0, 0), (12.5, 55)])
9
+ mm, inc = shell
10
+ items.append({
11
+ "object_id": f"OBJ-{i+1}",
12
+ "norad_cat_id": 10000 + i,
13
+ "object_name": f"SIM_OBJECT_{i+1}",
14
+ "object_type": random.choice(["PAYLOAD", "DEBRIS", "ROCKET BODY"]),
15
+ "mean_motion": round(random.gauss(mm, 0.12), 5),
16
+ "inclination": round(random.gauss(inc, 1.4), 5),
17
+ "eccentricity": round(abs(random.gauss(0.001, 0.002)), 6),
18
+ "raan": round(random.uniform(0, 360), 4),
19
+ "bstar": round(random.uniform(0.00001, 0.005), 8),
20
+ "launch_year": random.randint(1998, 2025),
21
+ })
22
+ return items
23
+
24
+ def main():
25
+ Base.metadata.create_all(bind=engine)
26
+ db = SessionLocal()
27
+ try:
28
+ for item in synthetic_objects():
29
+ upsert_space_object(db, item)
30
+ db.commit()
31
+ print("Synthetic objects loaded.")
32
+ finally:
33
+ db.close()
34
+
35
+ if __name__ == "__main__":
36
+ main()
app/scripts/train_baseline.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, random
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.metrics import roc_auc_score
5
+ from app.database import SessionLocal
6
+ from app.repository import list_objects
7
+ from app.feature_engineering import FEATURE_COLUMNS, combine_features
8
+ from app.graph_features import build_graph, pair_graph_features
9
+ from app.ml import train_models
10
+
11
+ def run_training():
12
+ db = SessionLocal()
13
+ try:
14
+ objs = list_objects(db, 5000)
15
+ objects = [{"object_id": o.object_id, "object_type": o.object_type, "mean_motion": o.mean_motion, "inclination": o.inclination, "eccentricity": o.eccentricity, "raan": o.raan, "bstar": o.bstar, "launch_year": o.launch_year} for o in objs]
16
+ finally:
17
+ db.close()
18
+ pairs = [tuple(random.sample(objects, 2)) for _ in range(4000)]
19
+ g = build_graph([(a["object_id"], b["object_id"]) for a, b in pairs[:1000]])
20
+ rows, raw_scores = [], []
21
+ for a, b in pairs:
22
+ trend = {"recurrence_count": float(random.choice([0,1,2,3,4])), "trend_delta_score": float(random.uniform(-0.1, 0.3)), "score_volatility_proxy": float(random.uniform(0, 0.2))}
23
+ f = combine_features(a, b, trend, pair_graph_features(g, a["object_id"], b["object_id"]))
24
+ score = 0.30*f["close_approach_proxy"] + 0.16*f["same_shell"] + 0.10*min(1.0,f["shell_density_proxy"]/12.0) + 0.10*min(1.0,f["graph_local_density"]*2.0) + 0.09*min(1.0,f["graph_jaccard"]) + 0.10*min(1.0,f["recurrence_count"]/5.0) + 0.08*max(0.0,f["trend_delta_score"]) + np.random.normal(0,0.05)
25
+ y = 1 if score > 0.48 else 0
26
+ rows.append({**f, "label": y})
27
+ raw_scores.append(score)
28
+ df = pd.DataFrame(rows)
29
+ path = train_models(df[FEATURE_COLUMNS].values, df["label"].values)
30
+ auc = float(roc_auc_score(df["label"].values, np.array(raw_scores)))
31
+ return path, len(df), {"pseudo_auc": round(auc, 4), "rows": int(len(df))}
32
+
33
+ if __name__ == "__main__":
34
+ p, r, m = run_training()
35
+ print(json.dumps({"model_path": p, "rows": r, "metrics": m}, indent=2))
app/services.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from itertools import combinations
2
+ from app.config import settings
3
+ from app.feature_engineering import normalize_object, combine_features, orbital_shell_key, FEATURE_COLUMNS
4
+ from app.graph_features import build_graph, pair_graph_features
5
+ from app.repository import upsert_space_object, save_pair_score, insert_pair_history, create_run, get_pair_history, list_objects
6
+ from app.ml import predict_local
7
+ from app.utils import new_id, dumps
8
+ from app.explanations import build_top_factors, analyst_summary, structured_explanation, recommended_action
9
+
10
+ def demo_objects(db, limit=200):
11
+ rows = list_objects(db, limit=limit)
12
+ return [{"object_id": r.object_id, "object_name": r.object_name, "object_type": r.object_type, "mean_motion": r.mean_motion, "inclination": r.inclination, "eccentricity": r.eccentricity, "raan": r.raan, "bstar": r.bstar, "launch_year": r.launch_year} for r in rows]
13
+
14
+ def generate_candidate_pairs(objects):
15
+ grouped = {}
16
+ for obj in objects:
17
+ key = orbital_shell_key(obj)
18
+ grouped.setdefault(key, []).append(obj)
19
+ candidates = []
20
+ for group in grouped.values():
21
+ if len(group) < 2:
22
+ continue
23
+ for a, b in combinations(group[:120], 2):
24
+ candidates.append((a, b))
25
+ if len(candidates) >= settings.MAX_CANDIDATE_PAIRS:
26
+ return candidates
27
+ return candidates
28
+
29
+ def _trend_features(db, pair_id):
30
+ hist = get_pair_history(db, pair_id, limit=10)
31
+ if len(hist) < 2:
32
+ return {"recurrence_count": float(len(hist)), "trend_delta_score": 0.0, "score_volatility_proxy": 0.0}
33
+ scores = [h.final_score for h in hist]
34
+ avg = sum(scores) / len(scores)
35
+ vol = sum(abs(x - avg) for x in scores) / len(scores)
36
+ return {"recurrence_count": float(len(hist)), "trend_delta_score": float(scores[0] - scores[-1]), "score_volatility_proxy": float(vol)}
37
+
38
+ def score_pair(db, a, b, graph_feats=None):
39
+ pair_id = f"{a['object_id']}__{b['object_id']}"
40
+ trend = _trend_features(db, pair_id)
41
+ graph = graph_feats or {"graph_degree_sum": 0.0, "graph_common_neighbors": 0.0, "graph_jaccard": 0.0, "graph_local_density": 0.0}
42
+ features = combine_features(a, b, trend, graph)
43
+ vector = [float(features.get(c, 0.0)) for c in FEATURE_COLUMNS]
44
+ risk, anomaly, final = predict_local(vector)
45
+ label = "critical" if final >= 0.9 else "high" if final >= 0.75 else "medium" if final >= 0.45 else "low"
46
+ top = build_top_factors(features, anomaly, final)
47
+ action = recommended_action(label)
48
+ summary = analyst_summary(features, top, final)
49
+ structured = structured_explanation(features, top, final, action)
50
+ return {
51
+ "pair_id": pair_id,
52
+ "risk_score": risk,
53
+ "anomaly_score": anomaly,
54
+ "final_score": final,
55
+ "risk_label": label,
56
+ "top_factors": top,
57
+ "analyst_summary": summary,
58
+ "structured_explanation": structured,
59
+ "recommended_action": action,
60
+ "features": features,
61
+ }
62
+
63
+ def scoring_cycle(db, objects, source="demo"):
64
+ run_id = new_id("run")
65
+ create_run(db, {"run_id": run_id, "source": source, "object_count": len(objects), "candidate_pair_count": 0, "scored_pair_count": 0, "completed": False})
66
+ for obj in objects:
67
+ upsert_space_object(db, obj)
68
+ db.commit()
69
+ candidates = generate_candidate_pairs(objects)
70
+ graph = build_graph([(a["object_id"], b["object_id"]) for a, b in candidates])
71
+ count = 0
72
+ for a, b in candidates:
73
+ graph_feats = pair_graph_features(graph, a["object_id"], b["object_id"])
74
+ result = score_pair(db, a, b, graph_feats)
75
+ hist = get_pair_history(db, result["pair_id"], limit=20)
76
+ recurrence = len(hist) + 1
77
+ trend_delta = result["final_score"] - hist[-1].final_score if hist else 0.0
78
+ save_pair_score(db, {
79
+ "pair_id": result["pair_id"],
80
+ "primary_object_id": a["object_id"],
81
+ "secondary_object_id": b["object_id"],
82
+ "latest_run_id": run_id,
83
+ "risk_score": result["risk_score"],
84
+ "anomaly_score": result["anomaly_score"],
85
+ "final_score": result["final_score"],
86
+ "risk_label": result["risk_label"],
87
+ "recurrence_count": recurrence,
88
+ "trend_delta_24h": trend_delta,
89
+ "shell_key": orbital_shell_key(a),
90
+ "top_factors_json": dumps(result["top_factors"]),
91
+ "feature_payload_json": dumps(result["features"] | {"analyst_summary": result["analyst_summary"], "structured_explanation": result["structured_explanation"]}),
92
+ })
93
+ insert_pair_history(db, {
94
+ "history_id": new_id("hist"),
95
+ "pair_id": result["pair_id"],
96
+ "run_id": run_id,
97
+ "risk_score": result["risk_score"],
98
+ "anomaly_score": result["anomaly_score"],
99
+ "final_score": result["final_score"],
100
+ })
101
+ count += 1
102
+ from app.models import ScoringRun
103
+ run = db.get(ScoringRun, run_id)
104
+ run.candidate_pair_count = len(candidates)
105
+ run.scored_pair_count = count
106
+ run.completed = True
107
+ db.add(run)
108
+ db.commit()
109
+ return {"run_id": run_id, "object_count": len(objects), "candidate_pair_count": len(candidates), "scored_pair_count": count}
app/utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, uuid
2
+
3
+ def safe_float(v, d=0.0):
4
+ try:
5
+ return d if v in (None, "") else float(v)
6
+ except Exception:
7
+ return d
8
+
9
+ def safe_int(v, d=0):
10
+ try:
11
+ return d if v in (None, "") else int(v)
12
+ except Exception:
13
+ return d
14
+
15
+ def dumps(x):
16
+ return json.dumps(x, default=str)
17
+
18
+ def new_id(prefix):
19
+ return f"{prefix}_{uuid.uuid4().hex[:16]}"
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ pydantic-settings==2.5.2
4
+ sqlalchemy==2.0.35
5
+ requests==2.32.3
6
+ numpy==2.1.2
7
+ pandas==2.2.3
8
+ scikit-learn==1.5.2
9
+ xgboost==2.1.1
10
+ joblib==1.4.2
11
+ networkx==3.3
12
+ python-multipart==0.0.9