neuralworm commited on
Commit
a75551e
·
1 Parent(s): d9974ec
Files changed (2) hide show
  1. app.py +53 -50
  2. requirements.txt +6 -9
app.py CHANGED
@@ -1,16 +1,22 @@
1
- # app.py - v1.1
2
  # Beschreibung: State-of-the-Art RAG-Pipeline mit Gemma-3, FAISS,
3
  # semantischem Chunking und Streaming-Antworten.
 
4
 
5
  import os
6
  import torch
7
  import gradio as gr
8
 
9
- from typing import List, Tuple, Dict, Generator
 
10
 
 
11
  from sentence_transformers import SentenceTransformer
12
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextStreamer
 
 
13
  from pypdf import PdfReader
 
14
  from langchain_text_splitters import RecursiveCharacterTextSplitter
15
  from langchain_community.vectorstores import FAISS
16
 
@@ -77,14 +83,14 @@ def extract_text_from_file(path: str) -> str:
77
  return f.read()
78
 
79
  if ext == ".pdf":
80
- text = []
81
  try:
82
  reader = PdfReader(path)
83
  for page in reader.pages:
84
  page_text = page.extract_text()
85
  if page_text:
86
- text.append(page_text)
87
- return "\n\n".join(text) # Seiten mit Doppel-Zeilenumbruch trennen
88
  except Exception as e:
89
  print(f"Fehler beim Lesen von PDF {path}: {e}")
90
  return ""
@@ -120,55 +126,45 @@ def index_files(file_paths: List[str], progress=gr.Progress(track_tqdm=True)) ->
120
  embed_model = get_embedding_model()
121
  text_splitter = get_text_splitter()
122
 
123
- all_chunks = []
124
- all_metadatas = []
 
125
 
126
- for path in progress.tqdm(file_paths, desc="Dateien verarbeiten"):
127
- if path is None:
128
- continue
129
  text = extract_text_from_file(path)
130
- if not text.strip():
131
- continue
132
 
133
  chunks = text_splitter.split_text(text)
134
- if not chunks:
135
- continue
136
 
137
  source_name = os.path.basename(path)
138
  for chunk in chunks:
139
- all_chunks.append(chunk)
140
- all_metadatas.append({"source": source_name})
141
 
142
- if not all_chunks:
143
  return "Kein Text in den Dateien gefunden, der indexiert werden konnte."
144
 
145
- # Embeddings in Batches erstellen für Effizienz
146
- progress(0.7, desc="Embeddings erstellen...")
147
- embeddings = embed_model.encode(all_chunks, show_progress_bar=True)
148
-
149
- progress(0.9, desc="FAISS Index aufbauen...")
150
- new_store = FAISS.from_texts_and_embeddings(
151
- texts=list(zip(all_chunks, all_metadatas)), # Workaround to store metadata
152
- embedding=embed_model, # Pass the model directly
153
- )
154
 
155
- # Workaround: Manually set texts and metadatas in FAISS index
156
- new_store.index_to_docstore_id = {i: str(i) for i in range(len(all_chunks))}
157
- new_store.docstore._dict = {str(i): gr.Document(page_content=all_chunks[i], metadata=all_metadatas[i]) for i in range(len(all_chunks))}
158
- new_store.index.add(embeddings)
159
 
160
  if VECTOR_STORE is None:
161
  VECTOR_STORE = new_store
162
  else:
163
- VECTOR_STORE.merge_from(new_store)
 
164
 
165
- return f"Index aktualisiert: {len(VECTOR_STORE.index_to_docstore_id)} Chunks insgesamt."
166
 
167
 
168
  def clear_index() -> str:
169
  """Leert den Vektorindex."""
170
  global VECTOR_STORE
171
  VECTOR_STORE = None
 
 
 
172
  return "Index geleert."
173
 
174
 
@@ -177,7 +173,7 @@ def retrieve_relevant_chunks(query: str, top_k: int = 5) -> List[Dict]:
177
  if VECTOR_STORE is None:
178
  return []
179
 
180
- # FAISS gibt Dokumente mit Metadaten und Scores zurück
181
  results_with_scores = VECTOR_STORE.similarity_search_with_score(query, k=top_k)
182
 
183
  formatted_results = []
@@ -185,7 +181,7 @@ def retrieve_relevant_chunks(query: str, top_k: int = 5) -> List[Dict]:
185
  formatted_results.append({
186
  "content": doc.page_content,
187
  "source": doc.metadata.get("source", "Unbekannt"),
188
- "score": 1 - score # FAISS L2-Distanz -> Kosinus-Ähnlichkeit (ungefähr)
189
  })
190
  return formatted_results
191
 
@@ -225,10 +221,7 @@ def answer_with_rag(question: str, history: List[Tuple[str, str]]) -> Generator[
225
  model, processor = get_llm()
226
  streamer = TextStreamer(processor, skip_prompt=True, skip_special_tokens=True)
227
 
228
- # 1. Retrieve
229
  retrieved = retrieve_relevant_chunks(question, top_k=5)
230
-
231
- # 2. Prompt
232
  prompt = build_rag_prompt(question, retrieved)
233
  messages = [{"role": "user", "content": prompt}]
234
 
@@ -239,7 +232,6 @@ def answer_with_rag(question: str, history: List[Tuple[str, str]]) -> Generator[
239
  return_tensors="pt"
240
  ).to(model.device)
241
 
242
- # 3. Generate
243
  generation_kwargs = dict(
244
  inputs,
245
  streamer=streamer,
@@ -249,12 +241,9 @@ def answer_with_rag(question: str, history: List[Tuple[str, str]]) -> Generator[
249
  top_p=0.9,
250
  )
251
 
252
- # In einem Thread ausführen, um das UI nicht zu blockieren
253
- from threading import Thread
254
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
255
  thread.start()
256
 
257
- # Streamer-Ausgabe an das UI weitergeben
258
  for new_text in streamer:
259
  yield new_text
260
 
@@ -266,7 +255,7 @@ def build_demo() -> gr.Blocks:
266
  with gr.Blocks(title="Gemma 3 RAG mit FAISS", theme="soft") as demo:
267
  gr.Markdown(
268
  """
269
- # 🔍 Gemma 3 RAG v1.1 – mit FAISS & Streaming
270
  **Eine "State of the Art" RAG-Pipeline mit `google/embeddinggemma-300m` und `google/gemma-3-4b-it`**
271
  1. Lade deine Dokumente hoch und klicke auf "Index aktualisieren".
272
  2. Stelle deine Fragen im Chatfenster. Die Antworten werden live gestreamt.
@@ -316,21 +305,35 @@ def build_demo() -> gr.Blocks:
316
  )
317
  send_btn = gr.Button("Senden", variant="primary", scale=1)
318
 
319
- def chat_interface(message, history):
320
- if not message.strip():
 
321
  return history
322
 
 
323
  history.append((message, ""))
324
 
325
- # Stream die Antwort in das letzte Element der History
326
  for token in answer_with_rag(message, history):
327
  history[-1] = (message, history[-1][1] + token)
328
  yield history
329
 
330
- # Event Listeners
331
- msg_textbox.submit(chat_interface, [msg_textbox, chatbot], chatbot)
332
- send_btn.click(chat_interface, [msg_textbox, chatbot], chatbot).then(
333
- lambda: gr.update(value=""), outputs=msg_textbox
 
 
 
 
 
 
 
 
 
 
 
 
334
  )
335
 
336
  return demo
@@ -338,5 +341,5 @@ def build_demo() -> gr.Blocks:
338
 
339
  if __name__ == "__main__":
340
  app_demo = build_demo()
341
- # Share=True erzeugt einen öffentlichen Link (nützlich für Colab/Remote)
342
  app_demo.launch(share=True)
 
1
+ # app.py - v1.2
2
  # Beschreibung: State-of-the-Art RAG-Pipeline mit Gemma-3, FAISS,
3
  # semantischem Chunking und Streaming-Antworten.
4
+ # (Fix: Korrekte FAISS-Indexierung mit langchain.Document)
5
 
6
  import os
7
  import torch
8
  import gradio as gr
9
 
10
+ from typing import List, Tuple, Generator, Dict
11
+ from threading import Thread
12
 
13
+ # ML / Transformers
14
  from sentence_transformers import SentenceTransformer
15
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextStreamer
16
+
17
+ # Dokumentenverarbeitung & RAG
18
  from pypdf import PdfReader
19
+ from langchain_core.documents import Document
20
  from langchain_text_splitters import RecursiveCharacterTextSplitter
21
  from langchain_community.vectorstores import FAISS
22
 
 
83
  return f.read()
84
 
85
  if ext == ".pdf":
86
+ text_parts = []
87
  try:
88
  reader = PdfReader(path)
89
  for page in reader.pages:
90
  page_text = page.extract_text()
91
  if page_text:
92
+ text_parts.append(page_text)
93
+ return "\n\n".join(text_parts)
94
  except Exception as e:
95
  print(f"Fehler beim Lesen von PDF {path}: {e}")
96
  return ""
 
126
  embed_model = get_embedding_model()
127
  text_splitter = get_text_splitter()
128
 
129
+ documents: List[Document] = []
130
+ for path in progress.tqdm(file_paths, desc="1/2: Dateien verarbeiten & chunken"):
131
+ if path is None: continue
132
 
 
 
 
133
  text = extract_text_from_file(path)
134
+ if not text.strip(): continue
 
135
 
136
  chunks = text_splitter.split_text(text)
137
+ if not chunks: continue
 
138
 
139
  source_name = os.path.basename(path)
140
  for chunk in chunks:
141
+ doc = Document(page_content=chunk, metadata={"source": source_name})
142
+ documents.append(doc)
143
 
144
+ if not documents:
145
  return "Kein Text in den Dateien gefunden, der indexiert werden konnte."
146
 
147
+ progress(0.5, desc="2/2: Embeddings erstellen & FAISS Index aufbauen...")
 
 
 
 
 
 
 
 
148
 
149
+ # FAISS.from_documents kümmert sich um Embedding und Indexierung
150
+ new_store = FAISS.from_documents(documents, embed_model)
 
 
151
 
152
  if VECTOR_STORE is None:
153
  VECTOR_STORE = new_store
154
  else:
155
+ # Fügt neue Dokumente zum bestehenden Index hinzu
156
+ VECTOR_STORE.add_documents(documents)
157
 
158
+ return f"Index aktualisiert: {VECTOR_STORE.index.ntotal} Chunks insgesamt."
159
 
160
 
161
  def clear_index() -> str:
162
  """Leert den Vektorindex."""
163
  global VECTOR_STORE
164
  VECTOR_STORE = None
165
+ # Garbage Collection anstoßen, um Speicher freizugeben
166
+ import gc
167
+ gc.collect()
168
  return "Index geleert."
169
 
170
 
 
173
  if VECTOR_STORE is None:
174
  return []
175
 
176
+ # similarity_search_with_score gibt Dokumente und L2-Distanz-Scores zurück
177
  results_with_scores = VECTOR_STORE.similarity_search_with_score(query, k=top_k)
178
 
179
  formatted_results = []
 
181
  formatted_results.append({
182
  "content": doc.page_content,
183
  "source": doc.metadata.get("source", "Unbekannt"),
184
+ "score": 1 - score # Konvertiert Distanz in eine Ähnlichkeits-Metrik
185
  })
186
  return formatted_results
187
 
 
221
  model, processor = get_llm()
222
  streamer = TextStreamer(processor, skip_prompt=True, skip_special_tokens=True)
223
 
 
224
  retrieved = retrieve_relevant_chunks(question, top_k=5)
 
 
225
  prompt = build_rag_prompt(question, retrieved)
226
  messages = [{"role": "user", "content": prompt}]
227
 
 
232
  return_tensors="pt"
233
  ).to(model.device)
234
 
 
235
  generation_kwargs = dict(
236
  inputs,
237
  streamer=streamer,
 
241
  top_p=0.9,
242
  )
243
 
 
 
244
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
245
  thread.start()
246
 
 
247
  for new_text in streamer:
248
  yield new_text
249
 
 
255
  with gr.Blocks(title="Gemma 3 RAG mit FAISS", theme="soft") as demo:
256
  gr.Markdown(
257
  """
258
+ # 🔍 Gemma 3 RAG v1.2 – mit FAISS & Streaming
259
  **Eine "State of the Art" RAG-Pipeline mit `google/embeddinggemma-300m` und `google/gemma-3-4b-it`**
260
  1. Lade deine Dokumente hoch und klicke auf "Index aktualisieren".
261
  2. Stelle deine Fragen im Chatfenster. Die Antworten werden live gestreamt.
 
305
  )
306
  send_btn = gr.Button("Senden", variant="primary", scale=1)
307
 
308
+ def chat_interface(message: str, history: list):
309
+ if not message or not message.strip():
310
+ # Leere Eingaben ignorieren
311
  return history
312
 
313
+ # Neue Nachricht zur History hinzufügen mit Platzhalter für Antwort
314
  history.append((message, ""))
315
 
316
+ # Stream die LLM-Antwort in den Platzhalter
317
  for token in answer_with_rag(message, history):
318
  history[-1] = (message, history[-1][1] + token)
319
  yield history
320
 
321
+ # Event Listeners für das Senden einer Nachricht
322
+ msg_textbox.submit(
323
+ fn=chat_interface,
324
+ inputs=[msg_textbox, chatbot],
325
+ outputs=chatbot,
326
+ ).then(
327
+ fn=lambda: gr.update(value=""), # Textbox leeren
328
+ outputs=msg_textbox
329
+ )
330
+ send_btn.click(
331
+ fn=chat_interface,
332
+ inputs=[msg_textbox, chatbot],
333
+ outputs=chatbot
334
+ ).then(
335
+ fn=lambda: gr.update(value=""), # Textbox leeren
336
+ outputs=msg_textbox
337
  )
338
 
339
  return demo
 
341
 
342
  if __name__ == "__main__":
343
  app_demo = build_demo()
344
+ # Share=True erzeugt einen öffentlichen Link, nützlich für Colab/Remote
345
  app_demo.launch(share=True)
requirements.txt CHANGED
@@ -1,21 +1,18 @@
1
- # requirements.txt - v1.1
2
- # Beschreibung: Aktualisierte Abhängigkeiten für eine robustere RAG-Pipeline.
3
 
4
  # Core ML/UI Frameworks
5
  gradio>=4.44.0
6
  transformers>=4.50.0
7
  torch>=2.1.0
8
  accelerate>=0.33.0
 
9
 
10
  # SentenceTransformers für EmbeddingGemma
11
  sentence-transformers>=5.0.0
12
 
13
- # Dokumentenverarbeitung
14
  pypdf>=5.0.0
15
  langchain-text-splitters>=0.2.0 # Für robustes, semantisches Chunking
16
-
17
- # Vektordatenbank (In-Memory)
18
- faiss-cpu>=1.8.0 # Effiziente Vektorsuche, CPU-Version ist für HF Spaces ideal
19
-
20
- # Optional, aber empfohlen für Quantisierung und Performance
21
- bitsandbytes>=0.43.0
 
1
+ # requirements.txt - v1.2
2
+ # Beschreibung: Korrigierte und erweiterte Abhängigkeiten für eine robuste RAG-Pipeline.
3
 
4
  # Core ML/UI Frameworks
5
  gradio>=4.44.0
6
  transformers>=4.50.0
7
  torch>=2.1.0
8
  accelerate>=0.33.0
9
+ bitsandbytes>=0.43.0
10
 
11
  # SentenceTransformers für EmbeddingGemma
12
  sentence-transformers>=5.0.0
13
 
14
+ # Dokumentenverarbeitung und Vektor-Datenbank
15
  pypdf>=5.0.0
16
  langchain-text-splitters>=0.2.0 # Für robustes, semantisches Chunking
17
+ langchain-community>=0.2.0 # BENÖTIGT für FAISS-Wrapper und Document-Objekt
18
+ faiss-cpu>=1.8.0 # Effiziente Vektorsuche (CPU-Version)