Liva21's picture
feat: Financial Sentiment API — FinBERT fine-tuned, FastAPI, Docker, TR/EN multilingual
7701077
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, field_validator
from typing import Optional
import time, os
from src.database import init_db, log_request
from src.multilingual import load_all_models, run_inference_multilingual
from src.enrichment import enrich
MODEL_DIR = os.getenv("MODEL_DIR", "models/finbert-finetuned")
MAX_LENGTH = int(os.getenv("MAX_LENGTH", "128"))
app = FastAPI(
title="Financial Sentiment Analysis API",
description="Türkçe ve İngilizce finansal metinleri analiz eder.",
version="3.0.0",
)
@app.on_event("startup")
async def startup():
init_db()
load_all_models()
class SentimentRequest(BaseModel):
text: str
@field_validator("text")
@classmethod
def validate(cls, v):
v = v.strip()
if not v:
raise ValueError("text boş olamaz")
if len(v) > 2000:
raise ValueError("text 2000 karakteri aşamaz")
return v
class SentimentScore(BaseModel):
negative: float
neutral: float
positive: float
class SentimentResponse(BaseModel):
text: str
translated_text: Optional[str] = None
sentiment: str
confidence: float
language: str
scores: SentimentScore
keywords: list[str] # ← YENİ
risk_score: float # ← YENİ
risk_level: str # ← YENİ: LOW / MEDIUM / HIGH
latency_ms: float
class BatchRequest(BaseModel):
texts: list[str]
@field_validator("texts")
@classmethod
def validate_batch(cls, v):
if not v:
raise ValueError("texts boş olamaz")
if len(v) > 32:
raise ValueError("max 32 metin")
return v
class BatchResponse(BaseModel):
results: list[SentimentResponse]
latency_ms: float
@app.get("/", tags=["Health"])
def root():
return {"status": "ok", "version": "3.0.0"}
@app.get("/health", tags=["Health"])
def health():
return {"status": "ok", "model_dir": MODEL_DIR}
@app.get("/monitoring/stats", tags=["Monitoring"])
def monitoring_stats():
from src.database import get_stats
return get_stats()
@app.post("/predict", response_model=SentimentResponse, tags=["Inference"])
def predict(req: SentimentRequest):
t0 = time.perf_counter()
result = run_inference_multilingual([req.text])[0]
# Enrichment — İngilizce metin üzerinde çalışır
analysis_text = result.get("translated_text") or result["text"]
enriched = enrich(analysis_text, result["sentiment"], result["confidence"])
latency = round((time.perf_counter() - t0) * 1000, 2)
log_request(
text = req.text,
sentiment = result["sentiment"],
confidence= result["confidence"],
latency_ms= latency,
endpoint = "/predict",
)
return SentimentResponse(
text = result["text"],
translated_text = result.get("translated_text"),
sentiment = result["sentiment"],
confidence = result["confidence"],
language = result["language"],
scores = SentimentScore(**result["scores"]),
keywords = enriched["keywords"],
risk_score = enriched["risk_score"],
risk_level = enriched["risk_level"],
latency_ms = latency,
)
@app.post("/predict/batch", response_model=BatchResponse, tags=["Inference"])
def predict_batch(req: BatchRequest):
t0 = time.perf_counter()
results = run_inference_multilingual(req.texts)
latency = round((time.perf_counter() - t0) * 1000, 2)
responses = []
for r in results:
analysis_text = r.get("translated_text") or r["text"]
enriched = enrich(analysis_text, r["sentiment"], r["confidence"])
log_request(
text = r["text"],
sentiment = r["sentiment"],
confidence= r["confidence"],
latency_ms= latency / len(results),
endpoint = "/predict/batch",
batch_size= len(req.texts),
)
responses.append(SentimentResponse(
text = r["text"],
translated_text = r.get("translated_text"),
sentiment = r["sentiment"],
confidence = r["confidence"],
language = r["language"],
scores = SentimentScore(**r["scores"]),
keywords = enriched["keywords"],
risk_score = enriched["risk_score"],
risk_level = enriched["risk_level"],
latency_ms = latency,
))
return BatchResponse(results=responses, latency_ms=latency)