DrValera commited on
Commit
3b8739b
·
verified ·
1 Parent(s): b690027

Added 2 endpoints (modelfit_chat, modelforecast_chat) to use in custom chats

Browse files
Files changed (2) hide show
  1. main.py +86 -4
  2. requirements.txt +3 -1
main.py CHANGED
@@ -1,5 +1,7 @@
1
- import os, asyncio, httpx
2
- from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
 
 
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; long runs may need 30+ min (1800+)
45
- UPSTREAM_TIMEOUT = float(os.getenv("UPSTREAM_TIMEOUT", "900")) # 15 minutes
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