Navy commited on
Commit
5de9ee6
·
1 Parent(s): 7e51bb5

add project

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