nyvra-api / app.py
supriyachola's picture
Update app.py
a5169e7 verified
Raw
History Blame Contribute Delete
29.3 kB
"""
app.py — Nyvra Safety API v4.1
==================================
Dart contract (read from source):
GET /predict_area?latitude=&longitude=
→ { safety_score:int, danger_level:str, danger_label:int, factors:[str] }
GET /analyze_route?origin_lat=&origin_lng=&dest_lat=&dest_lng=&time=
→ { overall_score:int,
routes:[ { name, duration, distance, safety_score:int,
factors:[str], is_recommended:bool } ] }
GET /heatmap_data?latitude=&longitude=&radius_km=&step_km=
→ { points:[ { lat, lng, risk:"Low"|"Medium"|"High", score:int } ],
center_lat, center_lng }
POST /ai { message:str } → { reply:str }
GET /health
Scoring v4.1:
base = safe_p^0.55 * 100 * (1 - high_p^0.50) — weighted expected value
thresholds: >=85 Low(GREEN) 45-84 Medium(YELLOW) <45 High(RED)
time penalty (deterministic, no safe_p dependency):
8PM-4AM -30 4AM -25 5-7AM -15 6-8PM -10 else 0
Both GET and POST endpoints exist for every action endpoint
because safety_api_service.dart uses POST while heatmap_service.dart uses GET.
"""
import datetime
import logging
import math
import os
import pickle
import warnings
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Optional
import numpy as np
import pandas as pd
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from geopy.distance import geodesic
from pydantic import BaseModel
warnings.filterwarnings("ignore")
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger("nyvra")
# ── Optional deps ──────────────────────────────────────────────────────────
_groq_key = os.environ.get("GROQ_API_KEY")
try:
from groq import Groq
groq_client = Groq(api_key=_groq_key)
log.info("Groq client ready")
except Exception as _ge:
groq_client = None
log.warning("Groq unavailable: %s", _ge)
try:
import httpx as _httpx
_HTTPX_OK = True
except ImportError:
_HTTPX_OK = False
# ── App ────────────────────────────────────────────────────────────────────
app = FastAPI(title="Nyvra Safety API", version="4.1")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
return {
"app": "Nyvra Safety API",
"version": "4.1",
"status": "running",
"endpoints": [
"GET /health",
"GET /predict_area?latitude=&longitude=",
"POST /predict_area",
"GET /analyze_route?origin_lat=&origin_lng=&dest_lat=&dest_lng=",
"POST /analyze_route",
"GET /heatmap_data?latitude=&longitude=&radius_km=&step_km=",
"POST /ai"
]
}
MODELS_DIR = os.path.join(os.path.dirname(__file__), "models")
R_0P5 = 0.5 / 6371.0
R_1KM = 1.0 / 6371.0
R_2KM = 2.0 / 6371.0
# ══════════════════════════════════════════════════════════════════════════
# MODEL LOADING
# ══════════════════════════════════════════════════════════════════════════
def _load(name: str):
path = os.path.join(MODELS_DIR, f"{name}.pkl")
if not os.path.exists(path):
raise FileNotFoundError(f"'{path}' not found")
with open(path, "rb") as f:
return pickle.load(f)
def _try_load(name: str):
try:
return _load(name), True
except Exception as e:
log.warning("Could not load %s: %s", name, e)
return None, False
print("=" * 55)
print("Loading Nyvra models ...")
scaler = _load("scaler")
kmeans = _load("kmeans")
center_lat = float(_load("center_lat"))
center_lon = float(_load("center_lon"))
ball_tree = _load("ball_tree")
X_columns, _ok = _try_load("X_columns")
if not _ok:
X_columns, _ok2 = _try_load("feature_columns")
if not _ok2:
raise RuntimeError("Neither X_columns.pkl nor feature_columns.pkl found.")
N_FEATURES = scaler.n_features_in_
log.info("Scaler expects %d features, X_columns has %d", N_FEATURES, len(X_columns))
lgb_model, lgb_ok = _try_load("lgb_model")
xgb_model, xgb_ok = _try_load("xgb_model")
rf_model, rf_ok = _try_load("rf_model")
et_model, et_ok = _try_load("et_model")
if not any([lgb_ok, xgb_ok, rf_ok]):
raise RuntimeError("No usable model found (lgb/xgb/rf all missing).")
_model_pool: List[tuple] = []
if lgb_ok: _model_pool.append((lgb_model, True, "LightGBM"))
if xgb_ok: _model_pool.append((xgb_model, False, "XGBoost"))
if rf_ok: _model_pool.append((rf_model, False, "RandomForest"))
if et_ok: _model_pool.append((et_model, False, "ExtraTrees"))
ENSEMBLE_OK = len(_model_pool) >= 2
print(f"Models : {[m[2] for m in _model_pool]}")
print(f"Ensemble : {ENSEMBLE_OK}")
print(f"Features : {N_FEATURES}")
print(f"Center : ({center_lat:.4f}, {center_lon:.4f})")
print("=" * 55)
# ══════════════════════════════════════════════════════════════════════════
# SCHEMAS — field names must match Dart JSON keys exactly
# ══════════════════════════════════════════════════════════════════════════
class AreaRequest(BaseModel):
latitude: float
longitude: float
hour: Optional[int] = None
month: Optional[int] = None
is_weekend: Optional[int] = None
class RouteRequest(BaseModel):
origin_lat: float
origin_lng: float
dest_lat: float
dest_lng: float
time: Optional[str] = "Now"
class AreaResponse(BaseModel):
safety_score: int # main.dart: _areaData['safety_score']
danger_level: str # main.dart: _areaData['danger_level'] "Low"|"Medium"|"High"
danger_label: int # main.dart: _areaData['danger_label'] 0|1|2
factors: List[str] # main.dart: _areaData['factors']
class RouteResult(BaseModel):
name: str # trip_planning: RouteOption.name
duration: str # trip_planning: RouteOption.duration
distance: str # trip_planning: RouteOption.distance
safety_score: int # trip_planning: RouteOption.safetyScore
factors: List[str]
is_recommended: bool # trip_planning: RouteOption.isRecommended
class RouteResponse(BaseModel):
overall_score: int # trip_planning parses this
routes: List[RouteResult]
class HeatmapPoint(BaseModel):
lat: float # heatmap_service: HeatmapPoint.lat
lng: float # heatmap_service: HeatmapPoint.lng
risk: str # heatmap_screen: _riskFill() switches on "Low"|"Medium"|"High"
score: int # heatmap_screen: p.score for circle radius
class HeatmapResponse(BaseModel):
points: List[HeatmapPoint]
center_lat: float
center_lng: float
class AiRequest(BaseModel):
message: str # chatbot_ui: {"message": msg}
class AiResponse(BaseModel):
reply: str # chatbot_ui: data["reply"]
# ══════════════════════════════════════════════════════════════════════════
# SCORING
# ══════════════════════════════════════════════════════════════════════════
def _time_penalty(hour: int) -> int:
"""
Deterministic penalty by hour — no safe_p dependency.
8PM-4AM: -30 4AM: -25 5-7AM: -15 6-8PM: -10 else: 0
"""
if hour >= 20 or hour < 4:
return 30
if hour == 4:
return 25
if 5 <= hour < 7:
return 15
if 18 <= hour < 20:
return 10
return 0
def _score_to_label(score: int) -> tuple:
"""
Consistent thresholds used across ALL endpoints.
heatmap_screen.dart has its own route-segment thresholds (>=70 green,
>=45 orange, <45 red) inside Flutter — those are independent of this.
"""
if score >= 85:
return 0, "Low"
elif score >= 45:
return 1, "Medium"
else:
return 2, "High"
def _score_to_factors(score: int, density: int, hour: int = 12) -> List[str]:
if score >= 85:
factors = ["Well-lit streets", "High foot traffic"]
elif score >= 70:
factors = ["Mostly safe area", "Some foot traffic"]
elif score >= 55:
factors = ["Moderate foot traffic", "Some isolated roads"]
elif score >= 45:
factors = ["Limited lighting", "Below-average safety"]
else:
factors = ["High crime density", "Avoid if possible"]
if density > 20:
factors.append(f"High incident density (~{density} nearby)")
elif density > 5:
factors.append(f"Moderate incidents (~{density} nearby)")
else:
factors.append("Low historical incidents")
if hour >= 20 or hour < 4:
if score < 45:
factors.append("Moon High risk — avoid travel at this hour")
elif score < 65:
factors.append("Moon Use caution — late night travel risky")
else:
factors.append("Moon Relatively safer but stay alert at night")
elif hour == 4:
factors.append("Moon Pre-dawn hours — stay alert")
elif 5 <= hour < 7:
factors.append("Sun Early morning — limited activity in area")
elif 18 <= hour < 20:
factors.append("Evening caution advised" if score < 65
else "Moderate evening activity — stay aware")
return factors
# ══════════════════════════════════════════════════════════════════════════
# FEATURE ENGINEERING — matches ML_Model.ipynb Cell 5
# ══════════════════════════════════════════════════════════════════════════
def _build_feature_row(
lat: float, lon: float,
hour: int = 12, month: int = 6, is_weekend: int = 0,
) -> np.ndarray:
dist_km = geodesic((lat, lon), (center_lat, center_lon)).km
dist_sq = dist_km ** 2
lat_lon_prod = lat * lon
coord_rad = np.radians([[lat, lon]])
d_0p5 = int(ball_tree.query_radius(coord_rad, r=R_0P5, count_only=True)[0])
d_1km = int(ball_tree.query_radius(coord_rad, r=R_1KM, count_only=True)[0])
d_2km = int(ball_tree.query_radius(coord_rad, r=R_2KM, count_only=True)[0])
density_ratio = d_1km / (d_0p5 + 1)
density_dist = d_1km * dist_km
cluster_id = int(kmeans.predict([[lat, lon]])[0])
quarter = (month - 1) // 3 + 1
is_night = int(hour >= 20 or hour <= 5)
row = {
"Latitude": lat,
"Longitude": lon,
"dist_to_center_km": dist_km,
"dist_sq": dist_sq,
"density_0p5km": float(d_0p5),
"density_1km": float(d_1km),
"density_2km": float(d_2km),
"density_ratio": density_ratio,
"density_dist_interaction": density_dist,
"lat_lon_product": lat_lon_prod,
"cluster_id": float(cluster_id),
"FIR_MONTH": float(month),
"FIR_QUARTER": float(quarter),
"FIR_IS_WEEKEND": float(is_weekend),
"is_night": float(is_night),
}
df = pd.DataFrame([row])
for col in X_columns:
if col not in df.columns:
df[col] = 0.0
df = df[list(X_columns)].fillna(0.0)
arr = df.to_numpy(dtype=np.float64)
if arr.shape[1] < N_FEATURES:
arr = np.hstack([arr, np.zeros((1, N_FEATURES - arr.shape[1]))])
elif arr.shape[1] > N_FEATURES:
arr = arr[:, :N_FEATURES]
return arr
# ══════════════════════════════════════════════════════════════════════════
# INFERENCE
# ══════════════════════════════════════════════════════════════════════════
def _safe_proba(model, X_scaled: np.ndarray, is_lgb: bool) -> Optional[np.ndarray]:
try:
name = type(model).__name__
if hasattr(model, "predict") and "Booster" in name:
p = model.predict(X_scaled)
return p[0] if p.ndim == 2 else p
if is_lgb or "XGB" in name or "Forest" in name or "Trees" in name:
return model.predict_proba(X_scaled)[0]
if "LogisticRegression" in name:
scores = model.decision_function(X_scaled)[0]
e = np.exp(scores - scores.max())
return e / e.sum()
return model.predict_proba(X_scaled)[0]
except Exception as e:
log.warning("Model %s failed: %s", type(model).__name__, e)
return None
def _predict_danger(
lat: float, lon: float,
hour: int = 12, month: int = 6, is_weekend: int = 0,
) -> dict:
X_raw = _build_feature_row(lat, lon, hour, month, is_weekend)
X_scaled = scaler.transform(X_raw)
probas = []
for model, is_lgb, _ in _model_pool:
p = _safe_proba(model, X_scaled, is_lgb)
if p is not None and len(p) == 3:
probas.append(p)
if not probas:
log.error("All models failed for (%.4f, %.4f)", lat, lon)
proba = np.array([0.33, 0.34, 0.33])
elif len(probas) == 1:
proba = probas[0]
else:
proba = np.mean(probas, axis=0)
safe_p = float(proba[0])
high_p = float(proba[2])
# Weighted expected-value formula (v4.1) — avoids clustering at 50-70
med_p = max(0.0, 1.0 - safe_p - high_p)
expected = safe_p * 100.0 + med_p * 50.0 # high_p contributes 0
raw = 100.0 * ((expected / 100.0) ** 0.55)
base = int(max(0.0, min(100.0, raw)))
penalty = _time_penalty(hour)
score = max(5, min(100, base - penalty))
label, _ = _score_to_label(score)
try:
density_idx = list(X_columns).index("density_1km")
density = int(X_raw[0, density_idx])
except (ValueError, IndexError):
density = 0
return {"label": label, "score": score, "density": density, "hour": hour}
# ══════════════════════════════════════════════════════════════════════════
# UTILITIES
# ══════════════════════════════════════════════════════════════════════════
def _now_params():
n = datetime.datetime.now()
return n.hour, n.month, int(n.weekday() >= 5)
def _parse_time(t: str) -> int:
t = (t or "Now").lower().strip()
if t in ("now", ""): return datetime.datetime.now().hour
if "night" in t: return 22
if "morning" in t: return 8
if "afternoon" in t: return 14
if "evening" in t: return 18
if "1 hour" in t: return (datetime.datetime.now().hour + 1) % 24
return datetime.datetime.now().hour
def _osrm_leg(o_lat: float, o_lng: float, d_lat: float, d_lng: float) -> dict:
if _HTTPX_OK:
try:
r = _httpx.get(
f"http://router.project-osrm.org/route/v1/driving/"
f"{o_lng},{o_lat};{d_lng},{d_lat}?overview=false",
timeout=3.0,
)
if r.status_code == 200:
leg = r.json()["routes"][0]["legs"][0]
return {
"dist_km": round(leg["distance"] / 1000, 1),
"mins": max(1, round(leg["duration"] / 60)),
}
except Exception:
pass
straight = geodesic((o_lat, o_lng), (d_lat, d_lng)).km
return {
"dist_km": round(straight * 1.3, 1),
"mins": max(5, round(straight * 1.3 / 28 * 60)),
}
def _build_routes(
o_lat: float, o_lng: float,
d_lat: float, d_lng: float,
hour: int, month: int, is_weekend: int,
) -> RouteResponse:
m1_lat = (o_lat + d_lat) / 2 + 0.010
m1_lng = (o_lng + d_lng) / 2 + 0.010
m2_lat = (o_lat + d_lat) / 2 - 0.008
m2_lng = (o_lng + d_lng) / 2 - 0.008
with ThreadPoolExecutor(max_workers=10) as ex:
f_main = ex.submit(_osrm_leg, o_lat, o_lng, d_lat, d_lng)
f_a1 = ex.submit(_osrm_leg, o_lat, o_lng, m1_lat, m1_lng)
f_b1 = ex.submit(_osrm_leg, m1_lat, m1_lng, d_lat, d_lng)
f_a2 = ex.submit(_osrm_leg, o_lat, o_lng, m2_lat, m2_lng)
f_b2 = ex.submit(_osrm_leg, m2_lat, m2_lng, d_lat, d_lng)
f_orig = ex.submit(_predict_danger, o_lat, o_lng, hour, month, is_weekend)
f_dest = ex.submit(_predict_danger, d_lat, d_lng, hour, month, is_weekend)
f_mid = ex.submit(_predict_danger, (o_lat+d_lat)/2, (o_lng+d_lng)/2, hour, month, is_weekend)
f_mid1 = ex.submit(_predict_danger, m1_lat, m1_lng, hour, month, is_weekend)
f_mid2 = ex.submit(_predict_danger, m2_lat, m2_lng, hour, month, is_weekend)
main = f_main.result()
a1, b1 = f_a1.result(), f_b1.result()
a2, b2 = f_a2.result(), f_b2.result()
orig_r = f_orig.result()
dest_r = f_dest.result()
mid_r = f_mid.result()
mid1_r = f_mid1.result()
mid2_r = f_mid2.result()
def ws(mid: dict) -> int:
return int(orig_r["score"] * 0.2 + mid["score"] * 0.5 + dest_r["score"] * 0.3)
routes_raw = [
{
"name": "Main Route",
"duration": f"{main['mins']} min",
"distance": f"{main['dist_km']} km",
"safety_score": ws(mid_r),
"factors": _score_to_factors(ws(mid_r), mid_r["density"], hour),
"is_recommended": False,
},
{
"name": "Alternate Route 1",
"duration": f"{a1['mins'] + b1['mins']} min",
"distance": f"{round(a1['dist_km'] + b1['dist_km'], 1)} km",
"safety_score": ws(mid1_r),
"factors": _score_to_factors(ws(mid1_r), mid1_r["density"], hour),
"is_recommended": False,
},
{
"name": "Alternate Route 2",
"duration": f"{a2['mins'] + b2['mins']} min",
"distance": f"{round(a2['dist_km'] + b2['dist_km'], 1)} km",
"safety_score": ws(mid2_r),
"factors": _score_to_factors(ws(mid2_r), mid2_r["density"], hour),
"is_recommended": False,
},
]
routes_raw.sort(key=lambda r: r["safety_score"], reverse=True)
routes_raw[0]["is_recommended"] = True
overall = int(sum(r["safety_score"] for r in routes_raw) / len(routes_raw))
return RouteResponse(
overall_score=overall,
routes=[RouteResult(**r) for r in routes_raw],
)
# ══════════════════════════════════════════════════════════════════════════
# ENDPOINTS
# ══════════════════════════════════════════════════════════════════════════
@app.get("/health")
def health():
return {
"status": "ok",
"version": "4.1",
"models": [m[2] for m in _model_pool],
"ensemble": ENSEMBLE_OK,
"n_features": N_FEATURES,
"n_clusters": kmeans.n_clusters,
"feature_cols": list(X_columns),
"scoring": "weighted_expected_v4.1",
}
# GET — used by heatmap_service.dart SafetyApiService.predictArea()
@app.get("/predict_area", response_model=AreaResponse)
def predict_area_get(latitude: float, longitude: float):
try:
hour, month, iw = _now_params()
r = _predict_danger(latitude, longitude, hour, month, iw)
label, level = _score_to_label(r["score"])
return AreaResponse(
safety_score=r["score"],
danger_level=level,
danger_label=label,
factors=_score_to_factors(r["score"], r["density"], hour),
)
except Exception as e:
log.exception("predict_area_get failed")
raise HTTPException(status_code=500, detail=str(e))
# POST — used by safety_api_service.dart SafetyApiService.predictArea()
@app.post("/predict_area", response_model=AreaResponse)
def predict_area_post(req: AreaRequest):
try:
h, mo, iw = _now_params()
hour = req.hour if req.hour is not None else h
month = req.month if req.month is not None else mo
is_weekend = req.is_weekend if req.is_weekend is not None else iw
r = _predict_danger(req.latitude, req.longitude, hour, month, is_weekend)
label, level = _score_to_label(r["score"])
return AreaResponse(
safety_score=r["score"],
danger_level=level,
danger_label=label,
factors=_score_to_factors(r["score"], r["density"], hour),
)
except Exception as e:
log.exception("predict_area_post failed")
raise HTTPException(status_code=500, detail=str(e))
# Alias used by some older callers
@app.post("/predict_area_full", response_model=AreaResponse)
def predict_area_full(req: AreaRequest):
return predict_area_post(req)
# GET — used by heatmap_service.dart SafetyApiService.analyzeRoute()
@app.get("/analyze_route", response_model=RouteResponse)
def analyze_route_get(
origin_lat: float, origin_lng: float,
dest_lat: float, dest_lng: float,
time: str = "Now",
):
try:
hour = _parse_time(time)
_, month, iw = _now_params()
return _build_routes(origin_lat, origin_lng, dest_lat, dest_lng,
hour, month, iw)
except Exception as e:
log.exception("analyze_route_get failed")
raise HTTPException(status_code=500, detail=str(e))
# POST — used by safety_api_service.dart SafetyApiService.analyzeRoute()
@app.post("/analyze_route", response_model=RouteResponse)
def analyze_route_post(req: RouteRequest):
try:
hour = _parse_time(req.time)
_, month, iw = _now_params()
return _build_routes(req.origin_lat, req.origin_lng,
req.dest_lat, req.dest_lng,
hour, month, iw)
except Exception as e:
log.exception("analyze_route_post failed")
raise HTTPException(status_code=500, detail=str(e))
# GET — used by heatmap_service.dart HeatmapService.fetchHeatmap()
@app.get("/heatmap_data", response_model=HeatmapResponse)
def heatmap_data(
latitude: float = 12.9716,
longitude: float = 77.5946,
radius_km: float = 2.0,
step_km: float = 0.3,
):
"""
risk field MUST be "Low", "Medium", or "High" —
heatmap_screen.dart _riskFill() switches on these exact strings.
score is used for circle radius: (100-score)/100 * 60 + 80.
"""
try:
hour, month, iw = _now_params()
lat_step = step_km / 111.0
lng_step = step_km / (111.0 * max(abs(math.cos(math.radians(latitude))), 0.01))
lat_range = np.arange(
latitude - radius_km / 111.0,
latitude + radius_km / 111.0,
lat_step,
)
lng_range = np.arange(
longitude - radius_km / 111.0,
longitude + radius_km / 111.0,
lng_step,
)
MAX_PTS = 200
if len(lat_range) * len(lng_range) > MAX_PTS:
s = max(1, int(math.ceil(math.sqrt(
len(lat_range) * len(lng_range) / MAX_PTS))))
lat_range = lat_range[::s]
lng_range = lng_range[::s]
grid = [(float(la), float(lo)) for la in lat_range for lo in lng_range]
log.info("Heatmap grid: %d points", len(grid))
results_map: dict = {}
with ThreadPoolExecutor(max_workers=24) as ex:
futures = {
ex.submit(_predict_danger, la, lo, hour, month, iw): (la, lo)
for la, lo in grid
}
for fut in as_completed(futures):
coord = futures[fut]
try:
results_map[coord] = fut.result()
except Exception as exc:
log.warning("Heatmap point %s skipped: %s", coord, exc)
points = []
for la, lo in grid:
res = results_map.get((la, lo))
if res is None:
continue
_lbl, level = _score_to_label(res["score"])
points.append(HeatmapPoint(
lat=round(la, 5),
lng=round(lo, 5),
risk=level,
score=res["score"],
))
if not points:
raise HTTPException(status_code=500,
detail="No valid predictions returned.")
return HeatmapResponse(
points=points,
center_lat=latitude,
center_lng=longitude,
)
except HTTPException:
raise
except Exception as e:
log.exception("heatmap_data failed")
raise HTTPException(status_code=500, detail=str(e))
# POST — chatbot_ui.dart getBotResponse() POSTs to /ai
@app.post("/ai", response_model=AiResponse)
async def ai_chat(body: AiRequest):
try:
msg = body.message.strip()
lower = msg.lower()
if not msg:
return AiResponse(reply="Please enter a message.")
FAST = [
({"danger", "emergency", "attack", "unsafe", "threat", "help"},
"Stay calm. Call 112 immediately and move to a safe, crowded place."),
({"police"},
"Call 112 for police. Use the SOS button in the app for instant alerts."),
({"sos", "panic", "alert"},
"Press the SOS button to instantly alert your emergency contacts."),
({"hospital", "ambulance", "injured", "hurt", "accident", "medical"},
"Call 108 for an ambulance. Tap Nearby Help for the nearest hospital."),
({"route", "safe route", "path", "navigate"},
"Tap Plan Safe Route on the home screen for ML-rated safe routes."),
({"heatmap", "crime map", "danger zone"},
"Check the Safety Heatmap on the home screen for real-time crime density."),
({"safe", "area", "location", "nearby"},
"Use the heatmap on the home screen for a live area safety overview."),
]
for kws, reply in FAST:
if any(k in lower for k in kws):
return AiResponse(reply=reply)
if groq_client is None:
return AiResponse(
reply="I am your Nyvra safety assistant. For emergencies, call 112. "
"Use the SOS button to alert your contacts instantly."
)
import asyncio
loop = asyncio.get_running_loop()
resp = await loop.run_in_executor(
None,
lambda: groq_client.chat.completions.create(
model="llama-3.1-8b-instant",
max_tokens=120,
temperature=0.4,
messages=[
{"role": "system", "content": (
"You are Nyvra, a concise women's safety assistant for India. "
"Reply in 2-3 short sentences. Be calm, practical, empathetic. "
"For emergencies always say: call 112."
)},
{"role": "user", "content": msg},
],
)
)
return AiResponse(reply=resp.choices[0].message.content.strip())
except Exception as e:
log.exception("ai_chat failed")
return AiResponse(
reply="I am here to help with your safety. For emergencies, call 112 immediately."
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000, reload=False)