Spaces:
Sleeping
Sleeping
Added 2 endpoints (modelfit_chat, modelforecast_chat) to use in custom chats
Browse files- main.py +86 -4
- requirements.txt +3 -1
main.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
-
import os, asyncio,
|
| 2 |
-
|
|
|
|
|
|
|
| 3 |
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from slowapi import Limiter
|
|
@@ -41,8 +43,8 @@ async def ratelimit_handler(request: Request, exc: RateLimitExceeded):
|
|
| 41 |
SDK_MAX_BODY_BYTES = int(os.getenv("SDK_MAX_BODY_BYTES", "25000000")) # 25MB default for demo
|
| 42 |
SDK_MAX_BODY_BYTES_extended = int(os.getenv("SDK_MAX_BODY_BYTES_extended", "125000000")) # 125MB default for prod
|
| 43 |
|
| 44 |
-
# How long to wait for upstream (API) response
|
| 45 |
-
UPSTREAM_TIMEOUT = float(os.getenv("UPSTREAM_TIMEOUT", "900")) # 15
|
| 46 |
# While waiting for a response: every PING_INTERVAL seconds we ping the backend (secure_ping) and, if SELF_URL is set, we also ping ourselves (keep-alive) so this Space stays awake
|
| 47 |
PING_INTERVAL = float(os.getenv("PING_INTERVAL", "270"))
|
| 48 |
|
|
@@ -127,6 +129,37 @@ def _extract_user_token(req: Request) -> str | None:
|
|
| 127 |
return None
|
| 128 |
return auth.split(" ", 1)[1].strip()
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
async def _forward(path: str, method: str = "GET", json_body=None, user_token: str | None = None):
|
| 131 |
"""
|
| 132 |
Forward request to the PRIVATE Space:
|
|
@@ -516,6 +549,55 @@ async def modelforecast(req: Request):
|
|
| 516 |
body = await req.json()
|
| 517 |
return await _forward("/modelforecast/", "POST", json_body=body, user_token=user_token)
|
| 518 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
@app.post("/modelfit-file/")
|
| 520 |
async def modelfit_file(
|
| 521 |
req: Request,
|
|
|
|
| 1 |
+
import os, asyncio, io
|
| 2 |
+
import httpx
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form, Body
|
| 5 |
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
from slowapi import Limiter
|
|
|
|
| 43 |
SDK_MAX_BODY_BYTES = int(os.getenv("SDK_MAX_BODY_BYTES", "25000000")) # 25MB default for demo
|
| 44 |
SDK_MAX_BODY_BYTES_extended = int(os.getenv("SDK_MAX_BODY_BYTES_extended", "125000000")) # 125MB default for prod
|
| 45 |
|
| 46 |
+
# How long to wait for upstream (API) response. modelforecast_ind on many individuals can take 60–90 min; set UPSTREAM_TIMEOUT high (e.g. 5400) or leave default.
|
| 47 |
+
UPSTREAM_TIMEOUT = float(os.getenv("UPSTREAM_TIMEOUT", "900")) # 15 min default so long forecast_ind runs don't hit ReadTimeout
|
| 48 |
# While waiting for a response: every PING_INTERVAL seconds we ping the backend (secure_ping) and, if SELF_URL is set, we also ping ourselves (keep-alive) so this Space stays awake
|
| 49 |
PING_INTERVAL = float(os.getenv("PING_INTERVAL", "270"))
|
| 50 |
|
|
|
|
| 129 |
return None
|
| 130 |
return auth.split(" ", 1)[1].strip()
|
| 131 |
|
| 132 |
+
# Chat endpoints: fetch data from URL, parse to table, forward to API
|
| 133 |
+
DATA_URL_FETCH_TIMEOUT = 60.0
|
| 134 |
+
DATA_URL_MAX_BYTES = 20 * 1024 * 1024 # 20MB
|
| 135 |
+
|
| 136 |
+
async def _fetch_url_to_records(data_url: str) -> list:
|
| 137 |
+
"""Fetch data_url (https only), parse CSV/Excel/JSON to list of dicts. Raises HTTPException on error."""
|
| 138 |
+
if not data_url.strip().lower().startswith("https://"):
|
| 139 |
+
raise HTTPException(status_code=400, detail="data_url must be an HTTPS URL.")
|
| 140 |
+
async with httpx.AsyncClient(timeout=DATA_URL_FETCH_TIMEOUT, follow_redirects=True) as client:
|
| 141 |
+
r = await client.get(data_url)
|
| 142 |
+
r.raise_for_status()
|
| 143 |
+
raw = r.content
|
| 144 |
+
content_type = (r.headers.get("content-type") or "").lower()
|
| 145 |
+
if len(raw) > DATA_URL_MAX_BYTES:
|
| 146 |
+
raise HTTPException(status_code=413, detail=f"Data at URL exceeds {DATA_URL_MAX_BYTES // (1024*1024)}MB limit.")
|
| 147 |
+
path_lower = data_url.split("?")[0].lower()
|
| 148 |
+
try:
|
| 149 |
+
if "json" in content_type or path_lower.endswith(".json"):
|
| 150 |
+
df = pd.read_json(io.BytesIO(raw))
|
| 151 |
+
elif "spreadsheet" in content_type or "excel" in content_type or path_lower.endswith((".xlsx", ".xls")):
|
| 152 |
+
df = pd.read_excel(io.BytesIO(raw))
|
| 153 |
+
else:
|
| 154 |
+
# CSV or default
|
| 155 |
+
df = pd.read_csv(io.BytesIO(raw))
|
| 156 |
+
except Exception as e:
|
| 157 |
+
raise HTTPException(status_code=400, detail=f"Could not parse data from URL: {str(e)[:200]}")
|
| 158 |
+
for col in df.columns:
|
| 159 |
+
if pd.api.types.is_datetime64_any_dtype(df[col]):
|
| 160 |
+
df[col] = df[col].astype(str)
|
| 161 |
+
return df.to_dict(orient="records")
|
| 162 |
+
|
| 163 |
async def _forward(path: str, method: str = "GET", json_body=None, user_token: str | None = None):
|
| 164 |
"""
|
| 165 |
Forward request to the PRIVATE Space:
|
|
|
|
| 549 |
body = await req.json()
|
| 550 |
return await _forward("/modelforecast/", "POST", json_body=body, user_token=user_token)
|
| 551 |
|
| 552 |
+
|
| 553 |
+
@app.post("/modelfit_chat/")
|
| 554 |
+
async def modelfit_chat(
|
| 555 |
+
req: Request,
|
| 556 |
+
data_url: str = Body(..., embed=True),
|
| 557 |
+
id_col: str = Body(..., embed=True),
|
| 558 |
+
time_col: str = Body(..., embed=True),
|
| 559 |
+
y: str = Body(..., embed=True),
|
| 560 |
+
lag_y: object = Body(None, embed=True),
|
| 561 |
+
lagged_features: object = Body({}, embed=True),
|
| 562 |
+
current_features: object = Body([], embed=True),
|
| 563 |
+
filter_by_significance: bool = Body(False, embed=True),
|
| 564 |
+
meanvar_test: bool = Body(False, embed=True),
|
| 565 |
+
signif: object = Body(0.05, embed=True),
|
| 566 |
+
):
|
| 567 |
+
"""Fetch training data from data_url (HTTPS), then call /modelfit/ on the API. Returns JSON fit result."""
|
| 568 |
+
user_token = _extract_user_token(req)
|
| 569 |
+
if not user_token:
|
| 570 |
+
raise HTTPException(status_code=401, detail="Missing Authorization Bearer token (dt+...)")
|
| 571 |
+
df_records = await _fetch_url_to_records(data_url)
|
| 572 |
+
payload = {
|
| 573 |
+
"df": df_records,
|
| 574 |
+
"id_col": id_col,
|
| 575 |
+
"time_col": time_col,
|
| 576 |
+
"y": y,
|
| 577 |
+
"lag_y": lag_y,
|
| 578 |
+
"lagged_features": lagged_features,
|
| 579 |
+
"current_features": current_features,
|
| 580 |
+
"filter_by_significance": filter_by_significance,
|
| 581 |
+
"meanvar_test": meanvar_test,
|
| 582 |
+
"signif": signif,
|
| 583 |
+
}
|
| 584 |
+
return await _forward("/modelfit/", "POST", json_body=payload, user_token=user_token)
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
@app.post("/modelforecast_chat/")
|
| 588 |
+
async def modelforecast_chat(
|
| 589 |
+
req: Request,
|
| 590 |
+
data_url: str = Body(..., embed=True),
|
| 591 |
+
):
|
| 592 |
+
"""Fetch forecast input data from data_url (HTTPS), then call /modelforecast/ on the API. Returns JSON forecast list."""
|
| 593 |
+
user_token = _extract_user_token(req)
|
| 594 |
+
if not user_token:
|
| 595 |
+
raise HTTPException(status_code=401, detail="Missing Authorization Bearer token (dt+...)")
|
| 596 |
+
df_records = await _fetch_url_to_records(data_url)
|
| 597 |
+
payload = {"df_forecast": df_records}
|
| 598 |
+
return await _forward("/modelforecast/", "POST", json_body=payload, user_token=user_token)
|
| 599 |
+
|
| 600 |
+
|
| 601 |
@app.post("/modelfit-file/")
|
| 602 |
async def modelfit_file(
|
| 603 |
req: Request,
|
requirements.txt
CHANGED
|
@@ -3,4 +3,6 @@ uvicorn
|
|
| 3 |
httpx
|
| 4 |
python-multipart
|
| 5 |
slowapi
|
| 6 |
-
limits
|
|
|
|
|
|
|
|
|
| 3 |
httpx
|
| 4 |
python-multipart
|
| 5 |
slowapi
|
| 6 |
+
limits
|
| 7 |
+
pandas
|
| 8 |
+
openpyxl
|