MIF_E31230745/public/python_backend/app.py

230 lines
7.6 KiB
Python

import os
import json
import time
import logging
from typing import Any, Dict, List
import requests
from flask import Flask, jsonify, request
from dotenv import load_dotenv
BASE_DIR = os.path.dirname(__file__)
ROOT_ENV = os.path.abspath(os.path.join(BASE_DIR, "..", "..", ".env"))
load_dotenv(ROOT_ENV)
load_dotenv(os.path.join(BASE_DIR, ".env"))
app = Flask(__name__)
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
BACKEND_TOKEN = os.getenv("BACKEND_TOKEN", "")
GEMINI_BASE_URL = os.getenv("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/models")
TIMEOUT_SECONDS = int(os.getenv("GEMINI_TIMEOUT", "30"))
MAJORS_FILE_PATH = os.getenv("MAJORS_FILE_PATH", os.path.join(BASE_DIR, "majors_data.json"))
LOG_FILE_PATH = os.getenv("PY_BACKEND_LOG_FILE", os.path.join(BASE_DIR, "backend.log"))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler(LOG_FILE_PATH, encoding="utf-8"),
],
)
logger = logging.getLogger("python_backend")
def _log(message: str) -> None:
logger.info(message)
def _is_authorized() -> bool:
if not BACKEND_TOKEN:
return True
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return False
token = auth_header.replace("Bearer ", "", 1).strip()
return token == BACKEND_TOKEN
def _extract_text(data: Dict[str, Any]) -> str:
return (
data.get("candidates", [{}])[0]
.get("content", {})
.get("parts", [{}])[0]
.get("text", "")
)
def _load_majors_file() -> Dict[str, Any]:
try:
with open(MAJORS_FILE_PATH, "r", encoding="utf-8") as handle:
data = json.load(handle)
except FileNotFoundError:
return {
"ok": False,
"message": f"File data jurusan tidak ditemukan: {MAJORS_FILE_PATH}",
"data": {"majors": []},
}
except json.JSONDecodeError as exc:
return {
"ok": False,
"message": f"Format JSON tidak valid: {exc}",
"data": {"majors": []},
}
majors = data.get("majors") if isinstance(data, dict) else []
if not isinstance(majors, list):
majors = []
return {
"ok": True,
"message": "ok",
"data": {
"schema_version": data.get("schema_version", "unknown") if isinstance(data, dict) else "unknown",
"last_updated": data.get("last_updated", "unknown") if isinstance(data, dict) else "unknown",
"notes": data.get("notes", "") if isinstance(data, dict) else "",
"majors": majors,
},
}
def _build_majors_context_text(majors_data: Dict[str, Any]) -> str:
majors = majors_data.get("majors", [])
if not majors:
return ""
lines = [
"DATA JURUSAN RESMI (WAJIB JADI ACUAN UTAMA)",
f"schema_version: {majors_data.get('schema_version', 'unknown')}",
f"last_updated: {majors_data.get('last_updated', 'unknown')}",
"Gunakan data berikut untuk menjawab informasi jurusan secara konsisten.",
"Jika ada konflik dengan asumsi model, utamakan data ini.",
]
for index, major in enumerate(majors, start=1):
name = major.get("name", "-")
description = major.get("description", "-")
prospects = major.get("career_prospects", "-")
preferences = ", ".join(major.get("study_preferences", [])) or "-"
keywords = ", ".join(major.get("keywords", [])) or "-"
lines.append(
f"{index}. {name} | deskripsi: {description} | preferensi studi: {preferences} | prospek: {prospects} | kata kunci: {keywords}"
)
return "\n".join(lines)
def _inject_majors_context(payload: Dict[str, Any], majors_context: str) -> Dict[str, Any]:
if not majors_context:
return payload
contents = payload.get("contents", [])
if not isinstance(contents, list) or not contents:
return payload
first = contents[0]
if isinstance(first, dict) and first.get("role") == "user":
parts = first.get("parts", [])
if isinstance(parts, list) and parts and isinstance(parts[0], dict):
original_text = str(parts[0].get("text", ""))
parts[0]["text"] = f"{majors_context}\n\n{original_text}"
return payload
contents.insert(0, {"role": "user", "parts": [{"text": majors_context}]})
return payload
@app.get("/health")
def health() -> Any:
majors_result = _load_majors_file()
majors_count = len(majors_result["data"].get("majors", []))
_log("[PY-BACKEND] GET /health")
return jsonify(
{
"ok": True,
"gemini_key_configured": bool(GEMINI_API_KEY),
"majors_file": MAJORS_FILE_PATH,
"majors_loaded": majors_count,
"majors_valid": majors_result["ok"],
}
)
@app.get("/api/majors")
def majors() -> Any:
result = _load_majors_file()
status = 200 if result["ok"] else 500
return jsonify(result), status
@app.post("/api/chat")
def chat() -> Any:
_log("[PY-BACKEND] POST /api/chat")
if not _is_authorized():
_log("[PY-BACKEND] Unauthorized request")
return jsonify({"success": False, "message": "Unauthorized"}), 401
if not GEMINI_API_KEY:
return jsonify({"success": False, "message": "GEMINI_API_KEY belum diset di backend Python"}), 500
body = request.get_json(silent=True) or {}
payload = body.get("payload")
models = body.get("models", ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite"])
if not isinstance(payload, dict) or not payload.get("contents"):
_log("[PY-BACKEND] Invalid payload")
return jsonify({"success": False, "message": "payload tidak valid"}), 422
majors_result = _load_majors_file()
if majors_result["ok"]:
majors_context = _build_majors_context_text(majors_result["data"])
payload = _inject_majors_context(payload, majors_context)
_log(f"[PY-BACKEND] Majors context loaded: {len(majors_result['data'].get('majors', []))} jurusan")
else:
_log(f"[PY-BACKEND] Warning: {majors_result['message']}")
last_error = ""
for model in models:
url = f"{GEMINI_BASE_URL}/{model}:generateContent?key={GEMINI_API_KEY}"
try:
resp = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=TIMEOUT_SECONDS,
)
except requests.RequestException as exc:
last_error = str(exc)
continue
if resp.ok:
data = resp.json()
text = _extract_text(data)
if text:
_log(f"[PY-BACKEND] Success using model: {model}")
return jsonify({"success": True, "message": text, "model": model})
last_error = "Respons model tidak berisi teks"
continue
if resp.status_code in (404, 429):
if resp.status_code == 429:
time.sleep(1)
last_error = f"Model {model} gagal dengan status {resp.status_code}"
continue
last_error = f"Gemini error {resp.status_code}: {resp.text[:300]}"
_log(f"[PY-BACKEND] Failed all models: {last_error}")
return jsonify({"success": False, "message": "Semua model gagal", "error": last_error}), 502
if __name__ == "__main__":
host = os.getenv("PY_BACKEND_HOST", "0.0.0.0")
port = int(os.getenv("PY_BACKEND_PORT", "5000"))
debug = os.getenv("PY_BACKEND_DEBUG", "true").lower() == "true"
app.run(host=host, port=port, debug=debug)