first init
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
from pathlib import Path
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
from openai import AsyncOpenAI
|
||||
from config import OPENAI_API_KEY, OPENAI_MODEL, ADMIN_TELEGRAM_ID
|
||||
import database as db
|
||||
import rag
|
||||
import document as doc_processor
|
||||
from guard import is_academic_topic, REJECT_MESSAGE
|
||||
|
||||
_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
|
||||
|
||||
ACADEMIC_SYSTEM_PROMPT = (
|
||||
"Anda adalah asisten akademik personal untuk dosen di perguruan tinggi Indonesia. "
|
||||
"Fokus HANYA pada topik akademik: pengajaran, riset, kurikulum, bimbingan mahasiswa, "
|
||||
"penulisan jurnal, administrasi kampus, dan teknologi pendidikan. "
|
||||
"Tolak dengan sopan jika ditanya di luar topik tersebut. "
|
||||
"Jawab dalam bahasa yang sama dengan pertanyaan pengguna."
|
||||
)
|
||||
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.effective_user
|
||||
existing = db.get_user(user.id)
|
||||
|
||||
if existing:
|
||||
await update.message.reply_text(
|
||||
f"👋 Selamat datang kembali, *{existing['full_name']}*!\n"
|
||||
f"🧠 Second Brain Anda aktif. Gunakan /help untuk panduan.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
if user.id == ADMIN_TELEGRAM_ID:
|
||||
await update.message.reply_text(
|
||||
"👑 *Mode Admin Aktif*\n\nGunakan /help_admin untuk melihat semua perintah admin.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
if not context.args:
|
||||
await update.message.reply_text(
|
||||
"👋 Halo! Bot ini khusus untuk dosen dengan token registrasi.\n\n"
|
||||
"Hubungi admin untuk mendapatkan token, lalu kirim:\n"
|
||||
"`/start TOKEN_ANDA`",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
token = context.args[0]
|
||||
token_data = db.use_token(token, user.id)
|
||||
if not token_data:
|
||||
await update.message.reply_text("❌ Token tidak valid atau sudah digunakan.")
|
||||
return
|
||||
|
||||
full_name = user.full_name or user.username or "Dosen"
|
||||
db.register_user(user.id, full_name, user.username or "", token)
|
||||
await update.message.reply_text(
|
||||
f"✅ *Registrasi berhasil!*\n\n"
|
||||
f"👤 {full_name}\n"
|
||||
f"🧠 Second Brain Anda telah aktif.\n\n"
|
||||
f"Gunakan /help untuk panduan lengkap.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
async def remember(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")
|
||||
return
|
||||
if not context.args:
|
||||
await update.message.reply_text("Gunakan: /remember Catatan yang ingin disimpan")
|
||||
return
|
||||
|
||||
content = " ".join(context.args)
|
||||
mem_id = db.save_memory_meta(user_data["id"], content, scope="private")
|
||||
await rag.add_memory(user_data["id"], mem_id, content, scope="private")
|
||||
await update.message.reply_text(f"✅ Tersimpan di Second Brain:\n_{content}_", parse_mode="Markdown")
|
||||
|
||||
|
||||
async def recall(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:
|
||||
# Tampilkan 10 catatan terbaru
|
||||
memories = db.get_recent_memories(user_data["id"])
|
||||
if not memories:
|
||||
await update.message.reply_text("Second Brain Anda masih kosong. Gunakan /remember untuk menyimpan catatan.")
|
||||
return
|
||||
lines = ["🧠 *Catatan Terbaru:*\n"]
|
||||
for m in memories:
|
||||
lines.append(f"• {m['content'][:150]}\n _{m['created_at'][:16]}_")
|
||||
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||
return
|
||||
|
||||
# Pencarian semantik
|
||||
query = " ".join(context.args)
|
||||
await update.message.chat.send_action("typing")
|
||||
results = await rag.search(user_data["id"], query)
|
||||
|
||||
if not results:
|
||||
await update.message.reply_text("Tidak ada hasil yang relevan ditemukan.")
|
||||
return
|
||||
|
||||
lines = [f"🔍 Hasil pencarian '{query}':\n"]
|
||||
for i, r in enumerate(results, 1):
|
||||
scope_label = "🌐 Global" if r["scope"] == "global" else "🔒 Pribadi"
|
||||
# cosine distance: 0 = identik, 2 = berlawanan → konversi ke persentase
|
||||
relevance = round(max(0, (1 - r["distance"])) * 100, 1)
|
||||
lines.append(f"{i}. [{scope_label}] {r['content'][:200]}\n {relevance}% relevan\n")
|
||||
await update.message.reply_text("\n".join(lines), parse_mode="Markdown")
|
||||
|
||||
|
||||
async def ask(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: /ask Pertanyaan Anda")
|
||||
return
|
||||
|
||||
question = " ".join(context.args)
|
||||
await update.message.chat.send_action("typing")
|
||||
|
||||
# Guard: hanya izinkan topik akademik
|
||||
if not await is_academic_topic(question, _client, OPENAI_MODEL):
|
||||
await update.message.reply_text(REJECT_MESSAGE)
|
||||
return
|
||||
|
||||
# Cari konteks relevan dari Second Brain
|
||||
relevant = await rag.search(user_data["id"], question, n_results=5)
|
||||
context_text = ""
|
||||
if relevant:
|
||||
context_text = "\n\nKonteks dari Second Brain pengguna:\n" + "\n".join(
|
||||
f"[{'Global' if r['scope'] == 'global' else 'Pribadi'}] {r['content']}"
|
||||
for r in relevant
|
||||
)
|
||||
|
||||
history = db.get_conversation_history(user_data["id"])
|
||||
system_prompt = (
|
||||
f"{ACADEMIC_SYSTEM_PROMPT}\n\n"
|
||||
f"Nama dosen: {user_data['full_name']}"
|
||||
f"{context_text}"
|
||||
)
|
||||
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
messages.extend(history)
|
||||
messages.append({"role": "user", "content": question})
|
||||
|
||||
try:
|
||||
response = await _client.chat.completions.create(
|
||||
model=OPENAI_MODEL,
|
||||
messages=messages,
|
||||
max_tokens=1500,
|
||||
)
|
||||
answer = response.choices[0].message.content
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ Gagal menghubungi AI: {str(e)[:100]}")
|
||||
return
|
||||
|
||||
db.save_conversation(user_data["id"], "user", question)
|
||||
db.save_conversation(user_data["id"], "assistant", answer)
|
||||
await update.message.reply_text(answer)
|
||||
|
||||
|
||||
async def upload_doc(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Upload dokumen ke Second Brain pribadi."""
|
||||
user_data = db.get_user(update.effective_user.id)
|
||||
if not user_data:
|
||||
await update.message.reply_text("⛔ Belum terdaftar.")
|
||||
return
|
||||
|
||||
doc = update.message.document
|
||||
if not doc:
|
||||
await update.message.reply_text(
|
||||
"📎 Kirim file PDF, DOCX, atau TXT dengan caption `/upload`",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
allowed_ext = {".pdf", ".docx", ".txt"}
|
||||
if Path(doc.file_name).suffix.lower() not in allowed_ext:
|
||||
await update.message.reply_text("❌ Format tidak didukung. Gunakan PDF, DOCX, atau TXT.")
|
||||
return
|
||||
|
||||
await update.message.reply_text("⏳ Memproses dokumen Anda...")
|
||||
|
||||
file = await doc.get_file()
|
||||
file_bytes = bytes(await file.download_as_bytearray())
|
||||
filename = doc_processor.save_file(file_bytes, doc.file_name)
|
||||
text = doc_processor.extract_text(filename)
|
||||
|
||||
if not text.strip():
|
||||
await update.message.reply_text("❌ Gagal mengekstrak teks dari dokumen.")
|
||||
return
|
||||
|
||||
chunks = doc_processor.chunk_text(text)
|
||||
doc_id = db.save_document_meta(
|
||||
user_id=user_data["id"],
|
||||
filename=filename,
|
||||
original_name=doc.file_name,
|
||||
scope="private",
|
||||
chunk_count=len(chunks)
|
||||
)
|
||||
await rag.add_document_chunks(user_id=user_data["id"], doc_id=doc_id, chunks=chunks, scope="private")
|
||||
|
||||
await update.message.reply_text(
|
||||
f"✅ {doc.file_name} berhasil diproses!\n"
|
||||
f"📄 {len(chunks)} bagian tersimpan di Second Brain Anda.\n"
|
||||
f"Gunakan /ask untuk bertanya tentang isi dokumen ini."
|
||||
)
|
||||
|
||||
|
||||
async def clear_chat(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
|
||||
db.clear_conversation(user_data["id"])
|
||||
await update.message.reply_text("🧹 Riwayat percakapan berhasil dihapus.")
|
||||
|
||||
|
||||
async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
if update.effective_user.id == ADMIN_TELEGRAM_ID:
|
||||
await update.message.reply_text("Gunakan /help_admin untuk perintah admin.")
|
||||
return
|
||||
|
||||
user_data = db.get_user(update.effective_user.id)
|
||||
if not user_data:
|
||||
await update.message.reply_text("Gunakan /start TOKEN untuk registrasi.")
|
||||
return
|
||||
|
||||
text = (
|
||||
"🧠 *Second Brain Bot — Panduan*\n\n"
|
||||
"*Catatan:*\n"
|
||||
"/remember `teks` — Simpan catatan baru\n"
|
||||
"/recall — Lihat 10 catatan terbaru\n"
|
||||
"/recall `kata kunci` — Cari dengan AI (semantik)\n\n"
|
||||
"*Dokumen:*\n"
|
||||
"📎 Kirim file + caption `/upload` — Upload PDF/DOCX/TXT\n\n"
|
||||
"*Tanya AI:*\n"
|
||||
"/ask `pertanyaan` — Tanya AI dengan konteks Second Brain\n"
|
||||
"/clear — Hapus riwayat percakapan\n\n"
|
||||
"_Bot ini khusus untuk topik akademik dan perkuliahan._"
|
||||
)
|
||||
await update.message.reply_text(text, parse_mode="Markdown")
|
||||
Reference in New Issue
Block a user