neuralworm commited on
Commit
90a2234
·
1 Parent(s): 5e019c1
Files changed (2) hide show
  1. app.py +102 -144
  2. requirements.txt +8 -6
app.py CHANGED
@@ -1,18 +1,16 @@
1
- # app.py - v1.4
2
- # Beschreibung: State-of-the-Art RAG-Pipeline mit Gemma-3, FAISS.
3
- # (Fix: API-Inkompatibilität zwischen SentenceTransformers und LangChain
4
- # durch Verwendung des `SentenceTransformerEmbeddings`-Wrappers behoben.)
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
- # SentenceTransformer ist hier nicht mehr direkt im Code nötig,
15
- # da der LangChain-Wrapper die Nutzung kapselt.
16
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextStreamer
17
 
18
  # Dokumentenverarbeitung & RAG
@@ -20,22 +18,24 @@ from pypdf import PdfReader
20
  from langchain_core.documents import Document
21
  from langchain_text_splitters import RecursiveCharacterTextSplitter
22
  from langchain_community.vectorstores import FAISS
23
- # <-- NEUER IMPORT: Der entscheidende Adapter zwischen den Bibliotheken
24
- from langchain_community.embeddings import SentenceTransformerEmbeddings
25
 
26
  # --------------------------------------------------------------------
27
- # Konfiguration
28
  # --------------------------------------------------------------------
29
  EMBED_MODEL_ID = "google/embeddinggemma-300m"
30
  LLM_MODEL_ID = "google/gemma-3-4b-it"
31
 
32
- # Globale States
33
- # <-- GEÄNDERT: Die Variable hält jetzt ein LangChain-kompatibles Objekt
34
- EMBEDDING_FUNCTION = None
35
  LLM_MODEL: Gemma3ForConditionalGeneration = None
36
  LLM_PROCESSOR: AutoProcessor = None
37
  VECTOR_STORE: FAISS = None
38
 
 
 
 
 
39
 
40
  # --------------------------------------------------------------------
41
  # Model Loading
@@ -43,25 +43,22 @@ VECTOR_STORE: FAISS = None
43
  def get_device() -> torch.device:
44
  """Gibt das verfügbare torch-Device zurück (CUDA, wenn verfügbar)."""
45
  if torch.cuda.is_available():
46
- print("CUDA ist verfügbar. Nutze GPU.")
47
  return torch.device("cuda")
48
- print("CUDA nicht verfügbar. Nutze CPU.")
49
  return torch.device("cpu")
50
 
51
 
52
- def get_embedding_function():
53
- """
54
- Lädt das Embedding-Modell über den LangChain-Wrapper.
55
- Dieser Wrapper sorgt für die API-Kompatibilität.
56
- """
57
  global EMBEDDING_FUNCTION
58
  if EMBEDDING_FUNCTION is None:
59
- # Der Wrapper kümmert sich um das Laden des Modells und die
60
- # korrekte Bereitstellung der .embed_documents() Methode.
61
- EMBEDDING_FUNCTION = SentenceTransformerEmbeddings(
 
62
  model_name=EMBED_MODEL_ID,
63
- model_kwargs={'device': get_device()}
64
  )
 
65
  return EMBEDDING_FUNCTION
66
 
67
 
@@ -70,7 +67,8 @@ def get_llm() -> Tuple[Gemma3ForConditionalGeneration, AutoProcessor]:
70
  global LLM_MODEL, LLM_PROCESSOR
71
  if LLM_MODEL is None or LLM_PROCESSOR is None:
72
  device = get_device()
73
- dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
 
74
 
75
  LLM_MODEL = Gemma3ForConditionalGeneration.from_pretrained(
76
  LLM_MODEL_ID,
@@ -78,6 +76,7 @@ def get_llm() -> Tuple[Gemma3ForConditionalGeneration, AutoProcessor]:
78
  device_map="auto",
79
  ).eval()
80
  LLM_PROCESSOR = AutoProcessor.from_pretrained(LLM_MODEL_ID)
 
81
 
82
  return LLM_MODEL, LLM_PROCESSOR
83
 
@@ -87,54 +86,38 @@ def get_llm() -> Tuple[Gemma3ForConditionalGeneration, AutoProcessor]:
87
  # --------------------------------------------------------------------
88
  def extract_text_from_file(path: str) -> str:
89
  """Extrahiert Text aus verschiedenen Dateitypen."""
 
90
  ext = os.path.splitext(path)[1].lower()
91
-
92
  if ext in [".txt", ".md", ".markdown"]:
93
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
94
- return f.read()
95
-
96
  if ext == ".pdf":
97
  text_parts = []
98
  try:
99
  reader = PdfReader(path)
100
  for page in reader.pages:
101
  page_text = page.extract_text()
102
- if page_text:
103
- text_parts.append(page_text)
104
  return "\n\n".join(text_parts)
105
  except Exception as e:
106
- print(f"Fehler beim Lesen von PDF {path}: {e}")
107
- return ""
108
-
109
  try:
110
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
111
- return f.read()
112
- except Exception:
113
- return ""
114
-
115
 
116
  def get_text_splitter() -> RecursiveCharacterTextSplitter:
117
  """Erstellt einen semantisch bewussten Text-Splitter."""
118
- return RecursiveCharacterTextSplitter(
119
- chunk_size=1000,
120
- chunk_overlap=200,
121
- length_function=len,
122
- add_start_index=True,
123
- )
124
-
125
 
126
  # --------------------------------------------------------------------
127
  # Indexing / RAG mit FAISS
128
  # --------------------------------------------------------------------
129
  def index_files(file_paths: List[str], progress=gr.Progress(track_tqdm=True)) -> str:
130
- """
131
- Liest Dateien, erstellt Chunks und baut/aktualisiert einen FAISS-Vektorindex.
132
- """
133
  global VECTOR_STORE
134
  if not file_paths:
135
  return "Keine Dateien zum Indexieren ausgewählt."
 
136
 
137
- # <-- GEÄNDERT: Holt jetzt die korrekte Wrapper-Instanz
138
  embedding_function = get_embedding_function()
139
  text_splitter = get_text_splitter()
140
 
@@ -143,129 +126,139 @@ def index_files(file_paths: List[str], progress=gr.Progress(track_tqdm=True)) ->
143
  if path is None: continue
144
 
145
  text = extract_text_from_file(path)
146
- if not text.strip(): continue
 
 
147
 
148
  chunks = text_splitter.split_text(text)
149
- if not chunks: continue
150
-
151
  source_name = os.path.basename(path)
152
  for chunk in chunks:
153
  doc = Document(page_content=chunk, metadata={"source": source_name})
154
  documents.append(doc)
155
 
 
 
 
 
156
  if not documents:
157
  return "Kein Text in den Dateien gefunden, der indexiert werden konnte."
158
 
159
  progress(0.5, desc="2/2: Embeddings erstellen & FAISS Index aufbauen...")
160
-
161
- # <-- GEÄNDERT: Übergibt das LangChain-kompatible Embedding-Objekt.
162
- # Dieser Aufruf wird jetzt erfolgreich sein.
163
  new_store = FAISS.from_documents(documents, embedding_function)
 
164
 
165
  if VECTOR_STORE is None:
166
  VECTOR_STORE = new_store
167
  else:
168
  VECTOR_STORE.add_documents(documents)
 
169
 
170
- return f"Index aktualisiert: {VECTOR_STORE.index.ntotal} Chunks insgesamt."
 
171
 
 
 
 
172
 
173
  def clear_index() -> str:
174
  """Leert den Vektorindex."""
175
  global VECTOR_STORE
176
  VECTOR_STORE = None
177
- import gc
178
- gc.collect()
179
  return "Index geleert."
180
 
181
-
182
  def retrieve_relevant_chunks(query: str, top_k: int = 5) -> List[Dict]:
183
  """Sucht die relevantesten Chunks mit FAISS."""
184
  if VECTOR_STORE is None:
 
185
  return []
186
 
 
187
  results_with_scores = VECTOR_STORE.similarity_search_with_score(query, k=top_k)
188
 
189
  formatted_results = []
190
  for doc, score in results_with_scores:
191
  formatted_results.append({
192
- "content": doc.page_content,
193
- "source": doc.metadata.get("source", "Unbekannt"),
194
- "score": 1 - score
195
  })
196
- return formatted_results
197
 
 
 
 
 
 
 
 
198
 
199
  # --------------------------------------------------------------------
200
  # LLM-Generierung mit Streaming
201
  # --------------------------------------------------------------------
202
  def build_rag_prompt(user_question: str, retrieved_chunks: List[Dict]) -> str:
203
- """Baut den RAG-Prompt für das LLM."""
204
  if not retrieved_chunks:
205
  context_str = "Es wurden keine relevanten Dokumente im Kontext gefunden."
206
  else:
207
  context_parts = []
208
  for i, ch in enumerate(retrieved_chunks, start=1):
209
  context_parts.append(
210
- f"Dokument [{i}] (Quelle: {ch['source']}, Relevanz: {ch['score']:.3f}):\n"
211
- f"\"{ch['content']}\""
212
  )
213
  context_str = "\n\n".join(context_parts)
214
-
215
- prompt = (
216
- f"Du bist ein präziser, hilfreicher Assistent. Deine Aufgabe ist es, die folgende Benutzerfrage ausschließlich "
217
- f"basierend auf den unten stehenden Kontext-Dokumenten zu beantworten. "
218
- f"Wenn die Antwort nicht in den Dokumenten enthalten ist, gib klar an: 'Die Information ist in den bereitgestellten Dokumenten nicht enthalten.' "
219
- f"Antworte auf Deutsch und fasse die relevanten Informationen zusammen, anstatt die Dokumente wörtlich zu zitieren.\n\n"
220
- f"--- Kontext-Dokumente ---\n"
221
- f"{context_str}\n\n"
222
- f"--- Benutzerfrage ---\n"
223
- f"{user_question}\n\n"
224
- f"--- Deine Antwort ---\n"
225
- )
226
  return prompt
227
 
228
-
229
  def answer_with_rag(question: str, history: list) -> Generator[str, None, None]:
230
  """Führt RAG durch und generiert eine gestreamte Antwort."""
 
231
  model, processor = get_llm()
232
  streamer = TextStreamer(processor, skip_prompt=True, skip_special_tokens=True)
233
 
234
  retrieved = retrieve_relevant_chunks(question, top_k=5)
235
  prompt = build_rag_prompt(question, retrieved)
236
- messages = [{"role": "user", "content": prompt}]
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
  inputs = processor.apply_chat_template(
239
- messages,
240
- tokenize=True,
241
- add_generation_prompt=True,
242
- return_tensors="pt"
243
  ).to(model.device)
244
 
245
- generation_kwargs = dict(
246
- inputs,
247
- streamer=streamer,
248
- max_new_tokens=1024,
249
- do_sample=True,
250
- temperature=0.7,
251
- top_p=0.9,
252
- )
253
 
254
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
255
  thread.start()
 
256
 
257
  for new_text in streamer:
258
  yield new_text
259
-
260
 
261
  # --------------------------------------------------------------------
262
  # Gradio UI
263
  # --------------------------------------------------------------------
264
  def build_demo() -> gr.Blocks:
265
- with gr.Blocks(title="Gemma 3 RAG mit FAISS", theme="soft") as demo:
266
  gr.Markdown(
267
  """
268
- # 🔍 Gemma 3 RAG v1.4mit FAISS & Streaming
269
  **Eine "State of the Art" RAG-Pipeline mit `google/embeddinggemma-300m` und `google/gemma-3-4b-it`**
270
  1. Lade deine Dokumente hoch und klicke auf "Index aktualisieren".
271
  2. Stelle deine Fragen im Chatfenster. Die Antworten werden live gestreamt.
@@ -274,78 +267,43 @@ def build_demo() -> gr.Blocks:
274
 
275
  with gr.Row():
276
  with gr.Column(scale=1):
 
277
  gr.Markdown("### 📁 Dokumenten-Management")
278
- file_uploader = gr.File(
279
- label="Dateien hochladen (.pdf, .txt, .md)",
280
- file_count="multiple",
281
- type="filepath",
282
- )
283
  with gr.Row():
284
  index_button = gr.Button("🔄 Index aktualisieren", variant="primary")
285
  clear_index_button = gr.Button("🧹 Index leeren")
286
-
287
  index_status = gr.Markdown("Index ist leer.")
288
-
289
- index_button.click(
290
- fn=index_files,
291
- inputs=file_uploader,
292
- outputs=index_status,
293
- )
294
- clear_index_button.click(
295
- fn=clear_index,
296
- inputs=None,
297
- outputs=index_status,
298
- )
299
 
300
  with gr.Column(scale=2):
 
301
  gr.Markdown("### 💬 Chat über deine Dokumente")
302
- chatbot = gr.Chatbot(
303
- label="Gemma-3 Chat",
304
- type="messages",
305
- show_copy_button=True,
306
- height=600,
307
- )
308
-
309
  with gr.Row():
310
- msg_textbox = gr.Textbox(
311
- label="Deine Frage",
312
- placeholder="Stelle eine Frage zu den Dokumenten...",
313
- scale=4,
314
- autofocus=True,
315
- )
316
  send_btn = gr.Button("Senden", variant="primary", scale=1)
317
 
318
  def chat_interface(message: str, history: list):
319
  if not message or not message.strip():
320
  return history
321
-
322
  history.append({"role": "user", "content": message})
323
  history.append({"role": "assistant", "content": ""})
324
-
325
  for token in answer_with_rag(message, history):
326
  history[-1]["content"] += token
327
  yield history
328
 
329
- msg_textbox.submit(
330
- fn=chat_interface,
331
- inputs=[msg_textbox, chatbot],
332
- outputs=chatbot,
333
- ).then(
334
- fn=lambda: gr.update(value=""),
335
- outputs=msg_textbox
336
- )
337
- send_btn.click(
338
- fn=chat_interface,
339
- inputs=[msg_textbox, chatbot],
340
- outputs=chatbot
341
- ).then(
342
- fn=lambda: gr.update(value=""),
343
- outputs=msg_textbox
344
- )
345
 
346
  return demo
347
 
348
-
349
  if __name__ == "__main__":
 
 
 
 
 
350
  app_demo = build_demo()
351
  app_demo.launch()
 
1
+ # app.py - v1.5
2
+ # Beschreibung: Finale, robuste Version mit umfangreichem Debugging, Assertions
3
+ # und Korrekturen für API-Inkompatibilitäten (Gemma-3 Prozessor, LangChain Embeddings).
 
4
 
5
  import os
6
  import torch
7
  import gradio as gr
8
+ import time # Für Debug-Timings
9
 
10
  from typing import List, Tuple, Generator, Dict
11
  from threading import Thread
12
 
13
  # ML / Transformers
 
 
14
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextStreamer
15
 
16
  # Dokumentenverarbeitung & RAG
 
18
  from langchain_core.documents import Document
19
  from langchain_text_splitters import RecursiveCharacterTextSplitter
20
  from langchain_community.vectorstores import FAISS
21
+ # NEUER, KORREKTER IMPORT für Zukunftssicherheit
22
+ from langchain_huggingface import HuggingFaceEmbeddings
23
 
24
  # --------------------------------------------------------------------
25
+ # Konfiguration & Globale States
26
  # --------------------------------------------------------------------
27
  EMBED_MODEL_ID = "google/embeddinggemma-300m"
28
  LLM_MODEL_ID = "google/gemma-3-4b-it"
29
 
30
+ EMBEDDING_FUNCTION: HuggingFaceEmbeddings = None
 
 
31
  LLM_MODEL: Gemma3ForConditionalGeneration = None
32
  LLM_PROCESSOR: AutoProcessor = None
33
  VECTOR_STORE: FAISS = None
34
 
35
+ # --- DEBUGGING HELPER ---
36
+ def print_debug(message: str):
37
+ """Konsistente Debug-Ausgabe mit Zeitstempel."""
38
+ print(f"[DEBUG {time.strftime('%H:%M:%S')}] {message}")
39
 
40
  # --------------------------------------------------------------------
41
  # Model Loading
 
43
  def get_device() -> torch.device:
44
  """Gibt das verfügbare torch-Device zurück (CUDA, wenn verfügbar)."""
45
  if torch.cuda.is_available():
 
46
  return torch.device("cuda")
 
47
  return torch.device("cpu")
48
 
49
 
50
+ def get_embedding_function() -> HuggingFaceEmbeddings:
51
+ """Lädt das Embedding-Modell über den korrekten LangChain-Wrapper."""
 
 
 
52
  global EMBEDDING_FUNCTION
53
  if EMBEDDING_FUNCTION is None:
54
+ device = get_device()
55
+ print_debug(f"Initialisiere Embedding-Modell '{EMBED_MODEL_ID}' auf Device '{device}'.")
56
+ # Nutzt jetzt die dedizierte, zukunftssichere Klasse.
57
+ EMBEDDING_FUNCTION = HuggingFaceEmbeddings(
58
  model_name=EMBED_MODEL_ID,
59
+ model_kwargs={'device': device}
60
  )
61
+ print_debug("Embedding-Modell erfolgreich initialisiert.")
62
  return EMBEDDING_FUNCTION
63
 
64
 
 
67
  global LLM_MODEL, LLM_PROCESSOR
68
  if LLM_MODEL is None or LLM_PROCESSOR is None:
69
  device = get_device()
70
+ print_debug(f"Initialisiere LLM '{LLM_MODEL_ID}' auf Device '{device}'.")
71
+ dtype = torch.bfloat16 if "cuda" in device.type else torch.float32
72
 
73
  LLM_MODEL = Gemma3ForConditionalGeneration.from_pretrained(
74
  LLM_MODEL_ID,
 
76
  device_map="auto",
77
  ).eval()
78
  LLM_PROCESSOR = AutoProcessor.from_pretrained(LLM_MODEL_ID)
79
+ print_debug("LLM und Prozessor erfolgreich initialisiert.")
80
 
81
  return LLM_MODEL, LLM_PROCESSOR
82
 
 
86
  # --------------------------------------------------------------------
87
  def extract_text_from_file(path: str) -> str:
88
  """Extrahiert Text aus verschiedenen Dateitypen."""
89
+ # ... (Keine Änderungen in dieser Funktion, bleibt wie in v1.4)
90
  ext = os.path.splitext(path)[1].lower()
 
91
  if ext in [".txt", ".md", ".markdown"]:
92
+ with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
 
 
93
  if ext == ".pdf":
94
  text_parts = []
95
  try:
96
  reader = PdfReader(path)
97
  for page in reader.pages:
98
  page_text = page.extract_text()
99
+ if page_text: text_parts.append(page_text)
 
100
  return "\n\n".join(text_parts)
101
  except Exception as e:
102
+ print(f"Fehler beim Lesen von PDF {path}: {e}"); return ""
 
 
103
  try:
104
+ with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
105
+ except Exception: return ""
 
 
 
106
 
107
  def get_text_splitter() -> RecursiveCharacterTextSplitter:
108
  """Erstellt einen semantisch bewussten Text-Splitter."""
109
+ return RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200, length_function=len)
 
 
 
 
 
 
110
 
111
  # --------------------------------------------------------------------
112
  # Indexing / RAG mit FAISS
113
  # --------------------------------------------------------------------
114
  def index_files(file_paths: List[str], progress=gr.Progress(track_tqdm=True)) -> str:
115
+ """Liest Dateien, erstellt Chunks und baut/aktualisiert einen FAISS-Vektorindex."""
 
 
116
  global VECTOR_STORE
117
  if not file_paths:
118
  return "Keine Dateien zum Indexieren ausgewählt."
119
+ print_debug(f"Indexierung gestartet für {len(file_paths)} Datei(en).")
120
 
 
121
  embedding_function = get_embedding_function()
122
  text_splitter = get_text_splitter()
123
 
 
126
  if path is None: continue
127
 
128
  text = extract_text_from_file(path)
129
+ if not text.strip():
130
+ print_debug(f"Datei '{os.path.basename(path)}' enthält keinen extrahierbaren Text.")
131
+ continue
132
 
133
  chunks = text_splitter.split_text(text)
 
 
134
  source_name = os.path.basename(path)
135
  for chunk in chunks:
136
  doc = Document(page_content=chunk, metadata={"source": source_name})
137
  documents.append(doc)
138
 
139
+ # ASSERT: Sicherstellen, dass wir eine Liste von Document-Objekten haben.
140
+ assert all(isinstance(d, Document) for d in documents), "Alle Elemente in 'documents' müssen vom Typ langchain.Document sein."
141
+ print_debug(f"Erfolgreich {len(documents)} Chunks aus den Dateien erstellt.")
142
+
143
  if not documents:
144
  return "Kein Text in den Dateien gefunden, der indexiert werden konnte."
145
 
146
  progress(0.5, desc="2/2: Embeddings erstellen & FAISS Index aufbauen...")
 
 
 
147
  new_store = FAISS.from_documents(documents, embedding_function)
148
+ print_debug("FAISS Index erfolgreich aus Dokumenten erstellt.")
149
 
150
  if VECTOR_STORE is None:
151
  VECTOR_STORE = new_store
152
  else:
153
  VECTOR_STORE.add_documents(documents)
154
+ print_debug("Neuen Index mit bestehendem Index zusammengeführt.")
155
 
156
+ # ASSERT: Sicherstellen, dass der Index nun existiert und Elemente enthält.
157
+ assert VECTOR_STORE is not None and VECTOR_STORE.index.ntotal > 0, "VECTOR_STORE wurde nicht korrekt initialisiert."
158
 
159
+ final_count = VECTOR_STORE.index.ntotal
160
+ print_debug(f"Indexierung abgeschlossen. Gesamtanzahl der Chunks im Index: {final_count}")
161
+ return f"Index aktualisiert: {final_count} Chunks insgesamt."
162
 
163
  def clear_index() -> str:
164
  """Leert den Vektorindex."""
165
  global VECTOR_STORE
166
  VECTOR_STORE = None
167
+ import gc; gc.collect()
168
+ print_debug("Vektor-Index wurde geleert.")
169
  return "Index geleert."
170
 
 
171
  def retrieve_relevant_chunks(query: str, top_k: int = 5) -> List[Dict]:
172
  """Sucht die relevantesten Chunks mit FAISS."""
173
  if VECTOR_STORE is None:
174
+ print_debug("Retrieval versucht, aber Vektor-Index ist leer.")
175
  return []
176
 
177
+ print_debug(f"Suche nach {top_k} relevanten Chunks für die Anfrage: '{query[:80]}...'")
178
  results_with_scores = VECTOR_STORE.similarity_search_with_score(query, k=top_k)
179
 
180
  formatted_results = []
181
  for doc, score in results_with_scores:
182
  formatted_results.append({
183
+ "content": doc.page_content, "source": doc.metadata.get("source", "Unbekannt"), "score": 1 - score
 
 
184
  })
 
185
 
186
+ # ASSERT: Sicherstellen, dass die zurückgegebene Struktur korrekt ist.
187
+ assert isinstance(formatted_results, list), "Retrieval-Ergebnis muss eine Liste sein."
188
+ if formatted_results:
189
+ assert all("content" in r and "source" in r and "score" in r for r in formatted_results), "Jedes Retrieval-Ergebnis muss 'content', 'source' und 'score' enthalten."
190
+
191
+ print_debug(f"{len(formatted_results)} Chunks gefunden.")
192
+ return formatted_results
193
 
194
  # --------------------------------------------------------------------
195
  # LLM-Generierung mit Streaming
196
  # --------------------------------------------------------------------
197
  def build_rag_prompt(user_question: str, retrieved_chunks: List[Dict]) -> str:
198
+ # ... (Keine Änderungen in dieser Funktion, bleibt wie in v1.4)
199
  if not retrieved_chunks:
200
  context_str = "Es wurden keine relevanten Dokumente im Kontext gefunden."
201
  else:
202
  context_parts = []
203
  for i, ch in enumerate(retrieved_chunks, start=1):
204
  context_parts.append(
205
+ f"Dokument [{i}] (Quelle: {ch['source']}, Relevanz: {ch['score']:.3f}):\n\"{ch['content']}\""
 
206
  )
207
  context_str = "\n\n".join(context_parts)
208
+ prompt = (f"Du bist ein präziser, hilfreicher Assistent. Deine Aufgabe ist es, die folgende Benutzerfrage ausschließlich "
209
+ f"basierend auf den unten stehenden Kontext-Dokumenten zu beantworten. "
210
+ f"Wenn die Antwort nicht in den Dokumenten enthalten ist, gib klar an: 'Die Information ist in den bereitgestellten Dokumenten nicht enthalten.' "
211
+ f"Antworte auf Deutsch und fasse die relevanten Informationen zusammen, anstatt die Dokumente wörtlich zu zitieren.\n\n"
212
+ f"--- Kontext-Dokumente ---\n{context_str}\n\n"
213
+ f"--- Benutzerfrage ---\n{user_question}\n\n"
214
+ f"--- Deine Antwort ---\n")
 
 
 
 
 
215
  return prompt
216
 
 
217
  def answer_with_rag(question: str, history: list) -> Generator[str, None, None]:
218
  """Führt RAG durch und generiert eine gestreamte Antwort."""
219
+ print_debug("Starte RAG-Antwort-Generierung.")
220
  model, processor = get_llm()
221
  streamer = TextStreamer(processor, skip_prompt=True, skip_special_tokens=True)
222
 
223
  retrieved = retrieve_relevant_chunks(question, top_k=5)
224
  prompt = build_rag_prompt(question, retrieved)
225
+ print_debug(f"Generierter RAG-Prompt (erste 200 Zeichen): '{prompt[:200].replace(chr(10), ' ')}...'")
226
+
227
+ # KORREKTUR: Gemma-3 erwartet eine Liste von Inhalts-Dictionaries.
228
+ messages = [
229
+ {"role": "user", "content": [{"type": "text", "text": prompt}]}
230
+ ]
231
+
232
+ # ASSERT: Überprüft die korrekte, verschachtelte Struktur vor dem Aufruf.
233
+ print_debug(f"Nachrichten-Struktur wird für Prozessor vorbereitet: {str(messages)[:200]}...")
234
+ assert isinstance(messages, list) and len(messages) > 0, "Messages muss eine nicht-leere Liste sein."
235
+ assert isinstance(messages[0], dict) and "role" in messages[0] and "content" in messages[0], "Nachricht muss ein Dictionary mit 'role' und 'content' sein."
236
+ assert isinstance(messages[0]["content"], list) and len(messages[0]["content"]) > 0, "Content muss eine nicht-leere Liste sein."
237
+ assert isinstance(messages[0]["content"][0], dict) and "type" in messages[0]["content"][0] and "text" in messages[0]["content"][0], "Content-Block muss ein Dictionary mit 'type' und 'text' sein."
238
+ print_debug("ASSERTIONS für Nachrichten-Struktur erfolgreich bestanden.")
239
 
240
  inputs = processor.apply_chat_template(
241
+ messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
 
 
 
242
  ).to(model.device)
243
 
244
+ generation_kwargs = dict(inputs, streamer=streamer, max_new_tokens=1024, do_sample=True, temperature=0.7, top_p=0.9)
 
 
 
 
 
 
 
245
 
246
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
247
  thread.start()
248
+ print_debug("LLM-Generierungs-Thread gestartet.")
249
 
250
  for new_text in streamer:
251
  yield new_text
252
+ print_debug("LLM-Generierung abgeschlossen.")
253
 
254
  # --------------------------------------------------------------------
255
  # Gradio UI
256
  # --------------------------------------------------------------------
257
  def build_demo() -> gr.Blocks:
258
+ with gr.Blocks(title="Gemma 3 RAG v1.5", theme="soft") as demo:
259
  gr.Markdown(
260
  """
261
+ # 🔍 Gemma 3 RAG v1.5Robust & Debug-Fähig
262
  **Eine "State of the Art" RAG-Pipeline mit `google/embeddinggemma-300m` und `google/gemma-3-4b-it`**
263
  1. Lade deine Dokumente hoch und klicke auf "Index aktualisieren".
264
  2. Stelle deine Fragen im Chatfenster. Die Antworten werden live gestreamt.
 
267
 
268
  with gr.Row():
269
  with gr.Column(scale=1):
270
+ # ... (Keine UI-Änderungen in dieser Spalte)
271
  gr.Markdown("### 📁 Dokumenten-Management")
272
+ file_uploader = gr.File(label="Dateien hochladen (.pdf, .txt, .md)", file_count="multiple", type="filepath")
 
 
 
 
273
  with gr.Row():
274
  index_button = gr.Button("🔄 Index aktualisieren", variant="primary")
275
  clear_index_button = gr.Button("🧹 Index leeren")
 
276
  index_status = gr.Markdown("Index ist leer.")
277
+ index_button.click(fn=index_files, inputs=file_uploader, outputs=index_status)
278
+ clear_index_button.click(fn=clear_index, inputs=None, outputs=index_status)
 
 
 
 
 
 
 
 
 
279
 
280
  with gr.Column(scale=2):
281
+ # ... (Keine UI-Änderungen in dieser Spalte)
282
  gr.Markdown("### 💬 Chat über deine Dokumente")
283
+ chatbot = gr.Chatbot(label="Gemma-3 Chat", type="messages", show_copy_button=True, height=600)
 
 
 
 
 
 
284
  with gr.Row():
285
+ msg_textbox = gr.Textbox(label="Deine Frage", placeholder="Stelle eine Frage zu den Dokumenten...", scale=4, autofocus=True)
 
 
 
 
 
286
  send_btn = gr.Button("Senden", variant="primary", scale=1)
287
 
288
  def chat_interface(message: str, history: list):
289
  if not message or not message.strip():
290
  return history
 
291
  history.append({"role": "user", "content": message})
292
  history.append({"role": "assistant", "content": ""})
 
293
  for token in answer_with_rag(message, history):
294
  history[-1]["content"] += token
295
  yield history
296
 
297
+ msg_textbox.submit(fn=chat_interface, inputs=[msg_textbox, chatbot], outputs=chatbot).then(fn=lambda: gr.update(value=""), outputs=msg_textbox)
298
+ send_btn.click(fn=chat_interface, inputs=[msg_textbox, chatbot], outputs=chatbot).then(fn=lambda: gr.update(value=""), outputs=msg_textbox)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
  return demo
301
 
 
302
  if __name__ == "__main__":
303
+ # Stelle sicher, dass die Modelle beim Start geladen werden,
304
+ # um eine Verzögerung bei der ersten Anfrage zu vermeiden.
305
+ get_embedding_function()
306
+ get_llm()
307
+
308
  app_demo = build_demo()
309
  app_demo.launch()
requirements.txt CHANGED
@@ -1,5 +1,5 @@
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
@@ -8,11 +8,13 @@ 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)
 
 
 
1
+ # requirements.txt - v1.5
2
+ # Beschreibung: Zukunftsicher durch dediziertes langchain-huggingface Paket.
3
 
4
  # Core ML/UI Frameworks
5
  gradio>=4.44.0
 
8
  accelerate>=0.33.0
9
  bitsandbytes>=0.43.0
10
 
11
+ # SentenceTransformers (indirekt über LangChain genutzt)
12
  sentence-transformers>=5.0.0
13
 
14
  # Dokumentenverarbeitung und Vektor-Datenbank
15
  pypdf>=5.0.0
16
+ langchain-core>=0.2.0
17
+ langchain-text-splitters>=0.2.0
18
+ langchain-community>=0.2.0
19
+ langchain-huggingface>=0.0.3 # <-- NEU: Dediziertes Paket für HF-Integration
20
+ faiss-cpu>=1.8.0