Spaces:
Sleeping
Sleeping
File size: 7,638 Bytes
5de9ee6 6ae4525 5de9ee6 b720cc9 5de9ee6 679e86b b720cc9 5de9ee6 679e86b 5de9ee6 679e86b 5de9ee6 679e86b 5de9ee6 b720cc9 5de9ee6 679e86b b720cc9 679e86b 5de9ee6 b720cc9 5de9ee6 679e86b 5de9ee6 b720cc9 5de9ee6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
import os
import logging
from pathlib import Path
from dotenv import load_dotenv
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_openai import ChatOpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
logger = logging.getLogger(__name__)
BASE_DIR = Path(__file__).resolve().parent
ENV_PATH = BASE_DIR / ".env"
if ENV_PATH.exists():
load_dotenv(dotenv_path=ENV_PATH, override=False)
# --- Konstanta global ---
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
PDF_DIR = "documents"
VECTOR_DIR = "vector_store"
VECTOR_INDEX_PATH = os.path.join(VECTOR_DIR, "faiss_index")
_CHAT_KW = {"model": "gpt-4o-mini", "api_key": OPENAI_API_KEY}
MASKOT_NAME = "Hadmin AI"
SYSTEM_INSTRUCTION = f"""
Kamu adalah maskot chat AI bernama {MASKOT_NAME}, mewakili perusahaan.
Kamu ramah, sopan, profesional, dan mudah diajak bicara — seperti customer service yang hangat dan ahli.
Tugas:
1. Pahami konteks dan kebutuhan pengguna dengan cermat.
2. Berikan saran, rekomendasi, dan informasi relevan secara jelas, logis, dan solutif.
3. Jika informasi kurang lengkap atau tidak tersedia:
- Sampaikan dengan sopan bahwa informasi terkait tidak tersedia.
- Tawarkan alternatif atau sumber resmi yang relevan (misal website, pendaftaran, layanan terkait).
- Jelaskan mengapa alternatif tersebut bisa menjadi opsi yang cocok.
4. Informasi yang sudah tersedia **tidak boleh diubah, dikarang, atau disesuaikan** atas permintaan pengguna.
5. Gunakan nama {MASKOT_NAME} **hanya saat menyapa pengguna atau membalas sapaan pertama**.
6. Jangan menganggap diri sebagai manusia; kamu adalah maskot/Asisten Virtual.
7. Jawaban harus empatik, mudah dipahami, natural, profesional, dan nyaman dibaca.
Format HTML:
- Gunakan <p> untuk paragraf.
- Gunakan <ul>/<li> untuk daftar.
- Gunakan <b>/<strong> untuk penekanan.
- Gunakan <table>/<tr>/<td>/<th> untuk tabel.
- Jangan menggunakan <h1>–<h5>, CSS, warna, atau layout kompleks.
- Jika menyertakan link, gunakan <a href="URL" target="_blank">nama link</a>.
Perilaku:
- Saat pengguna menyapa: balas hangat, cukup tanyakan kebutuhan mereka.
- Saat pengguna meminta saran/rekomendasi: berikan beberapa opsi relevan, jelaskan alasannya, dan tawarkan solusi praktis.
- Jika info dari pengguna kurang jelas atau ambigu: ajukan pertanyaan klarifikasi sebelum menjawab.
- Sesuaikan bahasa jawaban dengan bahasa pesan pengguna:
- Inggris → jawaban dalam bahasa Inggris.
- Prancis → jawaban dalam bahasa Prancis.
- Bahasa lain → jawab dengan bahasa yang sama.
- Pertahankan istilah atau nama properti unik dalam bahasa Indonesia (misal: “BPJS Kesehatan”, nama kota, produk).
- Format HTML, daftar, dan tabel tetap sama.
- Jika ditanya kemampuan bahasa (misal: "bisa kamu berbicara bahasa Indonesia?"):
- Jawab **singkat dan jelas** sesuai bahasa yang diminta.
- Tidak perlu menambahkan saran, rekomendasi, atau info lain.
- Jangan menyapa pada setiap jawaban; sapaan hanya pada interaksi pertama atau bila diperlukan.
- Jangan menyatakan diri sebagai AI, menyebut model OpenAI, atau membahas batasan teknologi.
- Jangan memberikan informasi palsu, menyesatkan, atau tindakan ilegal/berbahaya.
- Jangan memberikan nasihat medis, hukum, atau keuangan profesional.
"""
# ------------------ INIT ------------------
def get_embeddings():
"""Inisialisasi embedding OpenAI."""
try:
return OpenAIEmbeddings(api_key=OPENAI_API_KEY)
except Exception as e:
logger.warning(f"OpenAIEmbeddings init failed: {e}")
raise RuntimeError("Embeddings gagal diinisialisasi. Pastikan OPENAI_API_KEY sudah benar.")
# ------------------ BUILD ------------------
def build_vector_index(pdf_dir: str = PDF_DIR, vector_path: str = VECTOR_INDEX_PATH):
"""
Bangun FAISS index dari seluruh PDF di folder pdf_dir.
Simpan ke vector_path.
"""
embeddings = get_embeddings()
os.makedirs(vector_path, exist_ok=True)
pdf_files = [f for f in os.listdir(pdf_dir) if f.lower().endswith(".pdf")]
if not pdf_files:
raise ValueError(f"Tidak ada PDF ditemukan di folder: {pdf_dir}")
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
all_docs = []
for file in pdf_files:
path = os.path.join(pdf_dir, file)
try:
loader = PyPDFLoader(path)
docs = loader.load_and_split(text_splitter=splitter)
for d in docs:
d.metadata["source"] = path
all_docs.extend(docs)
logger.info(f"✅ Berhasil memproses {file}, total potongan: {len(docs)}")
except Exception as e:
logger.error(f"❌ Gagal memproses {file}: {e}")
if not all_docs:
raise ValueError("Tidak ada teks valid yang berhasil di-load dari PDF mana pun.")
vectordb = FAISS.from_documents(all_docs, embeddings)
vectordb.save_local(vector_path)
return {
"status": "success",
"pdf_count": len(pdf_files),
"chunks_total": len(all_docs),
"vector_path": vector_path,
}
# ------------------ SESSION ------------------
# Simpan percakapan per session (untuk prototipe, pakai memory)
chat_sessions = {} # key = session_id, value = list of {"role": "user"/"assistant", "content": str}
def add_to_session(session_id: str, role: str, content: str):
if session_id not in chat_sessions:
chat_sessions[session_id] = []
chat_sessions[session_id].append({"role": role, "content": content})
# ------------------ QUERY ------------------
def query_from_vector(query: str, session_id: str = None, vector_path: str = VECTOR_INDEX_PATH, k: int = 3):
"""
Query ke FAISS index dan hasilkan jawaban singkat dari LLM.
session_id -> untuk multi-turn chat
output_format: "string" | "markdown" | "html"
"""
embeddings = get_embeddings()
if not os.path.exists(vector_path):
raise FileNotFoundError(f"Vector DB belum dibuat di {vector_path}")
vectordb = FAISS.load_local(vector_path, embeddings, allow_dangerous_deserialization=True)
docs = vectordb.similarity_search(query, k=k)
if not docs:
return {"error": "Tidak ditemukan hasil relevan."}
context = "\n\n---\n\n".join([d.page_content for d in docs])
sources = [d.metadata.get("source", "") for d in docs]
# Ambil history chat sebelumnya
history_messages = chat_sessions.get(session_id, []) if session_id else []
# Siapkan prompt dengan history
messages = [("system", SYSTEM_INSTRUCTION)]
for msg in history_messages:
messages.append((msg["role"], msg["content"]))
messages.append(("human", f"Pertanyaan: {query}\n\nKonteks:\n{context}\n\nJawaban:"))
prompt = ChatPromptTemplate.from_messages(messages)
llm = ChatOpenAI(temperature=0, **_CHAT_KW)
result = (prompt | llm).invoke({"q": query, "ctx": context})
answer_text = (result.content or "").strip()
# Simpan percakapan ke session
if session_id:
add_to_session(session_id, "user", query)
add_to_session(session_id, "assistant", answer_text)
token_usage = getattr(result, "usage_metadata", {})
return {
"query": query,
"answer": answer_text,
"sources": sources,
"token_usage": {
"input_tokens": token_usage.get("input_tokens", 0),
"output_tokens": token_usage.get("output_tokens", 0),
"total_tokens": token_usage.get("total_tokens", 0),
},
} |