vnsd13 commited on
Commit
b4e4aa8
·
1 Parent(s): 74c645a
Files changed (3) hide show
  1. app/main.py +3 -2
  2. app/models.py +35 -9
  3. app/services/scheduler_service.py +112 -49
app/main.py CHANGED
@@ -77,7 +77,8 @@ async def register_device(request: RegisterRequest):
77
  async def schedule_reminder_endpoint(request: ScheduleRequest):
78
  if request.fcm_token not in device_tokens:
79
  return {"error": "Device belum terdaftar, Baka! Daftar dulu lewat /register-device."}
80
- schedule_new_reminder(request.fcm_token, request.task, request.remind_at)
81
- return {"message": f"Cih, yaudah aku ingetin nanti jam {request.remind_at}. Jangan sampai lupa juga, Baka!"}
82
 
 
 
 
83
  # NOTE: Tidak perlu blok __main__ karena dijalankan via Docker CMD (uvicorn app.main:app)
 
77
  async def schedule_reminder_endpoint(request: ScheduleRequest):
78
  if request.fcm_token not in device_tokens:
79
  return {"error": "Device belum terdaftar, Baka! Daftar dulu lewat /register-device."}
 
 
80
 
81
+ # request.remind_at sudah datetime; pakai UTC untuk APScheduler
82
+ schedule_new_reminder(request.fcm_token, request.task, request.remind_at_utc)
83
+ return {"message": f"Cih, yaudah aku ingetin nanti jam {request.remind_at.isoformat()}."}
84
  # NOTE: Tidak perlu blok __main__ karena dijalankan via Docker CMD (uvicorn app.main:app)
app/models.py CHANGED
@@ -1,20 +1,46 @@
1
- from pydantic import BaseModel
2
- from typing import List, Dict, Optional, Any
 
 
 
 
 
 
 
 
 
 
3
 
4
- # Blueprint untuk request chat dari Flutter
5
  class ChatRequest(BaseModel):
6
  message: str
7
- # INI YANG KITA UBAH! Sekarang dia mau menerima format history yang lebih rumit.
8
- history: List[Dict[str, Any]] = []
9
- username: Optional[str] = "Baka"
10
 
11
- # Blueprint untuk request pendaftaran device
12
  class RegisterRequest(BaseModel):
13
  fcm_token: str
14
 
15
- # Blueprint untuk request penjadwalan
16
  class ScheduleRequest(BaseModel):
17
  fcm_token: str
18
  task: str
19
- remind_at: str # Kita pakai string biar gampang dikirim dari Flutter
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
 
 
 
 
 
1
+ # app/models.py
2
+ from __future__ import annotations
3
+ from typing import List, Optional, Literal
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ # ====== Chat history ======
9
+ class Message(BaseModel):
10
+ role: Literal["user", "assistant", "system"] = "user"
11
+ content: str
12
+ ts: Optional[datetime] = None # optional timestamp
13
 
 
14
  class ChatRequest(BaseModel):
15
  message: str
16
+ # JANGAN pakai default [] (mutable). Pakai default_factory saja.
17
+ history: List[Message] = Field(default_factory=list)
18
+ username: str = "Baka"
19
 
20
+ # ====== Device register ======
21
  class RegisterRequest(BaseModel):
22
  fcm_token: str
23
 
24
+ # ====== Scheduler ======
25
  class ScheduleRequest(BaseModel):
26
  fcm_token: str
27
  task: str
28
+ # Pakai datetime supaya FastAPI otomatis validasi & parse ISO 8601 dari Flutter
29
+ # Contoh valid: "2025-10-12T22:05:00+07:00" atau "2025-10-12T15:05:00Z"
30
+ remind_at: datetime = Field(
31
+ ...,
32
+ description="ISO 8601 datetime (disarankan sertakan offset, mis. +07:00 atau Z)"
33
+ )
34
+
35
+ # Normalisasi: kalau user kirim tanpa timezone (naive), anggap WIB (+07:00)
36
+ @classmethod
37
+ def model_validate(cls, obj, *args, **kwargs):
38
+ m = super().model_validate(obj, *args, **kwargs)
39
+ if m.remind_at.tzinfo is None:
40
+ m.remind_at = m.remind_at.replace(tzinfo=timezone(timedelta(hours=7)))
41
+ return m
42
 
43
+ # Helper: konversi ke UTC buat scheduler
44
+ @property
45
+ def remind_at_utc(self) -> datetime:
46
+ return self.remind_at.astimezone(timezone.utc)
app/services/scheduler_service.py CHANGED
@@ -1,71 +1,134 @@
 
 
 
 
 
 
1
  import firebase_admin
2
- import google.generativeai as genai
3
- from datetime import datetime
4
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
5
  from apscheduler.triggers.date import DateTrigger
6
- from firebase_admin import credentials, messaging
7
 
8
- # Impor konfigurasi dari file config.py di Canvas-mu
 
 
9
  from app.config import FIREBASE_SERVICE_ACCOUNT_KEY_PATH, GOOGLE_API_KEY
10
 
11
- # --- Inisialisasi Firebase & Scheduler ---
12
- try:
13
- # Coba inisialisasi Firebase Admin SDK dari path yang ada di config
14
- cred = credentials.Certificate(FIREBASE_SERVICE_ACCOUNT_KEY_PATH)
15
- firebase_admin.initialize_app(cred)
16
- print("Firebase Admin SDK berhasil diinisialisasi.")
17
- except Exception as e:
18
- print(f"GAGAL inisialisasi Firebase Admin SDK, Baka! Cek lagi path file kuncimu di config.py. Error: {e}")
 
 
 
 
19
 
20
- # Buat instance scheduler yang akan berjalan di background
21
- scheduler = AsyncIOScheduler()
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- # Konfigurasi model Gemini khusus untuk scheduler (biar nggak bentrok)
24
- genai.configure(api_key=GOOGLE_API_KEY)
25
- proactive_model = genai.GenerativeModel('gemini-2.5-flash')
26
 
27
- # --- Variabel Global Sederhana untuk Menyimpan Token Device ---
28
- # Untuk produksi, ini harusnya disimpan di database. Tapi buat skripsi, ini cukup.
29
  device_tokens = set()
30
 
31
- # --- Logika Inti Scheduler ---
32
- async def send_proactive_message_task(fcm_token: str, task_description: str):
33
- """Fungsi yang akan dijalankan oleh scheduler untuk mengirim notifikasi."""
34
- print(f"Menjalankan tugas terjadwal untuk {fcm_token}: {task_description}")
 
 
 
 
 
 
35
  try:
36
- # 1. Buat prompt khusus untuk pesan proaktif yang akan dikirim ke Gemini
37
- proactive_prompt = f"Buat sebuah pesan pengingat yang sangat singkat (maksimal 1-2 kalimat) dengan gaya tsundere untuk mengingatkan user tentang '{task_description}'. Panggil dia Baka atau bodoh."
38
-
39
- # 2. Minta Gemini membuat pesannya
40
- response = await proactive_model.generate_content_async(proactive_prompt)
41
- tsundere_message = response.text.strip()
42
-
43
- # 3. Siapkan push notification untuk dikirim via Firebase
44
- message = messaging.Message(
45
- notification=messaging.Notification(
46
- title="Pesan dari Mini-chan! 😤",
47
- body=tsundere_message,
48
- ),
49
- token=fcm_token,
50
- )
51
-
52
- # 4. Kirim notifikasinya
53
- messaging.send(message)
54
- print(f"Pesan proaktif berhasil terkirim ke {fcm_token}: {tsundere_message}")
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  except Exception as e:
57
- print(f"Gagal mengirim pesan proaktif, Baka! Error: {e}")
 
 
 
 
 
 
 
 
 
 
58
 
59
- def schedule_new_reminder(fcm_token: str, task: str, remind_at: datetime):
60
- """Fungsi untuk menambahkan pekerjaan baru ke dalam antrian scheduler."""
 
 
 
 
 
 
 
61
  trigger = DateTrigger(run_date=remind_at)
62
- job_id = f"{fcm_token}_{remind_at.isoformat()}" # ID unik biar jadwal gak tumpang tindih
63
-
 
 
 
64
  scheduler.add_job(
65
  send_proactive_message_task,
66
  trigger=trigger,
67
  args=[fcm_token, task],
68
  id=job_id,
69
- replace_existing=True
 
 
 
70
  )
71
- print(f"Jadwal berhasil dibuat dengan ID: {job_id}")
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/scheduler_service.py
2
+ from __future__ import annotations
3
+ import asyncio
4
+ from typing import Optional
5
+ from datetime import datetime, timezone
6
+
7
  import firebase_admin
8
+ from firebase_admin import credentials, messaging
9
+
10
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
11
  from apscheduler.triggers.date import DateTrigger
 
12
 
13
+ import google.generativeai as genai
14
+
15
+ # Config
16
  from app.config import FIREBASE_SERVICE_ACCOUNT_KEY_PATH, GOOGLE_API_KEY
17
 
18
+ # ============== Firebase Init ==============
19
+ def _init_firebase_if_needed() -> None:
20
+ try:
21
+ # Jangan double-init
22
+ if not firebase_admin._apps:
23
+ cred = credentials.Certificate(FIREBASE_SERVICE_ACCOUNT_KEY_PATH) # type: ignore[arg-type]
24
+ firebase_admin.initialize_app(cred)
25
+ print("Firebase Admin SDK berhasil diinisialisasi.")
26
+ except Exception as e:
27
+ print(f"GAGAL inisialisasi Firebase Admin SDK, Baka! Error: {e}")
28
+
29
+ _init_firebase_if_needed()
30
 
31
+ # ============== Gemini Init (opsional) ==============
32
+ proactive_model: Optional[genai.GenerativeModel] = None
33
+ if GOOGLE_API_KEY:
34
+ try:
35
+ genai.configure(api_key=GOOGLE_API_KEY)
36
+ # ganti model kalau perlu; ini contoh yang cepat/hemat
37
+ proactive_model = genai.GenerativeModel("gemini-2.5-flash")
38
+ print("✅ GOOGLE_API_KEY terdeteksi. Gemini siap.")
39
+ except Exception as e:
40
+ proactive_model = None
41
+ print(f"⚠️ Gagal init Gemini: {e}. Pakai pesan fallback aja ya, Baka.")
42
+ else:
43
+ print("⚠️ GOOGLE_API_KEY tidak ditemukan. Pakai pesan fallback tanpa Gemini.")
44
 
45
+ # ============== Scheduler (UTC) ==============
46
+ # Jalankan di UTC. (Kamu sudah kirim remind_at_utc dari endpoint.)
47
+ scheduler = AsyncIOScheduler(timezone="UTC")
48
 
49
+ # Token device (untuk demo; produksi simpan di DB)
 
50
  device_tokens = set()
51
 
52
+ # ============== Helper Notif ==============
53
+ async def _send_fcm(token: str, title: str, body: str) -> None:
54
+ """
55
+ Kirim FCM tanpa nge-block event loop.
56
+ """
57
+ msg = messaging.Message(
58
+ notification=messaging.Notification(title=title, body=body),
59
+ token=token,
60
+ )
61
+ # firebase_admin.messaging.send() itu blocking (sync); pindahkan ke thread pool
62
  try:
63
+ await asyncio.to_thread(messaging.send, msg)
64
+ print(f"FCM terkirim ke {token}: {body}")
65
+ except Exception as e:
66
+ print(f"FCM gagal dikirim ke {token}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ async def _build_tsundere_message(task_description: str) -> str:
69
+ """
70
+ Bangun pesan tsundere singkat. Pakai Gemini kalau ada; kalau tidak, fallback.
71
+ """
72
+ if proactive_model is None:
73
+ return f"Hmph… {task_description} sekarang. Jangan males, Baka! 😤"
74
+
75
+ prompt = (
76
+ "Buat pesan pengingat SANGAT singkat (maks 1–2 kalimat) gaya tsundere, "
77
+ "dalam Bahasa Indonesia, panggil user 'Baka' atau 'bodoh', "
78
+ "untuk mengingatkan: '" + task_description + "'."
79
+ )
80
+ try:
81
+ resp = await proactive_model.generate_content_async(prompt)
82
+ text = (resp.text or "").strip()
83
+ if not text:
84
+ raise ValueError("Gemini balikin kosong")
85
+ return text
86
  except Exception as e:
87
+ print(f"⚠️ Gemini gagal bikin pesan: {e} — pakai fallback.")
88
+ return f"Hei, {task_description} tuh! Jangan sok lupa, Baka. 😒"
89
+
90
+ # ============== Job Function ==============
91
+ async def send_proactive_message_task(fcm_token: str, task_description: str):
92
+ """
93
+ Dipanggil scheduler pada waktu yang ditentukan (UTC).
94
+ """
95
+ print(f"Menjalankan job untuk {fcm_token} @ {datetime.now(timezone.utc).isoformat()}")
96
+ message = await _build_tsundere_message(task_description)
97
+ await _send_fcm(fcm_token, "Pesan dari Mini-chan! 😤", message)
98
 
99
+ # ============== API Jadwal ==============
100
+ def schedule_new_reminder(fcm_token: str, task: str, remind_at: datetime) -> str:
101
+ """
102
+ Tambahkan job baru pada waktu remind_at (harus tz-aware, sebaiknya UTC).
103
+ """
104
+ if remind_at.tzinfo is None:
105
+ raise ValueError("remind_at harus datetime timezone-aware, Baka!")
106
+
107
+ # Gunakan DateTrigger; APScheduler UTC sudah diset di scheduler
108
  trigger = DateTrigger(run_date=remind_at)
109
+
110
+ # ID unik (hindari karakter ilegal)
111
+ safe_when = remind_at.isoformat().replace(":", "-")
112
+ job_id = f"{fcm_token}_{safe_when}"
113
+
114
  scheduler.add_job(
115
  send_proactive_message_task,
116
  trigger=trigger,
117
  args=[fcm_token, task],
118
  id=job_id,
119
+ replace_existing=True,
120
+ misfire_grace_time=60, # toleransi 60s kalau container sempat lambat
121
+ coalesce=True, # gabungkan jika ketinggalan
122
+ max_instances=1,
123
  )
124
+ print(f"Jadwal dibuat: {job_id} @ {remind_at.isoformat()}")
125
+ return job_id
126
+
127
+ def cancel_reminder(job_id: str) -> bool:
128
+ try:
129
+ scheduler.remove_job(job_id)
130
+ print(f"Jadwal dibatalkan: {job_id}")
131
+ return True
132
+ except Exception as e:
133
+ print(f"Gagal batalin {job_id}: {e}")
134
+ return False