hitenvk22 commited on
Commit
183cc80
·
verified ·
1 Parent(s): 8194732

Initial GPU-accelerated FinStream API

Browse files
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ TRANSFORMERS_CACHE=/cache
6
+
7
+ RUN apt-get update && apt-get install -y --no-install-recommends \
8
+ gcc \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ WORKDIR /app
12
+
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ COPY . .
17
+
18
+ EXPOSE 7860
19
+
20
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,10 +1,24 @@
1
  ---
2
- title: FinStream API
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FinStream Sentiment API
3
+ emoji: 📈
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  ---
9
 
10
+ # FinStream Sentiment API
11
+
12
+ GPU-accelerated FastAPI backend for FinStream financial sentiment analysis.
13
+
14
+ ## API Endpoints
15
+
16
+ - `GET /health` - Service and model status
17
+ - `POST /predict` - Single text sentiment analysis
18
+ - `POST /analyze-csv` - Batch CSV analysis
19
+
20
+ ## Model
21
+
22
+ - **Model**: hitenvk22/FinStream-Sentiment
23
+ - **Architecture**: distilroberta-base
24
+ - **Task**: 3-class financial sentiment (bullish, neutral, bearish)
app/__init__.py ADDED
File without changes
app/api/__init__.py ADDED
File without changes
app/api/routes.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
+ import logging
3
+ import os
4
+ import re
5
+ from uuid import uuid4
6
+
7
+ import pandas as pd
8
+ from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile, status
9
+ from fastapi.responses import FileResponse
10
+ from fastapi.concurrency import run_in_threadpool
11
+
12
+ from ..schemas import (
13
+ BatchAnalysisResponse,
14
+ BatchAnalysisSummary,
15
+ HealthResponse,
16
+ PredictRequest,
17
+ PredictResponse,
18
+ )
19
+
20
+
21
+ router = APIRouter()
22
+ logger = logging.getLogger("finstream.api")
23
+
24
+ TEXT_COLUMN_HINTS = (
25
+ "text", "message", "sentence", "content", "news",
26
+ "headline", "comment", "description", "article", "body", "post",
27
+ )
28
+
29
+ REPORTS_DIR = "/tmp/reports"
30
+ os.makedirs(REPORTS_DIR, exist_ok=True)
31
+
32
+
33
+ def _get_model_manager(request: Request):
34
+ return request.app.state.model_manager
35
+
36
+
37
+ def _normalize_column_name(column_name: str) -> str:
38
+ return re.sub(r"[^a-z0-9]+", "", column_name.lower())
39
+
40
+
41
+ def _detect_text_column(frame: pd.DataFrame) -> str:
42
+ if frame.empty:
43
+ raise ValueError("CSV file is empty")
44
+ normalized_columns = {col: _normalize_column_name(str(col)) for col in frame.columns}
45
+ for column, normalized in normalized_columns.items():
46
+ if normalized in TEXT_COLUMN_HINTS or any(hint in normalized for hint in TEXT_COLUMN_HINTS):
47
+ return column
48
+ object_columns = frame.select_dtypes(include=["object", "string"]).columns.tolist()
49
+ if object_columns:
50
+ scored_columns = []
51
+ for column in object_columns:
52
+ series = frame[column].dropna().astype(str).str.strip()
53
+ if series.empty:
54
+ continue
55
+ average_length = series.str.len().mean()
56
+ non_empty_ratio = (series != "").mean()
57
+ scored_columns.append((float(average_length * non_empty_ratio), column))
58
+ if scored_columns:
59
+ scored_columns.sort(reverse=True)
60
+ return scored_columns[0][1]
61
+ return object_columns[0]
62
+ return frame.columns[0]
63
+
64
+
65
+ @router.get("/health", response_model=HealthResponse)
66
+ async def health_check(request: Request) -> HealthResponse:
67
+ mm = _get_model_manager(request)
68
+ return HealthResponse(
69
+ status="ok" if mm.is_ready else "degraded",
70
+ model_loaded=mm.is_ready,
71
+ device=mm.device,
72
+ model_name=mm.model_name,
73
+ )
74
+
75
+
76
+ @router.post("/predict", response_model=PredictResponse)
77
+ async def predict(payload: PredictRequest, request: Request) -> PredictResponse:
78
+ mm = _get_model_manager(request)
79
+ if not mm.is_ready:
80
+ raise HTTPException(status_code=503, detail="Model is not ready")
81
+ try:
82
+ result = await run_in_threadpool(mm.predict, payload.text)
83
+ return PredictResponse(**result)
84
+ except HTTPException:
85
+ raise
86
+ except Exception as exc:
87
+ logger.exception("Prediction failed")
88
+ raise HTTPException(status_code=500, detail="Prediction failed") from exc
89
+
90
+
91
+ @router.post("/analyze-csv", response_model=BatchAnalysisResponse)
92
+ async def analyze_csv(
93
+ request: Request,
94
+ file: UploadFile = File(...),
95
+ report_id: str | None = Form(default=None),
96
+ ) -> BatchAnalysisResponse:
97
+ mm = _get_model_manager(request)
98
+ if not mm.is_ready:
99
+ raise HTTPException(status_code=503, detail="Model is not ready")
100
+ if not file.filename.lower().endswith(".csv"):
101
+ raise HTTPException(status_code=400, detail="Please upload a CSV file")
102
+
103
+ try:
104
+ raw_bytes = await file.read()
105
+ if not raw_bytes:
106
+ raise ValueError("Uploaded CSV file is empty")
107
+
108
+ frame = pd.read_csv(BytesIO(raw_bytes))
109
+ detected_text_column = _detect_text_column(frame)
110
+ working_frame = frame.copy()
111
+ working_frame[detected_text_column] = (
112
+ working_frame[detected_text_column].fillna("").astype(str).str.strip()
113
+ )
114
+ working_frame = working_frame[working_frame[detected_text_column] != ""]
115
+
116
+ if working_frame.empty:
117
+ raise ValueError("No non-empty text rows were found in the CSV")
118
+
119
+ texts = working_frame[detected_text_column].tolist()
120
+ predictions = mm.predict_batch(texts)
121
+
122
+ rows = []
123
+ for idx, (text, pred) in enumerate(zip(texts, predictions), start=1):
124
+ label = str(pred.get("label", "unknown")).lower()
125
+ if label == "positive":
126
+ label = "bullish"
127
+ elif label == "negative":
128
+ label = "bearish"
129
+ rows.append({
130
+ "row_number": idx,
131
+ "message": text,
132
+ "predicted_label": label,
133
+ "confidence": float(pred.get("confidence", 0.0)),
134
+ })
135
+
136
+ pred_frame = pd.DataFrame(rows)
137
+ counts = pred_frame["predicted_label"].value_counts().to_dict()
138
+ total = len(pred_frame)
139
+ bullish_c = counts.get("bullish", 0)
140
+ neutral_c = counts.get("neutral", 0)
141
+ bearish_c = counts.get("bearish", 0)
142
+ unknown_c = counts.get("unknown", 0)
143
+
144
+ rid = report_id.strip() if report_id and report_id.strip() else f"FSR-{uuid4().hex[:10].upper()}"
145
+ net_sent = round(((bullish_c - bearish_c) / total), 4) if total else 0.0
146
+ avg_conf = round(float(pred_frame["confidence"].mean()), 4)
147
+
148
+ net_label = "positive" if net_sent > 0.12 else ("negative" if net_sent < -0.12 else "mixed")
149
+
150
+ summary = BatchAnalysisSummary(
151
+ report_id=rid,
152
+ detected_text_column=detected_text_column,
153
+ total_rows=total,
154
+ analyzed_rows=total,
155
+ bullish_count=bullish_c,
156
+ neutral_count=neutral_c,
157
+ bearish_count=bearish_c,
158
+ unknown_count=unknown_c,
159
+ bullish_pct=round((bullish_c / total) * 100, 2) if total else 0.0,
160
+ neutral_pct=round((neutral_c / total) * 100, 2) if total else 0.0,
161
+ bearish_pct=round((bearish_c / total) * 100, 2) if total else 0.0,
162
+ unknown_pct=round((unknown_c / total) * 100, 2) if total else 0.0,
163
+ net_sentiment=net_sent,
164
+ net_sentiment_label=net_label,
165
+ average_confidence=avg_conf,
166
+ report_pdf_url=f"/reports/{rid}.pdf",
167
+ )
168
+
169
+ return BatchAnalysisResponse(
170
+ summary=summary,
171
+ predictions=pred_frame.reset_index(drop=True).to_dict(orient="records"),
172
+ )
173
+ except HTTPException:
174
+ raise
175
+ except Exception as exc:
176
+ logger.exception("CSV analysis failed")
177
+ raise HTTPException(status_code=500, detail=f"CSV analysis failed: {exc}") from exc
app/schemas.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class PredictRequest(BaseModel):
5
+ text: str
6
+
7
+
8
+ class PredictResponse(BaseModel):
9
+ label: str
10
+ confidence: float
11
+
12
+
13
+ class HealthResponse(BaseModel):
14
+ status: str
15
+ model_loaded: bool
16
+ device: str
17
+ model_name: str
18
+
19
+
20
+ class BatchPredictionItem(BaseModel):
21
+ row_number: int
22
+ message: str
23
+ predicted_label: str
24
+ confidence: float
25
+
26
+
27
+ class BatchAnalysisSummary(BaseModel):
28
+ report_id: str
29
+ detected_text_column: str
30
+ total_rows: int
31
+ analyzed_rows: int
32
+ bullish_count: int
33
+ neutral_count: int
34
+ bearish_count: int
35
+ unknown_count: int
36
+ bullish_pct: float
37
+ neutral_pct: float
38
+ bearish_pct: float
39
+ unknown_pct: float
40
+ net_sentiment: float
41
+ net_sentiment_label: str
42
+ average_confidence: float
43
+ report_pdf_url: str
44
+
45
+
46
+ class BatchAnalysisResponse(BaseModel):
47
+ summary: BatchAnalysisSummary
48
+ predictions: list[dict]
app/services/__init__.py ADDED
File without changes
app/services/model_service.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import re
5
+ from threading import Lock
6
+
7
+ import torch
8
+ from anyio import to_thread
9
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
10
+
11
+
12
+ logger = logging.getLogger("finstream.model")
13
+
14
+ POSITIVE_WORDS = {
15
+ "beat", "beats", "bullish", "climb", "climbs", "climbed",
16
+ "gain", "gains", "gained", "growth", "higher",
17
+ "improve", "improves", "improved", "improvement", "improvements",
18
+ "outperform", "outperforms", "outperformed",
19
+ "profit", "profits", "profitable", "profitability",
20
+ "rally", "rallies", "rallied",
21
+ "rise", "rises", "rose", "risen",
22
+ "surge", "surges", "surged",
23
+ "strong", "stronger", "strongly",
24
+ "up", "uptick", "upside", "positive", "record",
25
+ "boost", "boosts", "boosted",
26
+ "upgrade", "upgrades", "upgraded",
27
+ "exceed", "exceeds", "exceeded",
28
+ "expand", "expands", "expanded", "expansion",
29
+ "accelerate", "accelerates", "accelerated",
30
+ "recover", "recovers", "recovered", "recovery",
31
+ "rebound", "rebounds", "rebounded",
32
+ "jump", "jumps", "jumped", "soar", "soars", "soared",
33
+ "dividend", "dividends", "buyback", "buybacks",
34
+ "upward", "uptrend", "bull", "upswing", "breakout",
35
+ "optimistic", "optimism", "momentum",
36
+ "upbeat", "win", "wins", "won", "success", "successful",
37
+ }
38
+
39
+ NEGATIVE_WORDS = {
40
+ "bearish", "decline", "declines", "declined",
41
+ "drop", "drops", "dropped",
42
+ "fall", "falls", "fell", "fallen",
43
+ "loss", "losses", "lost",
44
+ "miss", "misses", "missed",
45
+ "pressure", "pressures", "pressured",
46
+ "risk", "risks", "risky",
47
+ "selloff", "selloffs",
48
+ "slump", "slumps", "slumped",
49
+ "soft", "softer", "softness",
50
+ "weak", "weaker", "weakness", "weaknesses", "weaken", "weakens", "weakened",
51
+ "down", "downturn", "downturns", "downside", "downgrade",
52
+ "negative",
53
+ "cut", "cuts", "cutting",
54
+ "lower", "lowers", "lowered",
55
+ "reduce", "reduces", "reduced", "reduction",
56
+ "layoff", "layoffs", "bankrupt", "bankruptcy", "debt",
57
+ "default", "defaults",
58
+ "delay", "delays", "delayed",
59
+ "suspend", "suspends", "suspended", "suspension",
60
+ "worst", "worse", "worsen", "worsens", "worsened",
61
+ "volatile", "volatility",
62
+ "uncertainty", "uncertain",
63
+ "plunge", "plunges", "plunged",
64
+ "tumble", "tumbles", "tumbled",
65
+ "slide", "slides", "slid",
66
+ "crash", "crashes", "crashed",
67
+ "recession", "inflation", "inflationary",
68
+ "underperform", "underperforms", "underperformed",
69
+ }
70
+
71
+
72
+ def _normalize_label(raw_label: str) -> str:
73
+ normalized = raw_label.strip().lower()
74
+ if normalized in {"positive", "bullish", "label_1", "1", "pos"}:
75
+ return "bullish"
76
+ if normalized in {"negative", "bearish", "label_0", "0", "neg"}:
77
+ return "bearish"
78
+ if normalized in {"neutral", "label_2", "2"}:
79
+ return "neutral"
80
+ if "pos" in normalized:
81
+ return "bullish"
82
+ if "neg" in normalized:
83
+ return "bearish"
84
+ return normalized
85
+
86
+
87
+ class SentimentModelManager:
88
+ def __init__(self, model_name: str) -> None:
89
+ self.model_name = model_name
90
+ self.device = "cpu"
91
+ self._device_index = -1
92
+ self._pipeline = None
93
+ self._load_error: str | None = None
94
+
95
+ @property
96
+ def is_ready(self) -> bool:
97
+ return self._pipeline is not None and self._load_error is None
98
+
99
+ @property
100
+ def load_error(self) -> str | None:
101
+ return self._load_error
102
+
103
+ async def load_async(self) -> None:
104
+ await to_thread.run_sync(self.load)
105
+
106
+ def load(self) -> None:
107
+ if self._pipeline is not None:
108
+ return
109
+
110
+ try:
111
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
112
+ self._device_index = 0 if torch.cuda.is_available() else -1
113
+ logger.info("Loading model %s on %s", self.model_name, self.device)
114
+
115
+ tokenizer = AutoTokenizer.from_pretrained(self.model_name)
116
+ model = AutoModelForSequenceClassification.from_pretrained(
117
+ self.model_name,
118
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
119
+ low_cpu_mem_usage=True,
120
+ )
121
+ model.eval()
122
+
123
+ self._pipeline = pipeline(
124
+ task="sentiment-analysis",
125
+ model=model,
126
+ tokenizer=tokenizer,
127
+ device=self._device_index,
128
+ truncation=True,
129
+ framework="pt",
130
+ )
131
+ self._load_error = None
132
+ logger.info("Model loaded successfully on %s", self.device)
133
+ except Exception as exc:
134
+ self._load_error = str(exc)
135
+ logger.exception("Failed to load sentiment model")
136
+
137
+ @staticmethod
138
+ def _stem(token: str) -> str:
139
+ if len(token) <= 4:
140
+ return token
141
+ for suffix in ["ability", "abilities", "ification", "ifications",
142
+ "ization", "izations", "isation", "isations",
143
+ "ationally", "isation", "ization",
144
+ "iveness", "fulness", "iousness",
145
+ "ments", "ment", "ances", "ance",
146
+ "eness", "ness", "ship",
147
+ "able", "ably", "ible",
148
+ "ally", "wise", "like",
149
+ "ious", "eous", "uous",
150
+ "sion", "tion", "sions", "tions",
151
+ "ised", "ized", "ising", "izing",
152
+ "ative", "itive", "tive",
153
+ "less", "proof", "ward",
154
+ "ing", "ings",
155
+ "ed", "es", "er", "est", "ly"]:
156
+ if token.endswith(suffix) and len(token) - len(suffix) >= 3:
157
+ return token[:-len(suffix)]
158
+ return token
159
+
160
+ def _rule_based_predict(self, text: str) -> dict[str, float | str]:
161
+ tokens = re.findall(r"[a-zA-Z']+", text.lower())
162
+ if not tokens:
163
+ return {"label": "neutral", "confidence": 0.5}
164
+ stemmed_tokens = [self._stem(t) for t in tokens]
165
+ positive_hits = sum(
166
+ 1 for i, t in enumerate(tokens)
167
+ if t in POSITIVE_WORDS or stemmed_tokens[i] in POSITIVE_WORDS
168
+ )
169
+ negative_hits = sum(
170
+ 1 for i, t in enumerate(tokens)
171
+ if t in NEGATIVE_WORDS or stemmed_tokens[i] in NEGATIVE_WORDS
172
+ )
173
+ total_hits = positive_hits + negative_hits
174
+ score = positive_hits - negative_hits
175
+ if total_hits == 0:
176
+ return {"label": "neutral", "confidence": 0.5}
177
+ confidence = min(0.95, max(0.55, 0.55 + (abs(score) / total_hits) * 0.35))
178
+ if score > 0:
179
+ return {"label": "bullish", "confidence": round(confidence, 4)}
180
+ if score < 0:
181
+ return {"label": "bearish", "confidence": round(confidence, 4)}
182
+ return {"label": "neutral", "confidence": round(0.5 + (positive_hits / total_hits) * 0.1, 4)}
183
+
184
+ def predict(self, text: str) -> dict[str, float | str]:
185
+ if self._pipeline is None:
186
+ return self._rule_based_predict(text)
187
+ with torch.no_grad():
188
+ output = self._pipeline(text)
189
+ prediction = output[0] if isinstance(output, list) else output
190
+ return {
191
+ "label": _normalize_label(prediction.get("label", "unknown")),
192
+ "confidence": float(prediction.get("score", 0.0)),
193
+ }
194
+
195
+ def predict_batch(self, texts: list[str]) -> list[dict[str, float | str]]:
196
+ if self._pipeline is None:
197
+ return [self._rule_based_predict(text) for text in texts]
198
+ with torch.no_grad():
199
+ output = self._pipeline(texts)
200
+ if isinstance(output, dict):
201
+ output = [output]
202
+ results = []
203
+ for prediction in output:
204
+ if isinstance(prediction, list):
205
+ prediction = prediction[0]
206
+ results.append({
207
+ "label": _normalize_label(prediction.get("label", "unknown")),
208
+ "confidence": float(prediction.get("score", 0.0)),
209
+ })
210
+ return results
main.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ import logging
3
+ import os
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.responses import JSONResponse
8
+ from starlette.requests import Request
9
+ from fastapi.exceptions import RequestValidationError
10
+
11
+ from app.api.routes import router as api_router
12
+ from app.services.model_service import SentimentModelManager
13
+
14
+
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
18
+ )
19
+ logger = logging.getLogger("finstream")
20
+
21
+ MODEL_NAME = os.getenv("MODEL_NAME", "hitenvk22/FinStream-Sentiment")
22
+
23
+
24
+ @asynccontextmanager
25
+ async def lifespan(app: FastAPI):
26
+ mm = SentimentModelManager(model_name=MODEL_NAME)
27
+ app.state.model_manager = mm
28
+ await mm.load_async()
29
+ logger.info("Device: %s | Ready: %s", mm.device, mm.is_ready)
30
+ yield
31
+
32
+
33
+ app = FastAPI(
34
+ title="FinStream Sentiment API",
35
+ version="1.0.0",
36
+ description="GPU-accelerated FinStream sentiment inference on Hugging Face Spaces",
37
+ lifespan=lifespan,
38
+ )
39
+
40
+
41
+ @app.get("/")
42
+ async def root():
43
+ mm = getattr(app.state, "model_manager", None)
44
+ return {
45
+ "service": "FinStream Sentiment API",
46
+ "version": "1.0.0",
47
+ "mode": "transformers",
48
+ "status": "running",
49
+ "model": MODEL_NAME,
50
+ "device": mm.device if mm else "unknown",
51
+ "endpoints": {
52
+ "predict": "/predict",
53
+ "analyze_csv": "/analyze-csv",
54
+ "health": "/health",
55
+ },
56
+ }
57
+
58
+
59
+ app.add_middleware(
60
+ CORSMiddleware,
61
+ allow_origins=["*"],
62
+ allow_credentials=False,
63
+ allow_methods=["*"],
64
+ allow_headers=["*"],
65
+ )
66
+
67
+ app.include_router(api_router)
68
+
69
+
70
+ @app.exception_handler(RequestValidationError)
71
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
72
+ logger.warning("Validation error on %s: %s", request.url.path, exc.errors())
73
+ return JSONResponse(
74
+ status_code=422,
75
+ content={"detail": "Invalid request payload", "errors": exc.errors()},
76
+ )
77
+
78
+
79
+ @app.exception_handler(Exception)
80
+ async def unhandled_exception_handler(request: Request, exc: Exception):
81
+ logger.exception("Unhandled error on %s", request.url.path)
82
+ return JSONResponse(
83
+ status_code=500,
84
+ content={"detail": "Internal server error"},
85
+ )
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.30.0
3
+ transformers>=4.42.0
4
+ torch>=2.2.0
5
+ pydantic-settings>=2.4.0
6
+ pandas>=2.2.2
7
+ numpy>=1.26.4
8
+ python-multipart>=0.0.9
9
+ reportlab>=4.2.2
10
+ anyio>=4.4.0