FauzanAriyatmoko commited on
Commit
b80cddf
·
1 Parent(s): 7f7f589

feat: Implement initial RAG chatbot core functionalities including PDF processing, vector store, and RAG pipeline.

Browse files
.env.example ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Configuration
2
+ MODEL_NAME=THUDM/chatglm3-6b
3
+ EMBEDDING_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
4
+
5
+ # Device Configuration (auto/cuda/cpu)
6
+ DEVICE=auto
7
+
8
+ # Text Processing
9
+ CHUNK_SIZE=500
10
+ CHUNK_OVERLAP=50
11
+
12
+ # Retrieval Configuration
13
+ TOP_K_RETRIEVAL=3
14
+
15
+ # Generation Parameters
16
+ MAX_LENGTH=2048
17
+ TEMPERATURE=0.7
18
+ TOP_P=0.9
19
+
20
+ # Storage Paths
21
+ UPLOAD_DIR=data/uploads
22
+ VECTOR_DB_DIR=data/vector_db
.gitignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment
2
+ .env
3
+ *.env
4
+ .llm_env
5
+
6
+ # Data & Uploads
7
+ data/uploads/*
8
+ data/vector_db/*
9
+ !data/.gitkeep
10
+
11
+ # Python
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+ *.so
16
+ .Python
17
+ venv/
18
+ env/
19
+ ENV/
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+ *.swo
26
+
27
+ # Models (jika download lokal)
28
+ models/
29
+ *.bin
30
+ *.safetensors
31
+
32
+ # Logs
33
+ *.log
QUICKSTART.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Start Guide - RAG ChatBot
2
+
3
+ Panduan cepat untuk menjalankan RAG ChatBot.
4
+
5
+ ## 📦 Instalasi Dependencies
6
+
7
+ ```bash
8
+ # Install semua dependencies (membutuhkan waktu beberapa menit)
9
+ pip install -r requirements.txt
10
+ ```
11
+
12
+ **Catatan**: Dependencies cukup besar (~2-3GB), terutama PyTorch dan Transformers.
13
+
14
+ ## 🚀 Menjalankan Aplikasi
15
+
16
+ ```bash
17
+ python app.py
18
+ ```
19
+
20
+ Aplikasi akan:
21
+ 1. Load konfigurasi dari `.env`
22
+ 2. Inisialisasi vector database
23
+ 3. Launch Gradio interface di `http://localhost:7860`
24
+
25
+ **Catatan**: Model GLM akan di-download otomatis saat pertama kali digunakan (ukuran ~13GB untuk ChatGLM3-6B).
26
+
27
+ ## 📚 Workflow Penggunaan
28
+
29
+ ### 1. Upload PDF
30
+ - Buka tab "📤 Upload Dokumen"
31
+ - Pilih file PDF
32
+ - Klik "Process PDF"
33
+ - Tunggu hingga selesai
34
+
35
+ ### 2. Chat
36
+ - Buka tab "💬 Chat"
37
+ - Ketik pertanyaan tentang dokumen
38
+ - Model akan load otomatis (pertama kali akan lambat)
39
+ - Sistem akan mencari konteks relevan dan menjawab
40
+
41
+ ### 3. Lihat Sumber
42
+ - Source citations ditampilkan di bawah jawaban
43
+ - Klik untuk melihat chunk yang digunakan
44
+
45
+ ## ⚙️ Konfigurasi
46
+
47
+ Edit `.env` untuk mengubah settings:
48
+
49
+ ```bash
50
+ # Jika tidak punya GPU
51
+ DEVICE=cpu
52
+
53
+ # Untuk mengurangi memory usage
54
+ CHUNK_SIZE=300
55
+ TOP_K_RETRIEVAL=2
56
+ ```
57
+
58
+ ## 🐛 Troubleshooting
59
+
60
+ ### Error: CUDA out of memory
61
+ ```bash
62
+ # Gunakan CPU
63
+ DEVICE=cpu
64
+ ```
65
+
66
+ ### Error: Model download terlalu lambat
67
+ ```bash
68
+ # Set HuggingFace mirror (untuk Indonesia)
69
+ export HF_ENDPOINT=https://hf-mirror.com
70
+ ```
71
+
72
+ ### PDF tidak ter-extract
73
+ - Pastikan PDF berisi text (bukan scan)
74
+ - Coba PDF lain untuk testing
75
+ - Check logs untuk error detail
76
+
77
+ ## 📝 Testing
78
+
79
+ Sebelum testing full app, verify imports dulu:
80
+
81
+ ```bash
82
+ # Install pytest
83
+ pip install pytest
84
+
85
+ # Run basic tests
86
+ pytest tests/test_pdf_processor.py -v
87
+ ```
88
+
89
+ ## 💡 Tips
90
+
91
+ 1. **First Run**: Model download membutuhkan waktu, bersabar
92
+ 2. **GPU Recommended**: CPU bisa digunakan tapi lebih lambat
93
+ 3. **PDF Quality**: Gunakan PDF dengan text yang jelas
94
+ 4. **Chunk Size**: Sesuaikan berdasarkan panjang dokumen
95
+
96
+ ## 📞 Need Help?
97
+
98
+ Check README.md untuk dokumentasi lengkap atau buat issue di repository.
README.md CHANGED
@@ -1,17 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: LLM ChatBot Document
3
- emoji: 💬
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- hf_oauth: true
11
- hf_oauth_scopes:
12
- - inference-api
13
- license: mit
14
- short_description: Chat with your own document for practice
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  ---
16
 
17
- An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
 
 
 
1
+ # RAG ChatBot dengan GLM Model 🤖
2
+
3
+ <div align="center">
4
+
5
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
6
+ ![Python](https://img.shields.io/badge/python-3.8+-brightgreen.svg)
7
+ ![Gradio](https://img.shields.io/badge/gradio-5.42.0-orange.svg)
8
+
9
+ **Chat dengan dokumen PDF Anda menggunakan AI dengan teknologi RAG (Retrieval-Augmented Generation)**
10
+
11
+ [Demo](#demo) • [Fitur](#fitur) • [Instalasi](#instalasi) • [Penggunaan](#penggunaan) • [Arsitektur](#arsitektur)
12
+
13
+ </div>
14
+
15
  ---
16
+
17
+ ## 📖 Deskripsi
18
+
19
+ RAG ChatBot adalah aplikasi AI yang memungkinkan Anda untuk mengupload dokumen PDF dan melakukan tanya jawab interaktif tentang isi dokumen tersebut. Sistem menggunakan:
20
+
21
+ - **ChatGLM3-6B**: Model bahasa generatif untuk menghasilkan jawaban
22
+ - **RAG (Retrieval-Augmented Generation)**: Teknik untuk mencari informasi relevan dari dokumen
23
+ - **ChromaDB**: Vector database untuk penyimpanan dan pencarian semantic
24
+ - **Gradio**: Interface web yang modern dan interaktif
25
+
26
+ ## ✨ Fitur
27
+
28
+ - 📤 **Upload Multiple PDF**: Upload satu atau beberapa file PDF sekaligus
29
+ - 🔍 **Semantic Search**: Pencarian konteks menggunakan embeddings
30
+ - 💬 **Interactive Chat**: Chat dengan streaming response
31
+ - 📚 **Source Citations**: Lihat sumber informasi dari dokumen
32
+ - 🎨 **Modern UI**: Interface premium dengan gradients dan animasi
33
+ - ⚙️ **Configurable**: Atur parameters seperti temperature, top-p, dan retrieval count
34
+ - 💾 **Persistent Storage**: Dokumen tersimpan di vector database
35
+ - 🌐 **Bahasa Indonesia**: Full support untuk bahasa Indonesia
36
+
37
+ ## 🚀 Instalasi
38
+
39
+ ### Prerequisites
40
+
41
+ - Python 3.8 atau lebih tinggi
42
+ - (Opsional) NVIDIA GPU dengan CUDA untuk performa optimal
43
+
44
+ ### Langkah Instalasi
45
+
46
+ 1. **Clone repository**
47
+ ```bash
48
+ git clone <repository-url>
49
+ cd LLM-ChatBot-Document
50
+ ```
51
+
52
+ 2. **Buat virtual environment**
53
+ ```bash
54
+ python -m venv venv
55
+ source venv/bin/activate # Linux/Mac
56
+ # atau
57
+ venv\Scripts\activate # Windows
58
+ ```
59
+
60
+ 3. **Install dependencies**
61
+ ```bash
62
+ pip install -r requirements.txt
63
+ ```
64
+
65
+ 4. **Setup environment variables**
66
+ ```bash
67
+ cp .env.example .env
68
+ # Edit .env sesuai kebutuhan
69
+ ```
70
+
71
+ ## 📋 Penggunaan
72
+
73
+ ### Menjalankan Aplikasi
74
+
75
+ ```bash
76
+ python app.py
77
+ ```
78
+
79
+ Aplikasi akan berjalan di `http://localhost:7860`
80
+
81
+ ### Workflow
82
+
83
+ 1. **Upload Dokumen** (Tab 📤 Upload Dokumen)
84
+ - Pilih file PDF dari komputer Anda
85
+ - Klik "Process PDF"
86
+ - Tunggu hingga proses ekstraksi dan indexing selesai
87
+
88
+ 2. **Chat dengan Dokumen** (Tab 💬 Chat)
89
+ - Ketik pertanyaan Anda tentang isi dokumen
90
+ - Sistem akan mencari informasi relevan dan menjawab
91
+ - Lihat source citations untuk referensi
92
+
93
+ 3. **Kelola Dokumen** (Tab 📚 Kelola Dokumen)
94
+ - Lihat daftar dokumen yang tersimpan
95
+ - Hapus dokumen jika diperlukan
96
+ - Clear all untuk reset database
97
+
98
+ 4. **Info & Settings** (Tab ℹ️ Info & Pengaturan)
99
+ - Lihat informasi sistem
100
+ - Dokumentasi dan tips
101
+
102
+ ## 🏗️ Arsitektur
103
+
104
+ ```
105
+ ┌─────────────────┐
106
+ │ PDF Upload │
107
+ └────────┬────────┘
108
+
109
+
110
+ ┌─────────────────┐
111
+ │ Text Extraction │ (PyPDF2 + pdfplumber)
112
+ └────────┬────────┘
113
+
114
+
115
+ ┌─────────────────┐
116
+ │ Text Chunking │ (LangChain)
117
+ └────────┬────────┘
118
+
119
+
120
+ ┌─────────────────┐
121
+ │ Embeddings │ (SentenceTransformers)
122
+ └────────┬────────┘
123
+
124
+
125
+ ┌─────────────────┐
126
+ │ ChromaDB │ (Vector Storage)
127
+ └────────┬────────┘
128
+
129
+ ┌────┴─────┐
130
+ │ RAG │
131
+ └────┬─────┘
132
+
133
+ ┌────▼─────┐
134
+ │ChatGLM3 │ (Response Generation)
135
+ └──────────┘
136
+ ```
137
+
138
+ ## 📁 Struktur Project
139
+
140
+ ```
141
+ LLM-ChatBot-Document/
142
+
143
+ ├── app.py # Main application
144
+ ├── requirements.txt # Dependencies
145
+ ├── .env.example # Environment template
146
+ ├── .gitignore # Git ignore rules
147
+
148
+ ├── config/
149
+ │ ├── __init__.py
150
+ │ └── model_config.py # Model & app configuration
151
+
152
+ ├── utils/
153
+ │ ├── __init__.py
154
+ │ ├── pdf_processor.py # PDF extraction & chunking
155
+ │ ├── vector_store.py # ChromaDB management
156
+ │ ├── rag_pipeline.py # RAG implementation
157
+ │ └── ui_components.py # Gradio UI components
158
+
159
+ ├── data/
160
+ │ ├── uploads/ # Temporary PDF storage
161
+ │ └── vector_db/ # ChromaDB persistent storage
162
+
163
+ └── tests/ # Unit & integration tests
164
+ ```
165
+
166
+ ## ⚙️ Konfigurasi
167
+
168
+ Edit file `.env` untuk mengatur konfigurasi:
169
+
170
+ ```bash
171
+ # Model
172
+ MODEL_NAME=THUDM/chatglm3-6b
173
+ EMBEDDING_MODEL=sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
174
+
175
+ # Device (auto/cuda/cpu)
176
+ DEVICE=auto
177
+
178
+ # Text Processing
179
+ CHUNK_SIZE=500
180
+ CHUNK_OVERLAP=50
181
+
182
+ # Retrieval
183
+ TOP_K_RETRIEVAL=3
184
+
185
+ # Generation
186
+ MAX_LENGTH=2048
187
+ TEMPERATURE=0.7
188
+ TOP_P=0.9
189
+ ```
190
+
191
+ ## 🔧 Requirements
192
+
193
+ Berikut dependencies utama yang digunakan:
194
+
195
+ - `gradio==5.42.0` - Web interface
196
+ - `torch>=2.0.0` - Deep learning framework
197
+ - `transformers>=4.35.0` - Model loading
198
+ - `sentence-transformers>=2.2.2` - Embeddings
199
+ - `chromadb>=0.4.22` - Vector database
200
+ - `langchain>=0.1.0` - Text processing
201
+ - `PyPDF2>=3.0.0` - PDF extraction
202
+ - `pdfplumber>=0.10.0` - Alternative PDF extraction
203
+
204
+ ## 💡 Tips & Best Practices
205
+
206
+ 1. **Ukuran PDF**: Untuk hasil terbaik, gunakan PDF < 50MB
207
+ 2. **Format PDF**: Pastikan PDF berisi teks yang bisa di-extract (bukan scan gambar)
208
+ 3. **Chunk Size**: Sesuaikan `CHUNK_SIZE` berdasarkan jenis dokumen (500-1000 optimal)
209
+ 4. **GPU**: Gunakan GPU untuk loading model yang lebih cepat
210
+ 5. **Temperature**: Nilai lebih rendah (0.3-0.5) untuk jawaban lebih faktual
211
+
212
+ ## 🐛 Troubleshooting
213
+
214
+ ### Model Loading Error
215
+ ```bash
216
+ # Jika model terlalu besar, gunakan quantized version
217
+ MODEL_NAME=THUDM/chatglm3-6b-32k
218
+ ```
219
+
220
+ ### PDF Extraction Error
221
+ - Coba method alternatif dengan edit `pdf_processor.py`
222
+ - Pastikan PDF tidak ter-password
223
+
224
+ ### Memory Error
225
+ - Reduce `CHUNK_SIZE` and `BATCH_SIZE`
226
+ - Use CPU instead of GPU if OOM on GPU
227
+
228
+ ## 📝 License
229
+
230
+ MIT License - lihat file LICENSE untuk detail
231
+
232
+ ## 🤝 Contributing
233
+
234
+ Contributions welcome! Silakan buat issue atau pull request.
235
+
236
+ ## 📧 Contact
237
+
238
+ Untuk pertanyaan dan support, silakan buat issue di repository ini.
239
+
240
  ---
241
 
242
+ <div align="center">
243
+ Made with ❤️ using Gradio and ChatGLM
244
+ </div>
app.py CHANGED
@@ -1,70 +1,415 @@
 
 
 
 
 
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
3
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- def respond(
6
- message,
7
- history: list[dict[str, str]],
8
- system_message,
9
- max_tokens,
10
- temperature,
11
- top_p,
12
- hf_token: gr.OAuthToken,
13
- ):
14
- """
15
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
16
- """
17
- client = InferenceClient(token=hf_token.token, model="openai/gpt-oss-20b")
18
 
19
- messages = [{"role": "system", "content": system_message}]
 
20
 
21
- messages.extend(history)
22
 
23
- messages.append({"role": "user", "content": message})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- response = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- for message in client.chat_completion(
28
- messages,
29
- max_tokens=max_tokens,
30
- stream=True,
31
- temperature=temperature,
32
- top_p=top_p,
33
- ):
34
- choices = message.choices
35
- token = ""
36
- if len(choices) and choices[0].delta.content:
37
- token = choices[0].delta.content
 
 
 
 
38
 
39
- response += token
40
- yield response
 
 
 
 
 
 
 
 
 
 
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- chatbot = gr.ChatInterface(
47
- respond,
48
- type="messages",
49
- additional_inputs=[
50
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
51
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
52
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
53
- gr.Slider(
54
- minimum=0.1,
55
- maximum=1.0,
56
- value=0.95,
57
- step=0.05,
58
- label="Top-p (nucleus sampling)",
59
- ),
60
- ],
61
- )
62
 
63
- with gr.Blocks() as demo:
64
- with gr.Sidebar():
65
- gr.LoginButton()
66
- chatbot.render()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
 
68
 
69
  if __name__ == "__main__":
70
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG ChatBot dengan GLM Model dan Dashboard Gradio
3
+ Main application file
4
+ """
5
+ import os
6
  import gradio as gr
7
+ from pathlib import Path
8
 
9
+ from config.model_config import config
10
+ from utils.pdf_processor import PDFProcessor
11
+ from utils.vector_store import VectorStore
12
+ from utils.rag_pipeline import RAGPipeline
13
+ from utils.ui_components import (
14
+ CUSTOM_CSS,
15
+ format_sources,
16
+ create_document_card,
17
+ create_status_message
18
+ )
19
 
20
+ # Initialize components
21
+ pdf_processor = PDFProcessor()
22
+ vector_store = VectorStore()
23
+ rag_pipeline = RAGPipeline(vector_store)
 
 
 
 
 
 
 
 
 
24
 
25
+ # Global state
26
+ chat_history = []
27
 
28
+ # ========== Event Handlers ==========
29
 
30
+ def upload_pdf(files, progress=gr.Progress()):
31
+ """Handle PDF upload and processing"""
32
+ if not files:
33
+ return create_status_message("Tidak ada file yang dipilih", "error"), ""
34
+
35
+ results = []
36
+
37
+ for i, file in enumerate(files):
38
+ try:
39
+ progress((i + 1) / len(files), desc=f"Memproses {Path(file.name).name}...")
40
+
41
+ # Process PDF
42
+ pdf_info = pdf_processor.process_pdf(file.name)
43
+
44
+ # Add to vector store
45
+ vector_store.add_document(
46
+ filename=pdf_info["filename"],
47
+ chunks=pdf_info["chunks"],
48
+ metadata={
49
+ "total_chars": pdf_info["total_chars"],
50
+ "num_chunks": pdf_info["num_chunks"]
51
+ }
52
+ )
53
+
54
+ results.append(
55
+ f"✓ {pdf_info['filename']}: {pdf_info['num_chunks']} chunks, {pdf_info['total_chars']} karakter"
56
+ )
57
+
58
+ except Exception as e:
59
+ results.append(f"✗ {Path(file.name).name}: Error - {str(e)}")
60
+
61
+ summary = "\n".join(results)
62
+ status_msg = create_status_message(
63
+ f"Berhasil memproses {len(files)} file",
64
+ "success"
65
+ )
66
+
67
+ # Update document list
68
+ doc_list = get_document_list()
69
+
70
+ return status_msg + f"\n\n{summary}", doc_list
71
 
72
+ def chat_with_rag(message, history, use_rag, temperature, top_p, top_k):
73
+ """Handle chat interaction with RAG"""
74
+ if not message.strip():
75
+ return history, ""
76
+
77
+ # Convert history format for display
78
+ history = history or []
79
+
80
+ # Check if we need to load model
81
+ if rag_pipeline.model is None:
82
+ history.append({
83
+ "role": "assistant",
84
+ "content": "⏳ Loading model untuk pertama kali, mohon tunggu..."
85
+ })
86
+ yield history, ""
87
+
88
+ try:
89
+ rag_pipeline.load_model()
90
+ except Exception as e:
91
+ history[-1] = {
92
+ "role": "assistant",
93
+ "content": f"❌ Error loading model: {str(e)}"
94
+ }
95
+ yield history, ""
96
+ return
97
+
98
+ # Add user message
99
+ history.append({"role": "user", "content": message})
100
+ yield history, ""
101
+
102
+ # Prepare chat history for GLM (convert from Gradio format)
103
+ glm_history = []
104
+ for msg in history[:-1]: # Exclude current message
105
+ if msg["role"] == "user":
106
+ glm_history.append([msg["content"], ""])
107
+ elif msg["role"] == "assistant" and glm_history:
108
+ glm_history[-1][1] = msg["content"]
109
+
110
+ # Generate response
111
+ sources = []
112
+ full_response = ""
113
+
114
+ try:
115
+ for response, src in rag_pipeline.stream_response(
116
+ message,
117
+ history=glm_history,
118
+ use_rag=use_rag,
119
+ temperature=temperature,
120
+ top_p=top_p
121
+ ):
122
+ full_response = response
123
+ sources = src
124
+
125
+ # Update assistant message
126
+ if len(history) > 0 and history[-1]["role"] == "assistant":
127
+ history[-1]["content"] = response
128
+ else:
129
+ history.append({"role": "assistant", "content": response})
130
+
131
+ yield history, ""
132
+
133
+ except Exception as e:
134
+ error_msg = f"❌ Error: {str(e)}"
135
+ if len(history) > 0 and history[-1]["role"] == "assistant":
136
+ history[-1]["content"] = error_msg
137
+ else:
138
+ history.append({"role": "assistant", "content": error_msg})
139
+ yield history, ""
140
+ return
141
+
142
+ # Format sources
143
+ if sources and use_rag:
144
+ sources_html = format_sources(sources)
145
+ yield history, sources_html
146
+ else:
147
+ yield history, ""
148
 
149
+ def get_document_list():
150
+ """Get list of uploaded documents"""
151
+ docs = vector_store.list_documents()
152
+
153
+ if not docs:
154
+ return create_status_message("Belum ada dokumen yang di-upload", "info")
155
+
156
+ html = "<div style='margin-top: 1rem;'>"
157
+ html += f"<h3 style='color: #667eea;'>📚 Dokumen Tersimpan ({len(docs)})</h3>"
158
+
159
+ for doc in docs:
160
+ html += create_document_card(doc)
161
+
162
+ html += "</div>"
163
+ return html
164
 
165
+ def delete_document(filename):
166
+ """Delete a document from vector store"""
167
+ try:
168
+ vector_store.delete_document(filename)
169
+ return (
170
+ create_status_message(f"Berhasil menghapus: {filename}", "success"),
171
+ get_document_list()
172
+ )
173
+ except Exception as e:
174
+ return (
175
+ create_status_message(f"Error: {str(e)}", "error"),
176
+ get_document_list()
177
+ )
178
 
179
+ def clear_all_documents():
180
+ """Clear all documents"""
181
+ try:
182
+ vector_store.clear_all()
183
+ return (
184
+ create_status_message("Semua dokumen berhasil dihapus", "success"),
185
+ get_document_list()
186
+ )
187
+ except Exception as e:
188
+ return (
189
+ create_status_message(f"Error: {str(e)}", "error"),
190
+ get_document_list()
191
+ )
192
 
193
+ # ========== Gradio Interface ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(), title="RAG ChatBot - GLM") as demo:
196
+
197
+ # Header
198
+ gr.HTML("""
199
+ <div class='header-container'>
200
+ <h1 class='header-title'>🤖 RAG ChatBot dengan GLM</h1>
201
+ <p class='header-subtitle'>Chat dengan dokumen PDF Anda menggunakan AI</p>
202
+ </div>
203
+ """)
204
+
205
+ with gr.Tabs() as tabs:
206
+
207
+ # ===== Tab 1: Upload Documents =====
208
+ with gr.Tab("📤 Upload Dokumen"):
209
+ gr.Markdown("""
210
+ ### Upload PDF untuk Analisis
211
+ Upload satu atau beberapa file PDF. Sistem akan mengekstrak teks, membuat chunks, dan menyimpannya untuk retrieval.
212
+ """)
213
+
214
+ with gr.Row():
215
+ with gr.Column(scale=2):
216
+ file_upload = gr.File(
217
+ label="Pilih PDF Files",
218
+ file_types=[".pdf"],
219
+ file_count="multiple"
220
+ )
221
+ upload_btn = gr.Button("🚀 Process PDF", variant="primary", size="lg")
222
+
223
+ with gr.Column(scale=1):
224
+ gr.Markdown("""
225
+ **Tips:**
226
+ - Ukuran optimal: < 50MB per file
227
+ - Format: PDF dengan teks (bukan scan)
228
+ - Multiple files: Upload sekaligus
229
+ """)
230
+
231
+ upload_status = gr.HTML(label="Status")
232
+ upload_btn.click(
233
+ upload_pdf,
234
+ inputs=[file_upload],
235
+ outputs=[upload_status, gr.HTML(visible=False)]
236
+ )
237
+
238
+ # ===== Tab 2: Chat Interface =====
239
+ with gr.Tab("💬 Chat"):
240
+ gr.Markdown("""
241
+ ### Tanya Jawab dengan Dokumen
242
+ Ajukan pertanyaan tentang dokumen yang telah di-upload.
243
+ """)
244
+
245
+ chatbot = gr.Chatbot(
246
+ label="Conversation",
247
+ type="messages",
248
+ height=500,
249
+ avatar_images=(None, "🤖")
250
+ )
251
+
252
+ with gr.Row():
253
+ msg_input = gr.Textbox(
254
+ label="Pesan Anda",
255
+ placeholder="Tanyakan sesuatu tentang dokumen...",
256
+ scale=4
257
+ )
258
+ send_btn = gr.Button("📨 Send", variant="primary", scale=1)
259
+
260
+ sources_display = gr.HTML(label="Sumber Informasi")
261
+
262
+ with gr.Accordion("⚙️ Parameter Chat", open=False):
263
+ with gr.Row():
264
+ use_rag = gr.Checkbox(
265
+ label="Gunakan RAG (Retrieval)",
266
+ value=True,
267
+ info="Matikan untuk chat biasa tanpa dokumen"
268
+ )
269
+ temperature = gr.Slider(
270
+ minimum=0.1,
271
+ maximum=2.0,
272
+ value=config.TEMPERATURE,
273
+ step=0.1,
274
+ label="Temperature",
275
+ info="Kreativitas respons"
276
+ )
277
+ with gr.Row():
278
+ top_p = gr.Slider(
279
+ minimum=0.1,
280
+ maximum=1.0,
281
+ value=config.TOP_P,
282
+ step=0.05,
283
+ label="Top-p",
284
+ info="Nucleus sampling"
285
+ )
286
+ top_k = gr.Slider(
287
+ minimum=1,
288
+ maximum=10,
289
+ value=config.TOP_K_RETRIEVAL,
290
+ step=1,
291
+ label="Top-K Retrieval",
292
+ info="Jumlah chunks yang diambil"
293
+ )
294
+
295
+ clear_btn = gr.Button("🗑️ Clear Chat")
296
+
297
+ # Chat interactions
298
+ send_btn.click(
299
+ chat_with_rag,
300
+ inputs=[msg_input, chatbot, use_rag, temperature, top_p, top_k],
301
+ outputs=[chatbot, sources_display]
302
+ ).then(
303
+ lambda: "",
304
+ outputs=[msg_input]
305
+ )
306
+
307
+ msg_input.submit(
308
+ chat_with_rag,
309
+ inputs=[msg_input, chatbot, use_rag, temperature, top_p, top_k],
310
+ outputs=[chatbot, sources_display]
311
+ ).then(
312
+ lambda: "",
313
+ outputs=[msg_input]
314
+ )
315
+
316
+ clear_btn.click(
317
+ lambda: ([], ""),
318
+ outputs=[chatbot, sources_display]
319
+ )
320
+
321
+ # ===== Tab 3: Document Management =====
322
+ with gr.Tab("📚 Kelola Dokumen"):
323
+ gr.Markdown("""
324
+ ### Dokumen yang Tersimpan
325
+ Lihat dan kelola dokumen yang telah di-upload.
326
+ """)
327
+
328
+ doc_list_display = gr.HTML()
329
+
330
+ with gr.Row():
331
+ refresh_btn = gr.Button("🔄 Refresh List", variant="secondary")
332
+ clear_all_btn = gr.Button("🗑️ Hapus Semua", variant="stop")
333
+
334
+ doc_status = gr.HTML()
335
+
336
+ # Load documents on tab open
337
+ demo.load(
338
+ get_document_list,
339
+ outputs=[doc_list_display]
340
+ )
341
+
342
+ refresh_btn.click(
343
+ get_document_list,
344
+ outputs=[doc_list_display]
345
+ )
346
+
347
+ clear_all_btn.click(
348
+ clear_all_documents,
349
+ outputs=[doc_status, doc_list_display]
350
+ )
351
+
352
+ # ===== Tab 4: About & Settings =====
353
+ with gr.Tab("ℹ️ Info & Pengaturan"):
354
+ gr.Markdown(f"""
355
+ ### RAG ChatBot - Informasi Sistem
356
+
357
+ **Model yang Digunakan:**
358
+ - 🤖 LLM: `{config.MODEL_NAME}`
359
+ - 🔍 Embeddings: `{config.EMBEDDING_MODEL}`
360
+ - 💾 Vector DB: ChromaDB (Persistent)
361
+
362
+ **Konfigurasi:**
363
+ - Chunk Size: {config.CHUNK_SIZE}
364
+ - Chunk Overlap: {config.CHUNK_OVERLAP}
365
+ - Top-K Retrieval: {config.TOP_K_RETRIEVAL}
366
+ - Device: {config.DEVICE}
367
+
368
+ **Fitur:**
369
+ ✓ Upload multiple PDF files
370
+ ✓ Automatic text extraction & chunking
371
+ ✓ Semantic search dengan embeddings
372
+ ✓ Context-aware responses
373
+ ✓ Source citations
374
+ ✓ Persistent storage
375
+
376
+ **Tech Stack:**
377
+ - Framework: Gradio
378
+ - LLM: ChatGLM3 (Transformers)
379
+ - Embeddings: Sentence Transformers
380
+ - Vector DB: ChromaDB
381
+ - PDF Processing: PyPDF2 + pdfplumber
382
+ """)
383
+
384
+ with gr.Accordion("🔧 Advanced Settings", open=False):
385
+ gr.Markdown("""
386
+ Untuk mengubah konfigurasi model, edit file `.env`:
387
+ ```bash
388
+ MODEL_NAME=THUDM/chatglm3-6b
389
+ DEVICE=auto
390
+ CHUNK_SIZE=500
391
+ CHUNK_OVERLAP=50
392
+ ```
393
+ Kemudian restart aplikasi.
394
+ """)
395
 
396
+ # ========== Launch ==========
397
 
398
  if __name__ == "__main__":
399
+ # Ensure directories exist
400
+ os.makedirs(config.UPLOAD_DIR, exist_ok=True)
401
+ os.makedirs(config.VECTOR_DB_DIR, exist_ok=True)
402
+
403
+ print("=" * 60)
404
+ print("🚀 Launching RAG ChatBot dengan GLM")
405
+ print("=" * 60)
406
+ print(f"Model: {config.MODEL_NAME}")
407
+ print(f"Device: {config.DEVICE}")
408
+ print(f"Vector DB: {config.VECTOR_DB_DIR}")
409
+ print("=" * 60)
410
+
411
+ demo.launch(
412
+ server_name="0.0.0.0",
413
+ server_port=7860,
414
+ share=False
415
+ )
config/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Config package for RAG ChatBot"""
config/model_config.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for RAG ChatBot
3
+ """
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ # Load environment variables
8
+ load_dotenv()
9
+
10
+ class Config:
11
+ """Configuration class for RAG ChatBot"""
12
+
13
+ # Model Settings
14
+ MODEL_NAME = os.getenv("MODEL_NAME", "THUDM/chatglm3-6b")
15
+ EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
16
+
17
+ # Device Configuration
18
+ DEVICE = os.getenv("DEVICE", "auto")
19
+
20
+ # Text Processing
21
+ CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "500"))
22
+ CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "50"))
23
+
24
+ # Retrieval Configuration
25
+ TOP_K_RETRIEVAL = int(os.getenv("TOP_K_RETRIEVAL", "3"))
26
+
27
+ # Generation Parameters
28
+ MAX_LENGTH = int(os.getenv("MAX_LENGTH", "2048"))
29
+ TEMPERATURE = float(os.getenv("TEMPERATURE", "0.7"))
30
+ TOP_P = float(os.getenv("TOP_P", "0.9"))
31
+
32
+ # Storage Paths
33
+ UPLOAD_DIR = os.getenv("UPLOAD_DIR", "data/uploads")
34
+ VECTOR_DB_DIR = os.getenv("VECTOR_DB_DIR", "data/vector_db")
35
+
36
+ # Prompt Template
37
+ RAG_PROMPT_TEMPLATE = """Berdasarkan konteks berikut, jawab pertanyaan dengan akurat dan informatif.
38
+
39
+ Konteks:
40
+ {context}
41
+
42
+ Pertanyaan: {question}
43
+
44
+ Jawaban:"""
45
+
46
+ SYSTEM_PROMPT = """Kamu adalah asisten AI yang membantu pengguna memahami dokumen mereka.
47
+ Selalu gunakan informasi dari konteks yang diberikan untuk menjawab pertanyaan.
48
+ Jika informasi tidak ada dalam konteks, katakan dengan jelas bahwa informasi tersebut tidak tersedia dalam dokumen yang di-upload.
49
+ Jawab dalam bahasa Indonesia dengan jelas dan ringkas."""
50
+
51
+ # Create instance
52
+ config = Config()
data/.gitkeep ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Dependencies
2
+ gradio==5.42.0
3
+ torch>=2.0.0
4
+ transformers>=4.35.0
5
+ accelerate>=0.25.0
6
+
7
+ # RAG & Embeddings
8
+ sentence-transformers>=2.2.2
9
+ chromadb>=0.4.22
10
+ langchain>=0.1.0
11
+ langchain-community>=0.0.20
12
+
13
+ # PDF Processing
14
+ PyPDF2>=3.0.0
15
+ pdfplumber>=0.10.0
16
+
17
+ # Utilities
18
+ python-dotenv>=1.0.0
19
+ numpy>=1.24.0
20
+ tqdm>=4.66.0
tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Tests package"""
tests/test_imports.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple script to verify basic imports"""
2
+ import sys
3
+ print("Testing imports...")
4
+
5
+ try:
6
+ # Test core imports
7
+ print("✓ Testing config import...")
8
+ from config.model_config import config
9
+ print(f" Model: {config.MODEL_NAME}")
10
+
11
+ print("✓ Testing PDF processor import...")
12
+ from utils.pdf_processor import PDFProcessor
13
+ pdf_proc = PDFProcessor()
14
+ print(f" Chunk size: {config.CHUNK_SIZE}")
15
+
16
+ print("✓ Testing UI components import...")
17
+ from utils.ui_components import CUSTOM_CSS
18
+ print(f" CSS loaded: {len(CUSTOM_CSS)} chars")
19
+
20
+ print("\n✅ All basic imports successful!")
21
+ print("\nNote: Model and vector store imports require additional dependencies")
22
+ print("Run: pip install -r requirements.txt")
23
+
24
+ except Exception as e:
25
+ print(f"\n❌ Import error: {e}")
26
+ print("\nPlease install dependencies:")
27
+ print("pip install -r requirements.txt")
28
+ sys.exit(1)
tests/test_pdf_processor.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Basic tests for PDF processor
3
+ """
4
+ import pytest
5
+ from utils.pdf_processor import PDFProcessor
6
+
7
+ def test_pdf_processor_init():
8
+ """Test PDF processor initialization"""
9
+ processor = PDFProcessor()
10
+ assert processor is not None
11
+ assert processor.text_splitter is not None
12
+
13
+ def test_chunk_text():
14
+ """Test text chunking"""
15
+ processor = PDFProcessor()
16
+
17
+ sample_text = "This is a test. " * 100
18
+ chunks = processor.chunk_text(sample_text)
19
+
20
+ assert len(chunks) > 0
21
+ assert all(isinstance(chunk, str) for chunk in chunks)
22
+
23
+ # Note: Full PDF tests require actual PDF files
24
+ # Add integration tests with sample PDFs as needed
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Utils package for RAG ChatBot"""
utils/pdf_processor.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PDF Processing utilities for extracting and chunking text from PDF files
3
+ """
4
+ import os
5
+ from typing import List, Dict
6
+ import PyPDF2
7
+ import pdfplumber
8
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
9
+ from config.model_config import config
10
+
11
+ class PDFProcessor:
12
+ """Handle PDF text extraction and processing"""
13
+
14
+ def __init__(self):
15
+ self.text_splitter = RecursiveCharacterTextSplitter(
16
+ chunk_size=config.CHUNK_SIZE,
17
+ chunk_overlap=config.CHUNK_OVERLAP,
18
+ length_function=len,
19
+ separators=["\n\n", "\n", " ", ""]
20
+ )
21
+
22
+ def extract_text_from_pdf(self, pdf_path: str, method: str = "pdfplumber") -> str:
23
+ """
24
+ Extract text from PDF file
25
+
26
+ Args:
27
+ pdf_path: Path to PDF file
28
+ method: Extraction method ('pypdf2' or 'pdfplumber')
29
+
30
+ Returns:
31
+ Extracted text as string
32
+ """
33
+ text = ""
34
+
35
+ try:
36
+ if method == "pdfplumber":
37
+ text = self._extract_with_pdfplumber(pdf_path)
38
+ else:
39
+ text = self._extract_with_pypdf2(pdf_path)
40
+ except Exception as e:
41
+ print(f"Error extracting text from {pdf_path}: {e}")
42
+ # Fallback to alternative method
43
+ if method == "pdfplumber":
44
+ text = self._extract_with_pypdf2(pdf_path)
45
+ else:
46
+ text = self._extract_with_pdfplumber(pdf_path)
47
+
48
+ return text
49
+
50
+ def _extract_with_pypdf2(self, pdf_path: str) -> str:
51
+ """Extract text using PyPDF2"""
52
+ text = ""
53
+ with open(pdf_path, 'rb') as file:
54
+ pdf_reader = PyPDF2.PdfReader(file)
55
+ for page in pdf_reader.pages:
56
+ text += page.extract_text() + "\n"
57
+ return text
58
+
59
+ def _extract_with_pdfplumber(self, pdf_path: str) -> str:
60
+ """Extract text using pdfplumber (better for complex PDFs)"""
61
+ text = ""
62
+ with pdfplumber.open(pdf_path) as pdf:
63
+ for page in pdf.pages:
64
+ page_text = page.extract_text()
65
+ if page_text:
66
+ text += page_text + "\n"
67
+ return text
68
+
69
+ def chunk_text(self, text: str) -> List[str]:
70
+ """
71
+ Split text into chunks
72
+
73
+ Args:
74
+ text: Input text to chunk
75
+
76
+ Returns:
77
+ List of text chunks
78
+ """
79
+ chunks = self.text_splitter.split_text(text)
80
+ return chunks
81
+
82
+ def process_pdf(self, pdf_path: str) -> Dict:
83
+ """
84
+ Complete processing pipeline: extract and chunk PDF
85
+
86
+ Args:
87
+ pdf_path: Path to PDF file
88
+
89
+ Returns:
90
+ Dictionary with filename, text, and chunks
91
+ """
92
+ filename = os.path.basename(pdf_path)
93
+
94
+ # Extract text
95
+ text = self.extract_text_from_pdf(pdf_path)
96
+
97
+ if not text.strip():
98
+ raise ValueError(f"No text extracted from {filename}")
99
+
100
+ # Chunk text
101
+ chunks = self.chunk_text(text)
102
+
103
+ return {
104
+ "filename": filename,
105
+ "full_text": text,
106
+ "chunks": chunks,
107
+ "num_chunks": len(chunks),
108
+ "total_chars": len(text)
109
+ }
110
+
111
+ def get_pdf_info(self, pdf_path: str) -> Dict:
112
+ """
113
+ Get metadata about PDF file
114
+
115
+ Args:
116
+ pdf_path: Path to PDF file
117
+
118
+ Returns:
119
+ Dictionary with PDF metadata
120
+ """
121
+ info = {
122
+ "filename": os.path.basename(pdf_path),
123
+ "file_size": os.path.getsize(pdf_path),
124
+ "num_pages": 0
125
+ }
126
+
127
+ try:
128
+ with open(pdf_path, 'rb') as file:
129
+ pdf_reader = PyPDF2.PdfReader(file)
130
+ info["num_pages"] = len(pdf_reader.pages)
131
+ except Exception as e:
132
+ print(f"Error getting PDF info: {e}")
133
+
134
+ return info
utils/rag_pipeline.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG Pipeline for retrieving relevant context and generating responses
3
+ """
4
+ from typing import List, Dict, Optional
5
+ import torch
6
+ from transformers import AutoTokenizer, AutoModel
7
+ from config.model_config import config
8
+ from utils.vector_store import VectorStore
9
+
10
+ class RAGPipeline:
11
+ """RAG pipeline integrating retrieval and generation"""
12
+
13
+ def __init__(self, vector_store: VectorStore):
14
+ """
15
+ Initialize RAG pipeline
16
+
17
+ Args:
18
+ vector_store: VectorStore instance for retrieval
19
+ """
20
+ self.vector_store = vector_store
21
+ self.model = None
22
+ self.tokenizer = None
23
+ self.device = self._get_device()
24
+
25
+ def _get_device(self) -> str:
26
+ """Determine device (cuda/cpu) to use"""
27
+ if config.DEVICE == "auto":
28
+ return "cuda" if torch.cuda.is_available() else "cpu"
29
+ return config.DEVICE
30
+
31
+ def load_model(self):
32
+ """Load GLM model and tokenizer"""
33
+ if self.model is not None:
34
+ print("Model already loaded")
35
+ return
36
+
37
+ print(f"Loading model: {config.MODEL_NAME}")
38
+ print(f"Using device: {self.device}")
39
+
40
+ try:
41
+ self.tokenizer = AutoTokenizer.from_pretrained(
42
+ config.MODEL_NAME,
43
+ trust_remote_code=True
44
+ )
45
+
46
+ self.model = AutoModel.from_pretrained(
47
+ config.MODEL_NAME,
48
+ trust_remote_code=True,
49
+ torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
50
+ ).to(self.device)
51
+
52
+ # Set to evaluation mode
53
+ self.model = self.model.eval()
54
+
55
+ print(f"✓ Model loaded successfully on {self.device}")
56
+
57
+ except Exception as e:
58
+ print(f"Error loading model: {e}")
59
+ raise
60
+
61
+ def retrieve_relevant_chunks(self, query: str, top_k: Optional[int] = None) -> Dict:
62
+ """
63
+ Retrieve relevant document chunks for query
64
+
65
+ Args:
66
+ query: User query
67
+ top_k: Number of chunks to retrieve
68
+
69
+ Returns:
70
+ Dictionary with retrieved documents and metadata
71
+ """
72
+ return self.vector_store.query(query, top_k=top_k)
73
+
74
+ def build_context_prompt(self, query: str, retrieved_docs: List[str]) -> str:
75
+ """
76
+ Build prompt with retrieved context
77
+
78
+ Args:
79
+ query: User query
80
+ retrieved_docs: List of retrieved document chunks
81
+
82
+ Returns:
83
+ Formatted prompt string
84
+ """
85
+ if not retrieved_docs:
86
+ return f"Pertanyaan: {query}\n\nJawaban:"
87
+
88
+ # Combine retrieved documents as context
89
+ context = "\n\n".join([
90
+ f"[Dokumen {i+1}]\n{doc}"
91
+ for i, doc in enumerate(retrieved_docs)
92
+ ])
93
+
94
+ # Use template from config
95
+ prompt = config.RAG_PROMPT_TEMPLATE.format(
96
+ context=context,
97
+ question=query
98
+ )
99
+
100
+ return prompt
101
+
102
+ def generate_response(
103
+ self,
104
+ query: str,
105
+ history: Optional[List] = None,
106
+ use_rag: bool = True,
107
+ max_length: Optional[int] = None,
108
+ temperature: Optional[float] = None,
109
+ top_p: Optional[float] = None
110
+ ) -> tuple:
111
+ """
112
+ Generate response using RAG pipeline
113
+
114
+ Args:
115
+ query: User query
116
+ history: Chat history (for ChatGLM format)
117
+ use_rag: Whether to use RAG retrieval
118
+ max_length: Maximum response length
119
+ temperature: Sampling temperature
120
+ top_p: Nucleus sampling parameter
121
+
122
+ Returns:
123
+ Tuple of (response, sources)
124
+ """
125
+ if self.model is None:
126
+ self.load_model()
127
+
128
+ # Set default parameters
129
+ max_length = max_length or config.MAX_LENGTH
130
+ temperature = temperature or config.TEMPERATURE
131
+ top_p = top_p or config.TOP_P
132
+
133
+ sources = []
134
+
135
+ if use_rag:
136
+ # Retrieve relevant chunks
137
+ retrieval_results = self.retrieve_relevant_chunks(query)
138
+ retrieved_docs = retrieval_results["documents"]
139
+ sources = retrieval_results["metadatas"]
140
+
141
+ if not retrieved_docs:
142
+ return "Maaf, tidak ada dokumen yang relevan ditemukan. Silakan upload dokumen terlebih dahulu.", []
143
+
144
+ # Build prompt with context
145
+ prompt = self.build_context_prompt(query, retrieved_docs)
146
+ else:
147
+ prompt = query
148
+
149
+ # Generate response using ChatGLM
150
+ try:
151
+ response, history = self.model.chat(
152
+ self.tokenizer,
153
+ prompt,
154
+ history=history or [],
155
+ max_length=max_length,
156
+ temperature=temperature,
157
+ top_p=top_p
158
+ )
159
+
160
+ return response, sources
161
+
162
+ except Exception as e:
163
+ print(f"Error generating response: {e}")
164
+ return f"Maaf, terjadi kesalahan saat menggenerate respons: {str(e)}", []
165
+
166
+ def stream_response(
167
+ self,
168
+ query: str,
169
+ history: Optional[List] = None,
170
+ use_rag: bool = True,
171
+ max_length: Optional[int] = None,
172
+ temperature: Optional[float] = None,
173
+ top_p: Optional[float] = None
174
+ ):
175
+ """
176
+ Generate streaming response
177
+
178
+ Args:
179
+ query: User query
180
+ history: Chat history
181
+ use_rag: Whether to use RAG retrieval
182
+ max_length: Maximum response length
183
+ temperature: Sampling temperature
184
+ top_p: Nucleus sampling parameter
185
+
186
+ Yields:
187
+ Tuples of (response_chunk, sources)
188
+ """
189
+ if self.model is None:
190
+ self.load_model()
191
+
192
+ # Set default parameters
193
+ max_length = max_length or config.MAX_LENGTH
194
+ temperature = temperature or config.TEMPERATURE
195
+ top_p = top_p or config.TOP_P
196
+
197
+ sources = []
198
+
199
+ if use_rag:
200
+ # Retrieve relevant chunks
201
+ retrieval_results = self.retrieve_relevant_chunks(query)
202
+ retrieved_docs = retrieval_results["documents"]
203
+ sources = retrieval_results["metadatas"]
204
+
205
+ if not retrieved_docs:
206
+ yield "Maaf, tidak ada dokumen yang relevan ditemukan. Silakan upload dokumen terlebih dahulu.", []
207
+ return
208
+
209
+ # Build prompt with context
210
+ prompt = self.build_context_prompt(query, retrieved_docs)
211
+ else:
212
+ prompt = query
213
+
214
+ # Stream response using ChatGLM
215
+ try:
216
+ for response, history in self.model.stream_chat(
217
+ self.tokenizer,
218
+ prompt,
219
+ history=history or [],
220
+ max_length=max_length,
221
+ temperature=temperature,
222
+ top_p=top_p
223
+ ):
224
+ yield response, sources
225
+
226
+ except Exception as e:
227
+ print(f"Error streaming response: {e}")
228
+ yield f"Maaf, terjadi kesalahan: {str(e)}", []
utils/ui_components.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI Components and styling for Gradio interface
3
+ """
4
+
5
+ # Custom CSS for premium design
6
+ CUSTOM_CSS = """
7
+ /* Main theme */
8
+ :root {
9
+ --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ --success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
11
+ --card-bg: rgba(255, 255, 255, 0.05);
12
+ --glass-bg: rgba(255, 255, 255, 0.1);
13
+ }
14
+
15
+ /* Header styling */
16
+ .header-container {
17
+ background: var(--primary-gradient);
18
+ padding: 2rem;
19
+ border-radius: 12px;
20
+ margin-bottom: 1.5rem;
21
+ box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
22
+ }
23
+
24
+ .header-title {
25
+ color: white;
26
+ font-size: 2.5rem;
27
+ font-weight: 700;
28
+ text-align: center;
29
+ margin-bottom: 0.5rem;
30
+ }
31
+
32
+ .header-subtitle {
33
+ color: rgba(255, 255, 255, 0.9);
34
+ text-align: center;
35
+ font-size: 1.1rem;
36
+ }
37
+
38
+ /* Tab styling */
39
+ .tab-nav button {
40
+ font-size: 1rem;
41
+ font-weight: 600;
42
+ padding: 0.75rem 1.5rem;
43
+ border-radius: 8px;
44
+ transition: all 0.3s ease;
45
+ }
46
+
47
+ .tab-nav button:hover {
48
+ transform: translateY(-2px);
49
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
50
+ }
51
+
52
+ /* Card styling */
53
+ .info-card {
54
+ background: var(--card-bg);
55
+ backdrop-filter: blur(10px);
56
+ border-radius: 12px;
57
+ padding: 1.5rem;
58
+ margin: 1rem 0;
59
+ border: 1px solid rgba(255, 255, 255, 0.1);
60
+ }
61
+
62
+ /* Upload area */
63
+ .upload-area {
64
+ border: 2px dashed rgba(102, 126, 234, 0.5);
65
+ border-radius: 12px;
66
+ padding: 2rem;
67
+ text-align: center;
68
+ transition: all 0.3s ease;
69
+ }
70
+
71
+ .upload-area:hover {
72
+ border-color: #667eea;
73
+ background: rgba(102, 126, 234, 0.05);
74
+ }
75
+
76
+ /* Chat messages */
77
+ .message-bubble {
78
+ border-radius: 18px;
79
+ padding: 0.75rem 1rem;
80
+ margin: 0.5rem 0;
81
+ animation: slideIn 0.3s ease;
82
+ }
83
+
84
+ @keyframes slideIn {
85
+ from {
86
+ opacity: 0;
87
+ transform: translateY(10px);
88
+ }
89
+ to {
90
+ opacity: 1;
91
+ transform: translateY(0);
92
+ }
93
+ }
94
+
95
+ /* Source citations */
96
+ .source-citation {
97
+ background: var(--glass-bg);
98
+ border-left: 3px solid #667eea;
99
+ padding: 0.75rem;
100
+ margin: 0.5rem 0;
101
+ border-radius: 6px;
102
+ font-size: 0.9rem;
103
+ }
104
+
105
+ /* Buttons */
106
+ .primary-button {
107
+ background: var(--primary-gradient) !important;
108
+ color: white !important;
109
+ border: none !important;
110
+ padding: 0.75rem 2rem !important;
111
+ border-radius: 8px !important;
112
+ font-weight: 600 !important;
113
+ transition: all 0.3s ease !important;
114
+ }
115
+
116
+ .primary-button:hover {
117
+ transform: translateY(-2px) !important;
118
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
119
+ }
120
+
121
+ /* Status indicators */
122
+ .status-success {
123
+ color: #38ef7d;
124
+ font-weight: 600;
125
+ }
126
+
127
+ .status-error {
128
+ color: #ff6b6b;
129
+ font-weight: 600;
130
+ }
131
+
132
+ /* Loading animation */
133
+ .loading {
134
+ display: inline-block;
135
+ animation: pulse 1.5s ease-in-out infinite;
136
+ }
137
+
138
+ @keyframes pulse {
139
+ 0%, 100% { opacity: 1; }
140
+ 50% { opacity: 0.5; }
141
+ }
142
+
143
+ /* Document cards */
144
+ .doc-card {
145
+ background: var(--glass-bg);
146
+ border-radius: 12px;
147
+ padding: 1rem;
148
+ margin: 0.75rem 0;
149
+ border: 1px solid rgba(255, 255, 255, 0.1);
150
+ transition: all 0.3s ease;
151
+ }
152
+
153
+ .doc-card:hover {
154
+ transform: translateX(5px);
155
+ border-color: #667eea;
156
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
157
+ }
158
+
159
+ /* Responsive */
160
+ @media (max-width: 768px) {
161
+ .header-title {
162
+ font-size: 2rem;
163
+ }
164
+
165
+ .tab-nav button {
166
+ font-size: 0.9rem;
167
+ padding: 0.5rem 1rem;
168
+ }
169
+ }
170
+ """
171
+
172
+ def format_sources(sources: list) -> str:
173
+ """
174
+ Format source citations for display
175
+
176
+ Args:
177
+ sources: List of source metadata
178
+
179
+ Returns:
180
+ Formatted HTML string
181
+ """
182
+ if not sources:
183
+ return ""
184
+
185
+ html = "<div style='margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1);'>"
186
+ html += "<h4 style='color: #667eea; margin-bottom: 0.5rem;'>📚 Sumber:</h4>"
187
+
188
+ for i, source in enumerate(sources, 1):
189
+ filename = source.get('filename', 'Unknown')
190
+ chunk_idx = source.get('chunk_index', 0)
191
+ preview = source.get('chunk_text', '')[:150]
192
+
193
+ html += f"""
194
+ <div class='source-citation'>
195
+ <strong>#{i} {filename}</strong> (Chunk {chunk_idx})
196
+ <br><span style='color: rgba(255,255,255,0.7); font-size: 0.85rem;'>{preview}...</span>
197
+ </div>
198
+ """
199
+
200
+ html += "</div>"
201
+ return html
202
+
203
+ def format_file_size(size_bytes: int) -> str:
204
+ """Format file size in human-readable format"""
205
+ for unit in ['B', 'KB', 'MB', 'GB']:
206
+ if size_bytes < 1024.0:
207
+ return f"{size_bytes:.1f} {unit}"
208
+ size_bytes /= 1024.0
209
+ return f"{size_bytes:.1f} TB"
210
+
211
+ def create_document_card(doc_info: dict) -> str:
212
+ """
213
+ Create HTML card for document display
214
+
215
+ Args:
216
+ doc_info: Document information dictionary
217
+
218
+ Returns:
219
+ HTML string
220
+ """
221
+ filename = doc_info.get('filename', 'Unknown')
222
+ num_chunks = doc_info.get('num_chunks', 0)
223
+
224
+ html = f"""
225
+ <div class='doc-card'>
226
+ <div style='display: flex; justify-content: space-between; align-items: center;'>
227
+ <div>
228
+ <h4 style='margin: 0; color: #667eea;'>📄 {filename}</h4>
229
+ <p style='margin: 0.25rem 0 0 0; color: rgba(255,255,255,0.7); font-size: 0.9rem;'>
230
+ {num_chunks} chunks
231
+ </p>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ """
236
+ return html
237
+
238
+ def create_status_message(message: str, status_type: str = "info") -> str:
239
+ """
240
+ Create styled status message
241
+
242
+ Args:
243
+ message: Status message text
244
+ status_type: Type of status (success, error, info, warning)
245
+
246
+ Returns:
247
+ HTML string
248
+ """
249
+ icons = {
250
+ "success": "✓",
251
+ "error": "✗",
252
+ "info": "ℹ",
253
+ "warning": "⚠"
254
+ }
255
+
256
+ colors = {
257
+ "success": "#38ef7d",
258
+ "error": "#ff6b6b",
259
+ "info": "#667eea",
260
+ "warning": "#ffd93d"
261
+ }
262
+
263
+ icon = icons.get(status_type, "ℹ")
264
+ color = colors.get(status_type, "#667eea")
265
+
266
+ html = f"""
267
+ <div style='padding: 1rem; border-radius: 8px; background: rgba(255,255,255,0.05);
268
+ border-left: 4px solid {color}; margin: 1rem 0;'>
269
+ <span style='color: {color}; font-weight: 600; font-size: 1.1rem;'>{icon} {message}</span>
270
+ </div>
271
+ """
272
+ return html
utils/vector_store.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Vector store management for document embeddings
3
+ """
4
+ import os
5
+ import json
6
+ from typing import List, Dict, Optional
7
+ from sentence_transformers import SentenceTransformer
8
+ import chromadb
9
+ from chromadb.config import Settings
10
+ from config.model_config import config
11
+
12
+ class VectorStore:
13
+ """Manage document embeddings and vector database"""
14
+
15
+ def __init__(self):
16
+ """Initialize embedding model and vector database"""
17
+ print(f"Loading embedding model: {config.EMBEDDING_MODEL}")
18
+ self.embedding_model = SentenceTransformer(config.EMBEDDING_MODEL)
19
+
20
+ # Initialize ChromaDB
21
+ self.client = chromadb.PersistentClient(
22
+ path=config.VECTOR_DB_DIR,
23
+ settings=Settings(anonymized_telemetry=False)
24
+ )
25
+
26
+ # Get or create collection
27
+ self.collection = self.client.get_or_create_collection(
28
+ name="document_chunks",
29
+ metadata={"hnsw:space": "cosine"}
30
+ )
31
+
32
+ # Metadata file to track documents
33
+ self.metadata_file = os.path.join(config.VECTOR_DB_DIR, "documents_metadata.json")
34
+ self.documents_metadata = self._load_metadata()
35
+
36
+ def _load_metadata(self) -> Dict:
37
+ """Load documents metadata from file"""
38
+ if os.path.exists(self.metadata_file):
39
+ with open(self.metadata_file, 'r', encoding='utf-8') as f:
40
+ return json.load(f)
41
+ return {}
42
+
43
+ def _save_metadata(self):
44
+ """Save documents metadata to file"""
45
+ os.makedirs(os.path.dirname(self.metadata_file), exist_ok=True)
46
+ with open(self.metadata_file, 'w', encoding='utf-8') as f:
47
+ json.dump(self.documents_metadata, f, ensure_ascii=False, indent=2)
48
+
49
+ def create_embeddings(self, texts: List[str]) -> List[List[float]]:
50
+ """
51
+ Create embeddings for text chunks
52
+
53
+ Args:
54
+ texts: List of text chunks
55
+
56
+ Returns:
57
+ List of embedding vectors
58
+ """
59
+ embeddings = self.embedding_model.encode(texts, show_progress_bar=True)
60
+ return embeddings.tolist()
61
+
62
+ def add_document(self, filename: str, chunks: List[str], metadata: Optional[Dict] = None):
63
+ """
64
+ Add document chunks to vector store
65
+
66
+ Args:
67
+ filename: Name of the document
68
+ chunks: List of text chunks
69
+ metadata: Additional metadata about the document
70
+ """
71
+ if not chunks:
72
+ raise ValueError("No chunks provided")
73
+
74
+ # Generate unique IDs for chunks
75
+ doc_id = filename.replace(" ", "_").replace(".", "_")
76
+ chunk_ids = [f"{doc_id}_chunk_{i}" for i in range(len(chunks))]
77
+
78
+ # Create embeddings
79
+ print(f"Creating embeddings for {len(chunks)} chunks...")
80
+ embeddings = self.create_embeddings(chunks)
81
+
82
+ # Prepare metadata for each chunk
83
+ chunk_metadata = []
84
+ for i, chunk in enumerate(chunks):
85
+ chunk_meta = {
86
+ "filename": filename,
87
+ "chunk_index": i,
88
+ "chunk_text": chunk[:200] # Store preview
89
+ }
90
+ if metadata:
91
+ chunk_meta.update(metadata)
92
+ chunk_metadata.append(chunk_meta)
93
+
94
+ # Add to collection
95
+ self.collection.add(
96
+ ids=chunk_ids,
97
+ embeddings=embeddings,
98
+ documents=chunks,
99
+ metadatas=chunk_metadata
100
+ )
101
+
102
+ # Update documents metadata
103
+ self.documents_metadata[filename] = {
104
+ "num_chunks": len(chunks),
105
+ "doc_id": doc_id,
106
+ **(metadata or {})
107
+ }
108
+ self._save_metadata()
109
+
110
+ print(f"✓ Added {len(chunks)} chunks from '{filename}' to vector store")
111
+
112
+ def query(self, query_text: str, top_k: int = None) -> Dict:
113
+ """
114
+ Query vector store for relevant chunks
115
+
116
+ Args:
117
+ query_text: Query string
118
+ top_k: Number of results to return
119
+
120
+ Returns:
121
+ Dictionary with results
122
+ """
123
+ if top_k is None:
124
+ top_k = config.TOP_K_RETRIEVAL
125
+
126
+ # Create query embedding
127
+ query_embedding = self.embedding_model.encode([query_text])[0].tolist()
128
+
129
+ # Query collection
130
+ results = self.collection.query(
131
+ query_embeddings=[query_embedding],
132
+ n_results=top_k
133
+ )
134
+
135
+ return {
136
+ "documents": results["documents"][0] if results["documents"] else [],
137
+ "metadatas": results["metadatas"][0] if results["metadatas"] else [],
138
+ "distances": results["distances"][0] if results["distances"] else []
139
+ }
140
+
141
+ def delete_document(self, filename: str):
142
+ """
143
+ Delete all chunks of a document from vector store
144
+
145
+ Args:
146
+ filename: Name of document to delete
147
+ """
148
+ if filename not in self.documents_metadata:
149
+ raise ValueError(f"Document '{filename}' not found")
150
+
151
+ doc_id = self.documents_metadata[filename]["doc_id"]
152
+
153
+ # Get all chunk IDs for this document
154
+ results = self.collection.get(
155
+ where={"filename": filename}
156
+ )
157
+
158
+ if results["ids"]:
159
+ self.collection.delete(ids=results["ids"])
160
+ print(f"✓ Deleted {len(results['ids'])} chunks from '{filename}'")
161
+
162
+ # Remove from metadata
163
+ del self.documents_metadata[filename]
164
+ self._save_metadata()
165
+
166
+ def list_documents(self) -> List[Dict]:
167
+ """
168
+ List all documents in vector store
169
+
170
+ Returns:
171
+ List of document metadata
172
+ """
173
+ return [
174
+ {"filename": name, **meta}
175
+ for name, meta in self.documents_metadata.items()
176
+ ]
177
+
178
+ def clear_all(self):
179
+ """Clear all documents from vector store"""
180
+ self.client.delete_collection("document_chunks")
181
+ self.collection = self.client.get_or_create_collection(
182
+ name="document_chunks",
183
+ metadata={"hnsw:space": "cosine"}
184
+ )
185
+ self.documents_metadata = {}
186
+ self._save_metadata()
187
+ print("✓ Cleared all documents from vector store")