Improve Gemini chat flow, add Python backend gateway, and stabilize tests
This commit is contained in:
parent
fcac0ac627
commit
3f0ce730a4
|
|
@ -56,3 +56,7 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
|
|||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
GEMINI_API_KEY=
|
||||
GEMINI_BACKEND_URL=http://127.0.0.1:8001
|
||||
GEMINI_BACKEND_TOKEN=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
PANDUAN GABUNGAN: CHATBOT + API GEMINI + BACKEND PYTHON (app.py)
|
||||
|
||||
============================================================
|
||||
A. KENAPA SEBELUMNYA TIDAK ADA PENJELASAN PYTHON app.py?
|
||||
============================================================
|
||||
|
||||
Sebelumnya arsitektur chatbot di project ini langsung memanggil Gemini dari Laravel (PHP)
|
||||
melalui file app/Services/GeminiService.php.
|
||||
|
||||
Artinya:
|
||||
- Tidak ada service Python terpisah.
|
||||
- API key Gemini dibaca langsung dari env Laravel.
|
||||
|
||||
Sekarang sudah disiapkan backend Python (app.py) agar alur lebih fleksibel:
|
||||
- Laravel -> Python backend -> Gemini API
|
||||
- Kunci API dapat difokuskan di backend Python.
|
||||
|
||||
|
||||
============================================================
|
||||
B. STRUKTUR BARU YANG DITAMBAHKAN
|
||||
============================================================
|
||||
|
||||
Folder baru:
|
||||
- public/python_backend/app.py
|
||||
- public/python_backend/requirements.txt
|
||||
|
||||
Perubahan konfigurasi:
|
||||
- config/services.php
|
||||
tambah:
|
||||
- services.gemini.backend_url
|
||||
- services.gemini.backend_token
|
||||
|
||||
- .env.example
|
||||
tambah:
|
||||
GEMINI_API_KEY=
|
||||
GEMINI_BACKEND_URL=http://127.0.0.1:8001
|
||||
GEMINI_BACKEND_TOKEN=
|
||||
|
||||
Perubahan service Laravel:
|
||||
- app/Services/GeminiService.php
|
||||
- jika GEMINI_BACKEND_URL terisi, Laravel coba kirim request ke Python backend dulu
|
||||
- jika backend Python gagal, otomatis fallback ke pemanggilan Gemini langsung (mode lama)
|
||||
|
||||
|
||||
============================================================
|
||||
C. CARA KERJA ARSITEKTUR CHATBOT SEKARANG
|
||||
============================================================
|
||||
|
||||
1) User kirim chat dari UI Laravel.
|
||||
2) ChatbotController memanggil GeminiService::chat(...).
|
||||
3) GeminiService menyiapkan payload chat (contents + generationConfig).
|
||||
4) Jika GEMINI_BACKEND_URL terisi:
|
||||
- request dikirim ke Python: POST /api/chat
|
||||
5) Python backend mencoba model berurutan:
|
||||
- gemini-2.5-flash
|
||||
- gemini-2.0-flash
|
||||
- gemini-2.0-flash-lite
|
||||
6) Jika Python sukses -> balasan dikembalikan ke Laravel.
|
||||
7) Jika Python gagal -> Laravel fallback ke direct Gemini API.
|
||||
8) Jika semua gagal -> fallback response lokal di GeminiService.
|
||||
|
||||
|
||||
============================================================
|
||||
D. ENDPOINT PYTHON BACKEND
|
||||
============================================================
|
||||
|
||||
1) GET /health
|
||||
Fungsi: cek backend hidup + status key
|
||||
|
||||
2) POST /api/chat
|
||||
Body JSON:
|
||||
{
|
||||
"payload": { ... Gemini payload ... },
|
||||
"models": ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite"]
|
||||
}
|
||||
|
||||
Response sukses:
|
||||
{
|
||||
"success": true,
|
||||
"message": "...jawaban model...",
|
||||
"model": "gemini-2.5-flash"
|
||||
}
|
||||
|
||||
Response gagal:
|
||||
{
|
||||
"success": false,
|
||||
"message": "Semua model gagal",
|
||||
"error": "...detail..."
|
||||
}
|
||||
|
||||
|
||||
============================================================
|
||||
E. BATASAN / LIMIT CHATBOT (REKOMENDASI PRAKTIS)
|
||||
============================================================
|
||||
|
||||
Saat ini yang sudah ada:
|
||||
- history chat dari frontend dibatasi maksimal 20 item.
|
||||
- text history dipotong maksimal 1500 karakter per item sebelum dikirim.
|
||||
|
||||
Batasan yang disarankan agar stabil dan hemat biaya:
|
||||
1) Batasi input user per pesan (contoh: 500-1000 karakter).
|
||||
2) Batasi requests per menit per user (rate limit).
|
||||
3) Batasi token output (sudah ada maxOutputTokens=4096, bisa diturunkan jika perlu).
|
||||
4) Simpan cooldown jika terkena 429 berulang.
|
||||
5) Tetapkan timeout request (sudah ada timeout di PHP dan Python).
|
||||
6) Log usage untuk audit (siapa, kapan, berapa panjang pesan).
|
||||
|
||||
|
||||
============================================================
|
||||
F. LANGKAH MENJALANKAN BACKEND PYTHON
|
||||
============================================================
|
||||
|
||||
1) Masuk ke folder python backend
|
||||
cd public/python_backend
|
||||
|
||||
2) Buat virtual environment (opsional tapi disarankan)
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
|
||||
3) Install dependency
|
||||
pip install -r requirements.txt
|
||||
|
||||
4) Set environment variable untuk backend Python
|
||||
(PowerShell contoh)
|
||||
$env:GEMINI_API_KEY="ISI_API_KEY_GEMINI"
|
||||
$env:BACKEND_TOKEN="TOKEN_RAHASIA_BACKEND" # opsional tapi disarankan
|
||||
$env:PY_BACKEND_HOST="127.0.0.1"
|
||||
$env:PY_BACKEND_PORT="8001"
|
||||
|
||||
5) Jalankan server Python
|
||||
python app.py
|
||||
|
||||
6) Test health endpoint
|
||||
buka: http://127.0.0.1:8001/health
|
||||
|
||||
|
||||
============================================================
|
||||
G. KONFIGURASI LARAVEL AGAR PAKAI BACKEND PYTHON
|
||||
============================================================
|
||||
|
||||
Di .env Laravel, isi:
|
||||
- GEMINI_BACKEND_URL=http://127.0.0.1:8001
|
||||
- GEMINI_BACKEND_TOKEN=TOKEN_RAHASIA_BACKEND
|
||||
|
||||
Opsional:
|
||||
- GEMINI_API_KEY=... (tetap boleh ada untuk fallback direct mode)
|
||||
|
||||
Setelah ubah env:
|
||||
- php artisan optimize:clear
|
||||
|
||||
|
||||
============================================================
|
||||
H. KEAMANAN API KEY GEMINI
|
||||
============================================================
|
||||
|
||||
Praktik aman:
|
||||
1) Jangan hardcode API key di kode sumber.
|
||||
2) Simpan API key di env backend Python.
|
||||
3) Gunakan BACKEND_TOKEN agar endpoint Python tidak bisa dipakai bebas.
|
||||
4) Jalankan backend Python di localhost/internal network.
|
||||
5) Jangan commit file .env ke GitHub.
|
||||
|
||||
|
||||
============================================================
|
||||
I. SATU FILE RINGKAS UNTUK SIDANG (NARASI SINGKAT)
|
||||
============================================================
|
||||
|
||||
Arsitektur chatbot awal memanggil Gemini langsung dari Laravel. Agar manajemen API key
|
||||
lebih aman dan fleksibel, ditambahkan backend Python app.py sebagai gateway. Laravel
|
||||
mengirim payload ke Python endpoint /api/chat, lalu Python meneruskan ke Gemini dengan
|
||||
mekanisme pemilihan model bertahap. Jika backend Python gagal, Laravel tetap memiliki
|
||||
mekanisme fallback ke direct Gemini sehingga layanan chatbot lebih robust. Sistem juga
|
||||
menerapkan pembatasan history dan panjang pesan, dan dapat ditingkatkan dengan rate limit
|
||||
per pengguna agar performa tetap stabil.
|
||||
|
||||
|
||||
============================================================
|
||||
J. CATATAN PENTING
|
||||
============================================================
|
||||
|
||||
- Jika GEMINI_BACKEND_URL tidak diisi, sistem tetap berjalan seperti mode lama.
|
||||
- Jika GEMINI_BACKEND_URL diisi tapi backend Python mati, sistem otomatis fallback direct.
|
||||
- Untuk mode produksi, disarankan Python backend aktif + token + monitoring log.
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
PENJELASAN SISTEM REKOMENDASI JURUSAN POLIJE
|
||||
(Weighted Naive Bayes)
|
||||
|
||||
============================================================
|
||||
1) BOBOT KRITERIA
|
||||
============================================================
|
||||
|
||||
Sistem menggunakan 5 kriteria utama:
|
||||
1. Nilai akademik
|
||||
2. Minat
|
||||
3. Preferensi studi lanjutan
|
||||
4. Prestasi
|
||||
5. Cita-cita
|
||||
|
||||
Bobot per jurusan berbeda-beda (disesuaikan karakter jurusan):
|
||||
|
||||
- Produksi Pertanian:
|
||||
nilai=0.40, minat=0.35, pref=0.15, prestasi=0.05, cita_cita=0.05
|
||||
|
||||
- Teknologi Pertanian:
|
||||
nilai=0.50, minat=0.25, pref=0.15, prestasi=0.05, cita_cita=0.05
|
||||
|
||||
- Peternakan:
|
||||
nilai=0.40, minat=0.40, pref=0.10, prestasi=0.05, cita_cita=0.05
|
||||
|
||||
- Manajemen Agribisnis:
|
||||
nilai=0.35, minat=0.40, pref=0.15, prestasi=0.05, cita_cita=0.05
|
||||
|
||||
- Teknologi Informasi:
|
||||
nilai=0.45, minat=0.35, pref=0.12, prestasi=0.05, cita_cita=0.03
|
||||
|
||||
- Teknik:
|
||||
nilai=0.42, minat=0.38, pref=0.12, prestasi=0.05, cita_cita=0.03
|
||||
|
||||
- Kesehatan:
|
||||
nilai=0.45, minat=0.35, pref=0.10, prestasi=0.05, cita_cita=0.05
|
||||
|
||||
- Bahasa, Komunikasi, dan Pariwisata:
|
||||
nilai=0.30, minat=0.40, pref=0.15, prestasi=0.08, cita_cita=0.07
|
||||
|
||||
- Bisnis:
|
||||
nilai=0.35, minat=0.40, pref=0.15, prestasi=0.05, cita_cita=0.05
|
||||
|
||||
Catatan penting:
|
||||
Jika prestasi tidak diisi, bobot prestasi diubah menjadi 0.00,
|
||||
lalu bobot kriteria lain dinormalisasi agar total bobot tetap 1.00.
|
||||
|
||||
|
||||
============================================================
|
||||
2) CARA PERHITUNGAN NAIVE BAYES BERBOBOT
|
||||
============================================================
|
||||
|
||||
2.1 Rumus dasar weighted Naive Bayes
|
||||
|
||||
P(Hj|X) proporsional P(Hj) * produk_i( P(xi|Hj) ^ wi )
|
||||
|
||||
Keterangan:
|
||||
- Hj = jurusan ke-j
|
||||
- X = data siswa
|
||||
- wi = bobot kriteria ke-i
|
||||
|
||||
|
||||
2.2 Bentuk log (dipakai di sistem untuk stabilitas numerik)
|
||||
|
||||
logScore(Hj) = log(P(Hj)) +
|
||||
w_nilai * log(P(nilai|Hj)) +
|
||||
w_minat * log(P(minat|Hj)) +
|
||||
w_pref * log(P(pref|Hj)) +
|
||||
w_cita * log(P(cita|Hj)) +
|
||||
w_prestasi * log(P(prestasi|Hj))
|
||||
|
||||
Jika prestasi kosong, suku prestasi tidak dihitung.
|
||||
|
||||
|
||||
2.3 Prior
|
||||
|
||||
Prior dibuat seragam untuk semua jurusan:
|
||||
P(Hj) = 1 / N, dengan N = jumlah jurusan (9)
|
||||
|
||||
|
||||
2.4 Likelihood tiap kriteria
|
||||
|
||||
A) Nilai akademik
|
||||
- Sistem gabungkan dua komponen:
|
||||
1) Kecocokan kategori nilai (Tinggi/Sedang/Rendah)
|
||||
2) Kecocokan bobot mapel jurusan
|
||||
|
||||
- Rumus gabungan:
|
||||
p_nilai = 0.6 * p_nilai_category + 0.4 * p_nilai_subject
|
||||
|
||||
|
||||
B) Minat
|
||||
- Minat teks dipetakan + dihitung keyword coverage.
|
||||
- Rumus:
|
||||
combined = 0.6 * coverage + 0.4 * categoryMatch
|
||||
p_minat = 0.20 + combined * (matchProb_minat - 0.20)
|
||||
|
||||
|
||||
C) Preferensi studi
|
||||
- Jika pref siswa termasuk pref jurusan -> pakai matchProb_pref
|
||||
- Jika tidak cocok -> pakai (1 - matchProb_pref)
|
||||
|
||||
|
||||
D) Cita-cita
|
||||
- Berdasarkan keyword coverage cita-cita terhadap keyword jurusan:
|
||||
p_cita = 0.20 + coverage * (matchProb_cita - 0.20)
|
||||
|
||||
|
||||
E) Prestasi
|
||||
- Prestasi diklasifikasi level:
|
||||
tinggi / sedang / cukup / minimal
|
||||
- Skor dasar prestasi (baseScore) digabung relevansi keyword jurusan:
|
||||
combined = 0.75 * baseScore + 0.25 * relevance
|
||||
p_prestasi = 0.20 + combined * (matchProb_prestasi - 0.20)
|
||||
|
||||
|
||||
2.5 Batas likelihood
|
||||
|
||||
Setiap likelihood dibatasi agar stabil:
|
||||
0.05 <= p <= 0.98
|
||||
|
||||
|
||||
2.6 Softmax (normalisasi akhir)
|
||||
|
||||
Setelah semua logScore dihitung:
|
||||
|
||||
score_j = exp(logScore_j - maxLog)
|
||||
prob_j = score_j / sum(score_k)
|
||||
|
||||
prob_j adalah skor kecocokan akhir jurusan ke-j.
|
||||
Hasil diurutkan dari skor terbesar ke terkecil.
|
||||
|
||||
|
||||
============================================================
|
||||
3) LOGIKA REKOMENDASI (IMPLEMENTASI)
|
||||
============================================================
|
||||
|
||||
3.1 Validasi input
|
||||
|
||||
- Untuk siswa IPA, nilai wajib:
|
||||
mtk, fisika, kimia, biologi
|
||||
|
||||
- Untuk siswa IPS, nilai wajib:
|
||||
ekonomi, geografi, sosiologi, sejarah
|
||||
|
||||
- minat, pref_studi, cita_cita wajib diisi
|
||||
- prestasi boleh kosong
|
||||
|
||||
|
||||
3.2 Preprocessing
|
||||
|
||||
A) Hitung rata-rata nilai
|
||||
- Dari mata pelajaran yang diisi.
|
||||
|
||||
B) Kategorisasi nilai
|
||||
- Tinggi: 85-100
|
||||
- Sedang: 70-84
|
||||
- Rendah: 0-69
|
||||
|
||||
C) Pemetaan minat teks
|
||||
- Logika dan Komputer
|
||||
- Alam dan Tanaman
|
||||
- Pelayanan dan Kesehatan
|
||||
- Manajemen dan Bisnis
|
||||
- Mesin dan Listrik
|
||||
- Umum
|
||||
|
||||
D) Cita-cita
|
||||
- Diproses sebagai teks, lalu dicocokkan dengan keyword jurusan.
|
||||
|
||||
E) Prestasi
|
||||
- Jika ada kata seperti juara/menang/gold -> level tinggi
|
||||
- Jika finalis/medali/peringkat -> level sedang
|
||||
- Jika sertifikat/workshop/kursus -> level cukup
|
||||
- Selain itu -> level minimal
|
||||
- Jika kosong -> tidak dihitung dalam bobot akhir
|
||||
|
||||
|
||||
3.3 Scoring per jurusan
|
||||
|
||||
Untuk setiap jurusan, sistem:
|
||||
1. Ambil bobot dan match_prob jurusan
|
||||
2. Hitung p_nilai, p_minat, p_pref, p_cita, p_prestasi
|
||||
3. Hitung logScore weighted Naive Bayes
|
||||
4. Simpan detail kontribusi tiap kriteria
|
||||
|
||||
|
||||
3.4 Ranking output
|
||||
|
||||
1. LogScore semua jurusan dinormalisasi dengan softmax
|
||||
2. Dibentuk daftar hasil akhir (jurusan + skor)
|
||||
3. Diurutkan menurun berdasarkan skor
|
||||
4. Jurusan tertinggi jadi rekomendasi utama
|
||||
|
||||
|
||||
3.5 Penjelasan hasil
|
||||
|
||||
Sistem juga menghasilkan explanation text untuk tiap kriteria:
|
||||
- alasan berdasarkan nilai
|
||||
- alasan berdasarkan minat
|
||||
- alasan berdasarkan preferensi
|
||||
- alasan berdasarkan cita-cita
|
||||
- alasan berdasarkan prestasi (atau not counted jika kosong)
|
||||
|
||||
|
||||
============================================================
|
||||
4) ALUR SISTEM END-TO-END
|
||||
============================================================
|
||||
|
||||
1. Siswa login ke dashboard.
|
||||
2. Siswa buka halaman Analisis Rekomendasi.
|
||||
3. Siswa isi data:
|
||||
- nilai mapel sesuai kelompok
|
||||
- minat
|
||||
- preferensi studi
|
||||
- cita-cita
|
||||
- prestasi (opsional)
|
||||
4. Sistem validasi input.
|
||||
5. Sistem lakukan preprocessing.
|
||||
6. Sistem hitung skor semua jurusan dengan weighted Naive Bayes.
|
||||
7. Sistem normalisasi skor (softmax).
|
||||
8. Sistem tampilkan ranking 9 jurusan + detail alasan.
|
||||
9. Sistem simpan hasil rekomendasi ke database.
|
||||
10. Data hasil teratas disimpan ke session untuk konteks chatbot.
|
||||
11. Siswa bisa lanjut konsultasi chatbot berbasis hasil rekomendasi.
|
||||
|
||||
|
||||
============================================================
|
||||
5) RINGKASAN UNTUK SIDANG (SIAP BACA)
|
||||
============================================================
|
||||
|
||||
Sistem ini adalah SPK rekomendasi jurusan Polije berbasis weighted Naive Bayes
|
||||
menggunakan 5 kriteria utama: nilai, minat, preferensi studi, prestasi, dan
|
||||
cita-cita. Bobot kriteria disesuaikan per jurusan agar lebih representatif.
|
||||
Perhitungan dilakukan di domain log untuk stabilitas numerik, lalu dinormalisasi
|
||||
menggunakan softmax agar menghasilkan probabilitas akhir. Jika prestasi kosong,
|
||||
atribut prestasi dikeluarkan dari perhitungan dan bobot atribut lain
|
||||
dinormalisasi ulang. Output sistem berupa ranking 9 jurusan, nilai skor, serta
|
||||
penjelasan alasan kecocokan per kriteria sehingga hasil lebih transparan dan
|
||||
mudah dipertanggungjawabkan.
|
||||
|
|
@ -86,7 +86,7 @@ public function students(Request $request)
|
|||
|
||||
public function studentDetail($id)
|
||||
{
|
||||
$student = User::findOrFail($id);
|
||||
$student = User::where('role', 'siswa')->findOrFail($id);
|
||||
$recommendations = Recommendation::where('user_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
class GeminiService
|
||||
{
|
||||
protected $apiKey;
|
||||
protected $backendUrl;
|
||||
protected $backendToken;
|
||||
protected $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models/';
|
||||
|
||||
// Model priority list - try each if previous fails
|
||||
|
|
@ -21,6 +23,8 @@ class GeminiService
|
|||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.gemini.api_key');
|
||||
$this->backendUrl = rtrim((string) config('services.gemini.backend_url', ''), '/');
|
||||
$this->backendToken = (string) config('services.gemini.backend_token', '');
|
||||
}
|
||||
|
||||
public function chat($message, $context = [], $chatHistory = [])
|
||||
|
|
@ -79,6 +83,18 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
]
|
||||
];
|
||||
|
||||
// Jika backend Python dikonfigurasi, gunakan sebagai gateway Gemini terlebih dahulu.
|
||||
if (!empty($this->backendUrl)) {
|
||||
$proxyResponse = $this->sendViaPythonBackend($payload);
|
||||
if (($proxyResponse['success'] ?? false) === true) {
|
||||
return $proxyResponse;
|
||||
}
|
||||
|
||||
Log::warning('Python Gemini backend failed, fallback to direct API', [
|
||||
'error' => $proxyResponse['message'] ?? 'unknown',
|
||||
]);
|
||||
}
|
||||
|
||||
// Try each model until one works
|
||||
foreach ($this->models as $model) {
|
||||
$url = $this->baseUrl . $model . ':generateContent?key=' . $this->apiKey;
|
||||
|
|
@ -126,6 +142,55 @@ public function chat($message, $context = [], $chatHistory = [])
|
|||
}
|
||||
}
|
||||
|
||||
protected function sendViaPythonBackend(array $payload): array
|
||||
{
|
||||
try {
|
||||
$url = $this->backendUrl . '/api/chat';
|
||||
$request = Http::timeout(35)->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
|
||||
if (!empty($this->backendToken)) {
|
||||
$request = $request->withToken($this->backendToken);
|
||||
}
|
||||
|
||||
$response = $request->post($url, [
|
||||
'payload' => $payload,
|
||||
'models' => $this->models,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Python backend error HTTP ' . $response->status(),
|
||||
];
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
if (($data['success'] ?? false) === true && !empty($data['message'])) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $data['message'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $data['message'] ?? 'Python backend response invalid',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Python backend exception', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Python backend exception',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFallbackResponse($message, $context = [], $chatHistory = [])
|
||||
{
|
||||
$jurusan = $context['recommendation'] ?? null;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"laravel/tinker": "^2.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^3.10",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/breeze": "^1.29",
|
||||
"laravel/pint": "^1.0",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "85309d4524da1baae35ac151622b248f",
|
||||
"content-hash": "c9424317cf092c186df062db573db2bf",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
|
@ -5634,6 +5634,259 @@
|
|||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "doctrine/dbal",
|
||||
"version": "3.10.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/dbal.git",
|
||||
"reference": "95d84866bf3c04b2ddca1df7c049714660959aef"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/95d84866bf3c04b2ddca1df7c049714660959aef",
|
||||
"reference": "95d84866bf3c04b2ddca1df7c049714660959aef",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-runtime-api": "^2",
|
||||
"doctrine/deprecations": "^0.5.3|^1",
|
||||
"doctrine/event-manager": "^1|^2",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"psr/cache": "^1|^2|^3",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/cache": "< 1.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/cache": "^1.11|^2.0",
|
||||
"doctrine/coding-standard": "14.0.0",
|
||||
"fig/log-test": "^1",
|
||||
"jetbrains/phpstorm-stubs": "2023.1",
|
||||
"phpstan/phpstan": "2.1.30",
|
||||
"phpstan/phpstan-strict-rules": "^2",
|
||||
"phpunit/phpunit": "9.6.34",
|
||||
"slevomat/coding-standard": "8.27.1",
|
||||
"squizlabs/php_codesniffer": "4.0.1",
|
||||
"symfony/cache": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/console": "For helpful console commands such as SQL execution and import of files."
|
||||
},
|
||||
"bin": [
|
||||
"bin/doctrine-dbal"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\DBAL\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Guilherme Blanco",
|
||||
"email": "guilhermeblanco@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Roman Borschel",
|
||||
"email": "roman@code-factory.org"
|
||||
},
|
||||
{
|
||||
"name": "Benjamin Eberlei",
|
||||
"email": "kontakt@beberlei.de"
|
||||
},
|
||||
{
|
||||
"name": "Jonathan Wage",
|
||||
"email": "jonwage@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
|
||||
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
|
||||
"keywords": [
|
||||
"abstraction",
|
||||
"database",
|
||||
"db2",
|
||||
"dbal",
|
||||
"mariadb",
|
||||
"mssql",
|
||||
"mysql",
|
||||
"oci8",
|
||||
"oracle",
|
||||
"pdo",
|
||||
"pgsql",
|
||||
"postgresql",
|
||||
"queryobject",
|
||||
"sasql",
|
||||
"sql",
|
||||
"sqlite",
|
||||
"sqlserver",
|
||||
"sqlsrv"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/dbal/issues",
|
||||
"source": "https://github.com/doctrine/dbal/tree/3.10.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpdoctrine",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-24T08:03:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
"version": "1.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/deprecations.git",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<=7.5 || >=14"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||
"psr/log": "^1 || ^2 || ^3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Deprecations\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||
"homepage": "https://www.doctrine-project.org/",
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||
},
|
||||
"time": "2026-02-07T07:09:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/event-manager",
|
||||
"version": "2.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/event-manager.git",
|
||||
"reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
|
||||
"reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/common": "<2.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^14",
|
||||
"phpdocumentor/guides-cli": "^1.4",
|
||||
"phpstan/phpstan": "^2.1.32",
|
||||
"phpunit/phpunit": "^10.5.58"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Common\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Guilherme Blanco",
|
||||
"email": "guilhermeblanco@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Roman Borschel",
|
||||
"email": "roman@code-factory.org"
|
||||
},
|
||||
{
|
||||
"name": "Benjamin Eberlei",
|
||||
"email": "kontakt@beberlei.de"
|
||||
},
|
||||
{
|
||||
"name": "Jonathan Wage",
|
||||
"email": "jonwage@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Johannes Schmitt",
|
||||
"email": "schmittjoh@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Marco Pivetta",
|
||||
"email": "ocramius@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
|
||||
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
|
||||
"keywords": [
|
||||
"event",
|
||||
"event dispatcher",
|
||||
"event manager",
|
||||
"event system",
|
||||
"events"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/event-manager/issues",
|
||||
"source": "https://github.com/doctrine/event-manager/tree/2.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpdoctrine",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-29T07:11:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.24.1",
|
||||
|
|
@ -6798,6 +7051,55 @@
|
|||
],
|
||||
"time": "2026-01-27T05:48:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/cache.git",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Cache\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for caching libraries",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"psr",
|
||||
"psr-6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||
},
|
||||
"time": "2021-02-03T23:26:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
"version": "2.0.1",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
'gemini' => [
|
||||
'api_key' => env('GEMINI_API_KEY'),
|
||||
'backend_url' => env('GEMINI_BACKEND_URL'),
|
||||
'backend_token' => env('GEMINI_BACKEND_TOKEN'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@
|
|||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
$table->enum('role', ['admin', 'guru', 'siswa'])->default('siswa')->after('password');
|
||||
$table->enum('role', ['admin', 'guru', 'bk', 'siswa'])->default('siswa')->after('password');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ public function up(): void
|
|||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
$table->dropColumn('role');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
|
|
@ -11,10 +12,10 @@
|
|||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Modify role enum to include 'bk'
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->enum('role', ['admin', 'guru', 'bk', 'siswa'])->default('siswa')->change();
|
||||
});
|
||||
// Use raw SQL for MySQL to avoid Doctrine enum introspection issues in tests.
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE users MODIFY role ENUM('admin','guru','bk','siswa') NOT NULL DEFAULT 'siswa'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,8 +23,8 @@ public function up(): void
|
|||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->enum('role', ['admin', 'guru', 'siswa'])->default('siswa')->change();
|
||||
});
|
||||
if (DB::getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE users MODIFY role ENUM('admin','guru','siswa') NOT NULL DEFAULT 'siswa'");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
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)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"last_updated": "2026-04-21",
|
||||
"notes": "Edit file ini jika ada perubahan data jurusan. Backend Python akan membaca file ini untuk konteks chatbot.",
|
||||
"majors": [
|
||||
{
|
||||
"id": "produksi_pertanian",
|
||||
"name": "Produksi Pertanian",
|
||||
"description": "Mempelajari budidaya tanaman, pengelolaan lahan, dan produksi hasil pertanian modern.",
|
||||
"keywords": ["pertanian", "budidaya", "tanaman", "agronomi", "pangan"],
|
||||
"study_preferences": ["Pertanian & Lingkungan"],
|
||||
"career_prospects": "Petani modern, konsultan pertanian, pengelola perkebunan, peneliti pertanian, agronomis."
|
||||
},
|
||||
{
|
||||
"id": "teknologi_pertanian",
|
||||
"name": "Teknologi Pertanian",
|
||||
"description": "Mengintegrasikan teknologi dengan pertanian, termasuk mekanisasi, otomasi, dan pengolahan hasil pangan.",
|
||||
"keywords": ["teknologi pertanian", "mekanisasi", "otomasi", "smart farming", "teknologi pangan"],
|
||||
"study_preferences": ["Sains & Teknologi", "Pertanian & Lingkungan"],
|
||||
"career_prospects": "Teknisi pertanian, ahli mekanisasi, quality control pangan, peneliti teknologi pangan."
|
||||
},
|
||||
{
|
||||
"id": "peternakan",
|
||||
"name": "Peternakan",
|
||||
"description": "Mempelajari pengelolaan ternak, nutrisi hewan, reproduksi, dan pengolahan produk peternakan.",
|
||||
"keywords": ["peternakan", "ternak", "nutrisi hewan", "farm management", "kesehatan hewan"],
|
||||
"study_preferences": ["Pertanian & Lingkungan", "Kesehatan & Ilmu Hayat"],
|
||||
"career_prospects": "Peternak profesional, konsultan peternakan, manajer peternakan, ahli nutrisi hewan."
|
||||
},
|
||||
{
|
||||
"id": "manajemen_agribisnis",
|
||||
"name": "Manajemen Agribisnis",
|
||||
"description": "Menggabungkan ilmu pertanian dan bisnis, termasuk manajemen usaha, pemasaran, dan kewirausahaan agribisnis.",
|
||||
"keywords": ["agribisnis", "manajemen", "pemasaran", "wirausaha", "supply chain"],
|
||||
"study_preferences": ["Bisnis & Manajemen", "Pertanian & Lingkungan"],
|
||||
"career_prospects": "Manajer agribisnis, entrepreneur pertanian, konsultan pemasaran pertanian, analis pasar komoditas."
|
||||
},
|
||||
{
|
||||
"id": "teknologi_informasi",
|
||||
"name": "Teknologi Informasi",
|
||||
"description": "Mempelajari pengembangan software, data, jaringan, keamanan siber, dan teknologi digital terapan.",
|
||||
"keywords": ["programming", "software", "web", "data", "cybersecurity"],
|
||||
"study_preferences": ["Sains & Teknologi"],
|
||||
"career_prospects": "Software developer, web developer, network engineer, data analyst, cybersecurity specialist."
|
||||
},
|
||||
{
|
||||
"id": "teknik",
|
||||
"name": "Teknik",
|
||||
"description": "Mempelajari mesin, kelistrikan, elektronika, otomasi, dan sistem teknik industri.",
|
||||
"keywords": ["mesin", "listrik", "otomasi", "manufaktur", "mekatronika"],
|
||||
"study_preferences": ["Sains & Teknologi"],
|
||||
"career_prospects": "Teknisi mesin, ahli listrik, engineer industri, maintenance engineer, kontraktor."
|
||||
},
|
||||
{
|
||||
"id": "kesehatan",
|
||||
"name": "Kesehatan",
|
||||
"description": "Mempelajari ilmu kesehatan terapan, gizi, rekam medis, dan pelayanan kesehatan masyarakat.",
|
||||
"keywords": ["kesehatan", "gizi", "rekam medis", "farmasi", "kesehatan masyarakat"],
|
||||
"study_preferences": ["Kesehatan & Ilmu Hayat"],
|
||||
"career_prospects": "Ahli gizi, perekam medis, tenaga kesehatan, asisten apoteker, sanitarian."
|
||||
},
|
||||
{
|
||||
"id": "bahasa_komunikasi_pariwisata",
|
||||
"name": "Bahasa, Komunikasi, dan Pariwisata",
|
||||
"description": "Mempelajari bahasa, komunikasi profesional, perhotelan, layanan publik, dan industri pariwisata.",
|
||||
"keywords": ["bahasa", "komunikasi", "pariwisata", "hospitality", "media"],
|
||||
"study_preferences": ["Sosial & Humaniora", "Bisnis & Manajemen"],
|
||||
"career_prospects": "Tour guide, staf perhotelan, jurnalis, public relation, penerjemah, staf maskapai."
|
||||
},
|
||||
{
|
||||
"id": "bisnis",
|
||||
"name": "Bisnis",
|
||||
"description": "Mempelajari akuntansi, manajemen bisnis, perbankan, keuangan, dan administrasi niaga.",
|
||||
"keywords": ["akuntansi", "manajemen", "perbankan", "keuangan", "analisis bisnis"],
|
||||
"study_preferences": ["Bisnis & Manajemen"],
|
||||
"career_prospects": "Akuntan, staf perbankan, manajer bisnis, marketing executive, analis keuangan."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
flask==3.0.3
|
||||
requests==2.32.3
|
||||
python-dotenv==1.0.1
|
||||
|
|
@ -24,6 +24,8 @@ public function test_new_users_can_register(): void
|
|||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
'nis' => '1234567890',
|
||||
'kelompok_asal' => 'IPA',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public function test_admin_can_add_jurusan_data()
|
|||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$response = $this->actingAs($admin)->post(route('jurusan.store'), [
|
||||
$response = $this->actingAs($admin)->post(route('admin.jurusan.store'), [
|
||||
'nama_jurusan' => 'Informatika',
|
||||
'singkatan' => 'IF',
|
||||
'tujuan_kompetensi' => 'Profesional IT sejati',
|
||||
|
|
@ -41,7 +41,7 @@ public function test_bk_can_add_jurusan_data()
|
|||
{
|
||||
$bk = User::factory()->create(['role' => 'bk']);
|
||||
|
||||
$response = $this->actingAs($bk)->post(route('jurusan.store'), [
|
||||
$response = $this->actingAs($bk)->post(route('bk.jurusan.store'), [
|
||||
'nama_jurusan' => 'Akuntansi',
|
||||
'singkatan' => 'AK',
|
||||
'tujuan_kompetensi' => 'Profesional akuntansi',
|
||||
|
|
@ -65,7 +65,7 @@ public function test_admin_guru_bk_store_validates_email_and_password()
|
|||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
// Invalid email format
|
||||
$response = $this->actingAs($admin)->post(route('admin.store'), [
|
||||
$response = $this->actingAs($admin)->post(route('admin.guru-bk.store'), [
|
||||
'email' => 'invalid-email',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
|
|
@ -74,7 +74,7 @@ public function test_admin_guru_bk_store_validates_email_and_password()
|
|||
$response->assertSessionHasErrors('email');
|
||||
|
||||
// Password too short
|
||||
$response = $this->actingAs($admin)->post(route('admin.store'), [
|
||||
$response = $this->actingAs($admin)->post(route('admin.guru-bk.store'), [
|
||||
'email' => 'valid@example.com',
|
||||
'password' => 'pass',
|
||||
'password_confirmation' => 'pass',
|
||||
|
|
@ -113,7 +113,7 @@ public function test_admin_student_detail_only_accepts_siswa_id()
|
|||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$bk = User::factory()->create(['role' => 'bk']);
|
||||
|
||||
$response = $this->actingAs($admin)->get(route('admin.studentDetail', $bk->id));
|
||||
$response = $this->actingAs($admin)->get(route('admin.student.detail', $bk->id));
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue