feat: add natural language reminder with LLM parsing and confirmation flow

This commit is contained in:
Power BI Dev
2026-06-04 10:52:36 +07:00
parent 2dfa6f95fb
commit 7acdd5347c
6 changed files with 304 additions and 6 deletions
+99 -3
View File
@@ -1,14 +1,20 @@
import html
from datetime import datetime
from pathlib import Path
from telegram import Update
from telegram.ext import ContextTypes
from openai import AsyncOpenAI
from config import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL, ADMIN_TELEGRAM_ID
from config import DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL, ADMIN_TELEGRAM_ID, BOT_TIMEZONE
from zoneinfo import ZoneInfo
import database as db
import rag
import reminder as reminder_module
import document as doc_processor
from guard import is_academic_topic, REJECT_MESSAGE
_pending_confirmations: dict = {}
REMINDER_KEYWORDS = ["ingatkan", "pengingat", "remind", "jadwalkan", "tolong ingat"]
_client = AsyncOpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)
ACADEMIC_SYSTEM_PROMPT = (
@@ -219,16 +225,106 @@ async def upload_doc(update: Update, context: ContextTypes.DEFAULT_TYPE):
)
async def list_reminders(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_data = db.get_user(update.effective_user.id)
if not user_data:
await update.message.reply_text("⛔ Belum terdaftar.")
return
reminders = db.get_user_reminders(user_data["id"])
if not reminders:
await update.message.reply_text("Tidak ada pengingat aktif.")
return
tz = ZoneInfo(BOT_TIMEZONE)
lines = ["⏰ <b>Pengingat Aktif:</b>\n"]
for r in reminders:
if r["is_recurring"]:
schedule_str = f"Setiap hari jam {r['remind_at'][:5]} WIB"
else:
try:
dt = datetime.fromisoformat(r["remind_at"]).replace(tzinfo=tz)
schedule_str = dt.strftime("%d %b %Y %H:%M WIB")
except Exception:
schedule_str = r["remind_at"]
lines.append(f"#{r['id']} — <i>{html.escape(r['message'])}</i>\n 📅 {schedule_str}")
lines.append("\nGunakan /cancel_remind ID untuk membatalkan.")
await update.message.reply_text("\n".join(lines), parse_mode="HTML")
async def cancel_remind(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_data = db.get_user(update.effective_user.id)
if not user_data:
await update.message.reply_text("⛔ Belum terdaftar.")
return
if not context.args:
await update.message.reply_text("Gunakan: /cancel_remind ID")
return
try:
reminder_id = int(context.args[0])
except ValueError:
await update.message.reply_text("ID harus berupa angka.")
return
success = db.cancel_reminder(reminder_id, user_data["id"])
if not success:
await update.message.reply_text("❌ Pengingat tidak ditemukan.")
return
for job in context.application.job_queue.get_jobs_by_name(f"reminder_{reminder_id}"):
job.schedule_removal()
await update.message.reply_text("✅ Pengingat berhasil dibatalkan.")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_data = db.get_user(update.effective_user.id)
if not user_data:
await update.message.reply_text("⛔ Belum terdaftar. Gunakan /start TOKEN untuk registrasi.")
return
question = update.message.text.strip()
if not question:
text = update.message.text.strip()
if not text:
return
user_id = update.effective_user.id
# Cek pending konfirmasi reminder
if user_id in _pending_confirmations:
answer = text.lower()
if answer in ("ya", "iya", "yes", "y"):
pending = _pending_confirmations.pop(user_id)
parsed = pending["parsed"]
reminder_id = db.create_reminder(
telegram_id=user_id,
user_db_id=user_data["id"],
message=parsed["message"],
remind_at=parsed["remind_at"],
is_recurring=parsed.get("is_recurring", False),
recur_type=parsed.get("recur_type"),
)
reminder_module.schedule_new_reminder(
context.application.job_queue, reminder_id, user_id, parsed["message"], parsed
)
await update.message.reply_text("✅ Pengingat berhasil disimpan! Gunakan /reminders untuk melihat daftar.")
return
elif answer in ("tidak", "batal", "cancel", "no", "n"):
_pending_confirmations.pop(user_id)
await update.message.reply_text("❌ Pengingat dibatalkan.")
return
else:
_pending_confirmations.pop(user_id)
# Deteksi intent reminder dari plain text
text_lower = text.lower()
if any(kw in text_lower for kw in REMINDER_KEYWORDS):
await update.message.chat.send_action("typing")
parsed = await reminder_module.parse_reminder_intent(text, _client, DEEPSEEK_MODEL)
if parsed:
_pending_confirmations[user_id] = {"parsed": parsed}
await update.message.reply_text(reminder_module.format_confirmation(parsed), parse_mode="HTML")
return
question = text
await update.message.chat.send_action("typing")
if not await is_academic_topic(question, _client, DEEPSEEK_MODEL):