Update app.py
Browse files
app.py
CHANGED
@@ -15,11 +15,17 @@ from pathlib import Path
|
|
15 |
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
16 |
|
17 |
# LLM dan indexing
|
18 |
-
from llama_index.core import Settings, VectorStoreIndex, SimpleDirectoryReader, PromptTemplate
|
|
|
|
|
|
|
|
|
19 |
from llama_index.llms.cerebras import Cerebras
|
20 |
from llama_index.embeddings.nomic import NomicEmbedding
|
21 |
-
from llama_index.core.node_parser import MarkdownNodeParser
|
22 |
from llama_index.readers.docling import DoclingReader
|
|
|
|
|
|
|
23 |
|
24 |
# Speech-to-text dan text-to-speech dengan Groq
|
25 |
from groq import Groq
|
@@ -54,7 +60,13 @@ groq_client = Groq(api_key=GROQ_API_KEY)
|
|
54 |
def load_cerebras_llm():
|
55 |
logging.info("Memuat Cerebras LLM")
|
56 |
try:
|
57 |
-
llm = Cerebras(
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
logging.debug("Cerebras LLM berhasil dimuat")
|
59 |
return llm
|
60 |
except Exception as e:
|
@@ -67,7 +79,8 @@ def create_embedding():
|
|
67 |
embed_model = NomicEmbedding(
|
68 |
model_name="nomic-embed-text-v1.5",
|
69 |
vision_model_name="nomic-embed-vision-v1.5",
|
70 |
-
api_key=NOMIC_API_KEY
|
|
|
71 |
)
|
72 |
Settings.embed_model = embed_model
|
73 |
logging.debug("Embedding model berhasil di-set")
|
@@ -97,27 +110,107 @@ def load_documents(file_list):
|
|
97 |
for doc in docs:
|
98 |
# Menyimpan metadata sumber dokumen
|
99 |
doc.metadata["source"] = file_name
|
|
|
100 |
documents.append(doc)
|
101 |
if not documents:
|
102 |
logging.error("Tidak ditemukan dokumen yang valid.")
|
103 |
return "Tidak ditemukan dokumen yang valid.", None
|
104 |
|
105 |
llm = load_cerebras_llm()
|
106 |
-
create_embedding()
|
107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
# Custom prompt yang memaksa jawaban hanya berdasarkan dokumen
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
{context_str}
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
file_key = f"doc-{uuid.uuid4()}"
|
122 |
global_file_cache[file_key] = query_engine
|
123 |
logging.info(f"Berhasil memuat {len(documents)} dokumen: {', '.join(doc_names)} dengan file_key: {file_key}")
|
@@ -139,15 +232,34 @@ async def document_chat(file_key: str, prompt: str, audio_file=None, translate_a
|
|
139 |
transcription = transcribe_or_translate_audio(audio_file, translate=translate_audio)
|
140 |
logging.debug(f"Hasil transkripsi: {transcription}")
|
141 |
prompt = f"{prompt} {transcription}".strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
142 |
response = await asyncio.to_thread(query_engine.query, prompt)
|
143 |
answer = str(response)
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
except Exception as e:
|
152 |
logging.error(f"Error processing document_chat: {e}")
|
153 |
return history + [(prompt, f"Error processing query: {str(e)}")]
|
@@ -253,7 +365,12 @@ def doc_chat_with_tts(prompt, history, file_key, audio_file, translate, voice, e
|
|
253 |
audio_path = None
|
254 |
else:
|
255 |
logging.info("Memulai konversi jawaban akhir ke audio dengan TTS")
|
256 |
-
|
|
|
|
|
|
|
|
|
|
|
257 |
logging.info(f"Audio output dihasilkan: {audio_path}")
|
258 |
else:
|
259 |
audio_path = None
|
|
|
15 |
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
16 |
|
17 |
# LLM dan indexing
|
18 |
+
from llama_index.core import Settings, VectorStoreIndex, SimpleDirectoryReader, PromptTemplate, ServiceContext
|
19 |
+
from llama_index.core.retrievers import VectorIndexRetriever
|
20 |
+
from llama_index.core.query_engine import RetrieverQueryEngine
|
21 |
+
from llama_index.core.postprocessor import SimilarityPostprocessor, KeywordNodePostprocessor
|
22 |
+
from llama_index.core.node_parser import MarkdownNodeParser, SentenceSplitter
|
23 |
from llama_index.llms.cerebras import Cerebras
|
24 |
from llama_index.embeddings.nomic import NomicEmbedding
|
|
|
25 |
from llama_index.readers.docling import DoclingReader
|
26 |
+
from llama_index.core.response_synthesizers import CompactAndRefine
|
27 |
+
from llama_index.core.vector_stores import MetadataFilters, ExactMatchFilter
|
28 |
+
from llama_index.vector_stores.faiss import FaissVectorStore
|
29 |
|
30 |
# Speech-to-text dan text-to-speech dengan Groq
|
31 |
from groq import Groq
|
|
|
60 |
def load_cerebras_llm():
|
61 |
logging.info("Memuat Cerebras LLM")
|
62 |
try:
|
63 |
+
llm = Cerebras(
|
64 |
+
model="llama-4-scout-17b-16e-instruct",
|
65 |
+
api_key=CEREBRAS_API_KEY,
|
66 |
+
temperature=0.1, # Temperatur rendah untuk mengurangi kreativitas
|
67 |
+
max_tokens=1024, # Batasi panjang output
|
68 |
+
top_p=0.9 # Mengurangi variasi respons
|
69 |
+
)
|
70 |
logging.debug("Cerebras LLM berhasil dimuat")
|
71 |
return llm
|
72 |
except Exception as e:
|
|
|
79 |
embed_model = NomicEmbedding(
|
80 |
model_name="nomic-embed-text-v1.5",
|
81 |
vision_model_name="nomic-embed-vision-v1.5",
|
82 |
+
api_key=NOMIC_API_KEY,
|
83 |
+
embed_batch_size=10 # Batching untuk performa
|
84 |
)
|
85 |
Settings.embed_model = embed_model
|
86 |
logging.debug("Embedding model berhasil di-set")
|
|
|
110 |
for doc in docs:
|
111 |
# Menyimpan metadata sumber dokumen
|
112 |
doc.metadata["source"] = file_name
|
113 |
+
doc.metadata["file_name"] = file_name
|
114 |
documents.append(doc)
|
115 |
if not documents:
|
116 |
logging.error("Tidak ditemukan dokumen yang valid.")
|
117 |
return "Tidak ditemukan dokumen yang valid.", None
|
118 |
|
119 |
llm = load_cerebras_llm()
|
120 |
+
embed_model = create_embedding()
|
121 |
+
|
122 |
+
# Gunakan SentenceSplitter untuk chunking yang lebih baik
|
123 |
+
node_parser = SentenceSplitter(
|
124 |
+
chunk_size=512, # Ukuran chunk
|
125 |
+
chunk_overlap=50, # Overlap antar chunk untuk menjaga konteks
|
126 |
+
separator=" ", # Pemisah
|
127 |
+
paragraph_separator="\n\n",
|
128 |
+
secondary_chunking_regex="[^,.;。]+[,.;。]?",
|
129 |
+
)
|
130 |
+
|
131 |
+
# Set service context untuk pengaturan global
|
132 |
+
service_context = ServiceContext.from_defaults(
|
133 |
+
llm=llm,
|
134 |
+
embed_model=embed_model,
|
135 |
+
node_parser=node_parser
|
136 |
+
)
|
137 |
+
Settings.llm = llm
|
138 |
+
Settings.embed_model = embed_model
|
139 |
+
|
140 |
# Custom prompt yang memaksa jawaban hanya berdasarkan dokumen
|
141 |
+
qa_template = """
|
142 |
+
Kamu adalah asisten yang sangat hati-hati yang hanya menjawab berdasarkan informasi yang ada dalam dokumen.
|
143 |
+
Jika pertanyaan tidak dapat dijawab hanya berdasarkan konteks, katakan "Maaf, saya tidak menemukan informasi tersebut dalam dokumen yang diberikan."
|
144 |
+
|
145 |
+
Jika pertanyaannya tidak relevan dengan dokumen, katakan "Pertanyaan ini tidak relevan dengan dokumen yang sedang dianalisis."
|
146 |
+
|
147 |
+
Jangan pernah mengada-ada atau membuat informasi. Jika kamu tidak yakin, katakan bahwa kamu tidak bisa menjawab dengan pasti berdasarkan dokumen.
|
148 |
+
|
149 |
+
Saat menjawab, selalu berikan kembali sumber informasimu dengan format yang jelas.
|
150 |
+
|
151 |
+
Konteks Dokumen:
|
152 |
{context_str}
|
153 |
+
|
154 |
+
Pertanyaan: {query_str}
|
155 |
+
|
156 |
+
Jawabanmu (hanya berdasarkan konteks dokumen):
|
157 |
+
"""
|
158 |
+
qa_prompt_tmpl = PromptTemplate(qa_template)
|
159 |
+
|
160 |
+
# Inisialisasi FAISS Vector Store
|
161 |
+
vector_store = FaissVectorStore(dim=embed_model.embed_dim)
|
162 |
+
|
163 |
+
# Parse dokumen menjadi node
|
164 |
+
nodes = node_parser.get_nodes_from_documents(documents)
|
165 |
+
|
166 |
+
# Embed nodes dan simpan ke FAISS
|
167 |
+
for i, node in enumerate(nodes):
|
168 |
+
if i % 10 == 0:
|
169 |
+
logging.debug(f"Embedding node {i+1}/{len(nodes)}")
|
170 |
+
node_embedding = embed_model.get_text_embedding(
|
171 |
+
node.get_content(metadata_mode="all")
|
172 |
+
)
|
173 |
+
node.embedding = node_embedding
|
174 |
+
vector_store.add(node_embedding, node.node_id, node)
|
175 |
+
|
176 |
+
logging.info(f"Berhasil embedding {len(nodes)} nodes ke FAISS vector store")
|
177 |
+
|
178 |
+
# Buat index dengan FAISS vector store
|
179 |
+
index = VectorStoreIndex.from_vector_store(
|
180 |
+
vector_store=vector_store,
|
181 |
+
service_context=service_context,
|
182 |
+
show_progress=True
|
183 |
+
)
|
184 |
+
|
185 |
+
# Buat retriever dengan parameter yang dioptimalkan
|
186 |
+
retriever = VectorIndexRetriever(
|
187 |
+
index=index,
|
188 |
+
similarity_top_k=5, # Ambil 5 dokumen teratas
|
189 |
+
vector_store_query_mode="hybrid", # Gunakan hybrid search (keyword + semantic)
|
190 |
+
alpha=0.5 # Bobot untuk hybrid search
|
191 |
+
)
|
192 |
+
|
193 |
+
# Buat postprocessor untuk penyaringan hasil retrieval
|
194 |
+
postprocessors = [
|
195 |
+
SimilarityPostprocessor(similarity_cutoff=0.7), # Hapus hasil dengan skor rendah
|
196 |
+
KeywordNodePostprocessor(required_keywords=[]), # Filter by keyword (opsional)
|
197 |
+
]
|
198 |
+
|
199 |
+
# Buat response synthesizer yang lebih robust
|
200 |
+
response_synthesizer = CompactAndRefine(
|
201 |
+
service_context=service_context,
|
202 |
+
text_qa_template=qa_prompt_tmpl,
|
203 |
+
refine_template=qa_prompt_tmpl,
|
204 |
+
verbose=True
|
205 |
+
)
|
206 |
+
|
207 |
+
# Buat query engine dengan komponen yang dioptimalkan
|
208 |
+
query_engine = RetrieverQueryEngine(
|
209 |
+
retriever=retriever,
|
210 |
+
response_synthesizer=response_synthesizer,
|
211 |
+
node_postprocessors=postprocessors
|
212 |
+
)
|
213 |
+
|
214 |
file_key = f"doc-{uuid.uuid4()}"
|
215 |
global_file_cache[file_key] = query_engine
|
216 |
logging.info(f"Berhasil memuat {len(documents)} dokumen: {', '.join(doc_names)} dengan file_key: {file_key}")
|
|
|
232 |
transcription = transcribe_or_translate_audio(audio_file, translate=translate_audio)
|
233 |
logging.debug(f"Hasil transkripsi: {transcription}")
|
234 |
prompt = f"{prompt} {transcription}".strip()
|
235 |
+
|
236 |
+
# Pastikan prompt valid dan tidak kosong
|
237 |
+
if not prompt or prompt.strip() == "":
|
238 |
+
return history + [("", "Pertanyaan tidak boleh kosong. Silakan ajukan pertanyaan.")]
|
239 |
+
|
240 |
+
# Proses query
|
241 |
response = await asyncio.to_thread(query_engine.query, prompt)
|
242 |
answer = str(response)
|
243 |
+
|
244 |
+
# Tambahkan informasi sumber dokumen dengan format yang lebih jelas
|
245 |
+
sources_text = ""
|
246 |
+
if hasattr(response, "source_nodes") and response.source_nodes:
|
247 |
+
sources = []
|
248 |
+
for i, node in enumerate(response.source_nodes, 1):
|
249 |
+
source = node.metadata.get('source', 'Tidak ada sumber')
|
250 |
+
score = node.score if hasattr(node, 'score') else 'N/A'
|
251 |
+
content_preview = node.get_content()[:100] + "..." if len(node.get_content()) > 100 else node.get_content()
|
252 |
+
sources.append(f"[{i}] Sumber: {source} (Relevansi: {score:.2f})\nPreview: {content_preview}")
|
253 |
+
sources_text = "\n\n" + "Sumber Informasi:\n" + "\n".join(sources)
|
254 |
+
|
255 |
+
# Jika tidak ada sumber yang relevan dan jawaban terlalu generik, kembalikan informasi tidak ditemukan
|
256 |
+
if (not hasattr(response, "source_nodes") or not response.source_nodes) and \
|
257 |
+
not "tidak menemukan informasi" in answer.lower():
|
258 |
+
answer = "Maaf, saya tidak menemukan informasi yang relevan dalam dokumen yang diberikan."
|
259 |
+
|
260 |
+
final_answer = answer + sources_text
|
261 |
+
|
262 |
+
return history + [(prompt, final_answer)]
|
263 |
except Exception as e:
|
264 |
logging.error(f"Error processing document_chat: {e}")
|
265 |
return history + [(prompt, f"Error processing query: {str(e)}")]
|
|
|
365 |
audio_path = None
|
366 |
else:
|
367 |
logging.info("Memulai konversi jawaban akhir ke audio dengan TTS")
|
368 |
+
# Hapus bagian sumber untuk TTS
|
369 |
+
if "Sumber Informasi:" in last_assistant:
|
370 |
+
tts_text = last_assistant.split("Sumber Informasi:")[0].strip()
|
371 |
+
else:
|
372 |
+
tts_text = last_assistant
|
373 |
+
audio_path = convert_text_to_speech(tts_text, voice)
|
374 |
logging.info(f"Audio output dihasilkan: {audio_path}")
|
375 |
else:
|
376 |
audio_path = None
|