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)}" ) )