488 lines
12 KiB
Python
488 lines
12 KiB
Python
from uuid import uuid4
|
|
|
|
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel, Field
|
|
from typing import Literal
|
|
from training_input import validate_column
|
|
from model_registry import bundle, history, activate, queue, train_job
|
|
|
|
from model_service import (
|
|
analisis_teks,
|
|
preprocessing_detail,
|
|
get_model_info
|
|
)
|
|
|
|
from database import (
|
|
test_db_connection,
|
|
get_database_summary,
|
|
save_analysis_batch,
|
|
get_analysis_batch,
|
|
get_analysis_history,
|
|
save_single_prediction,
|
|
get_single_prediction_history,
|
|
get_single_prediction
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# FASTAPI
|
|
# ============================================================
|
|
|
|
app = FastAPI(
|
|
title="S-Learn Sentiment Analysis API",
|
|
description=(
|
|
"Backend API analisis sentimen S-Learn "
|
|
"menggunakan TF-IDF dan Logistic Regression."
|
|
),
|
|
version="1.0.0"
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# CORS
|
|
# ============================================================
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origin_regex=r"http://(localhost|127\.0\.0\.1):\d+",
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"]
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# REQUEST MODEL
|
|
# ============================================================
|
|
|
|
class TextRequest(BaseModel):
|
|
text: str
|
|
|
|
|
|
class CSVReviewItem(BaseModel):
|
|
source_row_number: int
|
|
text: str
|
|
|
|
|
|
class CSVAnalysisRequest(BaseModel):
|
|
file_name: str
|
|
selected_column: str
|
|
reviews: list[CSVReviewItem]
|
|
|
|
|
|
class TrainingRequest(BaseModel):
|
|
selected_column: str = Field(min_length=1, max_length=200)
|
|
confirmed_review_column: Literal[True]
|
|
file_name: str = Field(min_length=1, max_length=200)
|
|
reviews: list[str] = Field(min_length=1, max_length=5000)
|
|
|
|
|
|
@app.get('/models')
|
|
def model_history():
|
|
return {'status': 'success', 'data': history()}
|
|
|
|
|
|
@app.get('/model-info/original')
|
|
def original_model_info():
|
|
return {'status': 'success', 'data': bundle('v1')[2]}
|
|
|
|
|
|
@app.post('/models/{version}/activate')
|
|
def activate_model(version: str):
|
|
try:
|
|
return {'status': 'success', 'data': activate(version)}
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
|
|
|
|
@app.post('/training', status_code=202)
|
|
def training(request: TrainingRequest, background: BackgroundTasks):
|
|
try:
|
|
validate_column(request.selected_column)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
if any(len(text) > 10000 for text in request.reviews):
|
|
raise HTTPException(status_code=400, detail='Panjang ulasan maksimal 10.000 karakter.')
|
|
if not any(text.strip() for text in request.reviews):
|
|
raise HTTPException(status_code=400, detail='CSV tidak berisi ulasan.')
|
|
try:
|
|
job_id = queue(request.file_name)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=409, detail=str(error)) from error
|
|
background.add_task(train_job, job_id, request.reviews, request.selected_column)
|
|
return {'status': 'success', 'job_id': job_id}
|
|
|
|
|
|
# ============================================================
|
|
# ROOT
|
|
# ============================================================
|
|
|
|
@app.get("/")
|
|
def root():
|
|
return {
|
|
"message": "S-Learn Sentiment Analysis API",
|
|
"status": "running"
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# HEALTH CHECK BACKEND
|
|
# ============================================================
|
|
|
|
@app.get("/health")
|
|
def health_check():
|
|
return {
|
|
"status": "ok",
|
|
"service": "api_slearn",
|
|
"model": "loaded"
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# HEALTH CHECK DATABASE
|
|
# ============================================================
|
|
|
|
@app.get("/database/health")
|
|
def database_health():
|
|
try:
|
|
koneksi = test_db_connection()
|
|
summary = get_database_summary()
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": "Koneksi MySQL berhasil.",
|
|
"database": koneksi["database"],
|
|
"tables": koneksi["tables"],
|
|
"summary": summary
|
|
}
|
|
|
|
except ConnectionError as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=str(error)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# INFORMASI MODEL
|
|
# ============================================================
|
|
|
|
@app.get("/model-info")
|
|
def model_info():
|
|
return {
|
|
"status": "success",
|
|
"data": get_model_info()
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# PREPROCESSING SATU ULASAN
|
|
# ============================================================
|
|
|
|
@app.post("/preprocess")
|
|
def preprocess(request: TextRequest):
|
|
try:
|
|
hasil = preprocessing_detail(
|
|
request.text
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": hasil
|
|
}
|
|
|
|
except ValueError as error:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=str(error)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# PREDIKSI SATU ULASAN
|
|
# ============================================================
|
|
|
|
@app.post("/predict")
|
|
def predict(request: TextRequest):
|
|
try:
|
|
# ----------------------------------------------------
|
|
# 1. ANALISIS TEKS
|
|
# ----------------------------------------------------
|
|
|
|
hasil = analisis_teks(
|
|
request.text
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# 2. SIMPAN HASIL KE MYSQL
|
|
# ----------------------------------------------------
|
|
|
|
try:
|
|
prediction_id = save_single_prediction(
|
|
hasil
|
|
)
|
|
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=(
|
|
"Prediksi berhasil dilakukan, "
|
|
"tetapi hasil gagal disimpan "
|
|
"ke database. "
|
|
f"{str(error)}"
|
|
)
|
|
)
|
|
|
|
# ----------------------------------------------------
|
|
# 3. KEMBALIKAN HASIL
|
|
# ----------------------------------------------------
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": (
|
|
"Prediksi berhasil dilakukan "
|
|
"dan disimpan ke riwayat."
|
|
),
|
|
"data": {
|
|
**hasil,
|
|
"prediction_id": prediction_id
|
|
}
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
|
|
except ValueError as error:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=str(error)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# RIWAYAT PREDIKSI SATU ULASAN
|
|
# ============================================================
|
|
|
|
@app.get("/predictions")
|
|
def prediction_history():
|
|
try:
|
|
history = get_single_prediction_history()
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": history,
|
|
"total": len(history)
|
|
}
|
|
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=(
|
|
"Gagal mengambil riwayat "
|
|
"Prediksi Satu Ulasan. "
|
|
f"{str(error)}"
|
|
)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# DETAIL SATU PREDIKSI
|
|
# ============================================================
|
|
|
|
@app.get("/predictions/{prediction_id}")
|
|
def prediction_detail(
|
|
prediction_id: int
|
|
):
|
|
if prediction_id < 1:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="ID prediksi tidak valid."
|
|
)
|
|
|
|
try:
|
|
hasil = get_single_prediction(
|
|
prediction_id
|
|
)
|
|
|
|
if hasil is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Riwayat prediksi tidak ditemukan."
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": hasil
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=(
|
|
"Gagal mengambil detail prediksi. "
|
|
f"{str(error)}"
|
|
)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# ANALISIS CSV
|
|
# ============================================================
|
|
|
|
@app.post("/analysis/csv")
|
|
def analysis_csv(
|
|
request: CSVAnalysisRequest
|
|
):
|
|
if not request.file_name.strip():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Nama file CSV tidak boleh kosong."
|
|
)
|
|
|
|
if not request.selected_column.strip():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Kolom ulasan belum dipilih."
|
|
)
|
|
|
|
if not request.reviews:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Tidak ada ulasan yang dapat dianalisis."
|
|
)
|
|
|
|
batch_id = (
|
|
"BATCH-"
|
|
+ uuid4().hex[:12].upper()
|
|
)
|
|
|
|
hasil_analisis = []
|
|
snapshot = bundle()
|
|
|
|
for review in request.reviews:
|
|
|
|
if review.source_row_number < 1:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Nomor baris CSV tidak valid."
|
|
)
|
|
|
|
try:
|
|
hasil = analisis_teks(
|
|
review.text, snapshot=snapshot
|
|
)
|
|
|
|
except ValueError as error:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"Baris {review.source_row_number}: "
|
|
f"{str(error)}"
|
|
)
|
|
)
|
|
|
|
hasil[
|
|
"source_row_number"
|
|
] = review.source_row_number
|
|
|
|
hasil_analisis.append(
|
|
hasil
|
|
)
|
|
|
|
try:
|
|
batch = save_analysis_batch(
|
|
batch_id=batch_id,
|
|
file_name=request.file_name.strip(),
|
|
selected_column=request.selected_column.strip(),
|
|
results=hasil_analisis
|
|
)
|
|
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=(
|
|
"Analisis berhasil dilakukan, "
|
|
"tetapi hasil gagal disimpan "
|
|
"ke database. "
|
|
f"{str(error)}"
|
|
)
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": (
|
|
"CSV berhasil dianalisis dan "
|
|
"disimpan ke database."
|
|
),
|
|
"data": {
|
|
**batch,
|
|
"results": hasil_analisis
|
|
}
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# RIWAYAT SEMUA BATCH CSV
|
|
# ============================================================
|
|
|
|
@app.get("/analysis")
|
|
def analysis_history():
|
|
try:
|
|
history = get_analysis_history()
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": history,
|
|
"total": len(history)
|
|
}
|
|
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=(
|
|
"Gagal mengambil riwayat analisis. "
|
|
f"{str(error)}"
|
|
)
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# DETAIL SATU BATCH CSV
|
|
# ============================================================
|
|
|
|
@app.get("/analysis/{batch_id}")
|
|
def analysis_detail(
|
|
batch_id: str
|
|
):
|
|
try:
|
|
hasil = get_analysis_batch(
|
|
batch_id.strip()
|
|
)
|
|
|
|
if hasil is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Batch analisis tidak ditemukan."
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": hasil
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
|
|
except Exception as error:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=(
|
|
"Gagal mengambil hasil analisis. "
|
|
f"{str(error)}"
|
|
)
|
|
)
|