neuralworm commited on
Commit
24766ca
·
1 Parent(s): dac3615

add openai data export compatibility

Browse files
Files changed (2) hide show
  1. app.py +62 -68
  2. requirements.txt +0 -1
app.py CHANGED
@@ -1,14 +1,14 @@
1
- # app.py - v2.5 (Professional JSON Parsing)
2
- # Beschreibung: Ersetzt den fehleranfälligen manuellen Parser durch die robuste,
3
- # stream-basierte `ijson`-Bibliothek. Dies ist der korrekte,
4
- # pythonische Ansatz, um große oder fehlerhafte JSON-Dateien zu verarbeiten.
 
5
 
6
  import os
7
  import torch
8
  import gradio as gr
9
  import time
10
- import json
11
- import ijson # <-- DAS RICHTIGE WERKZEUG
12
 
13
  from typing import List, Tuple, Generator, Dict
14
  from threading import Thread
@@ -72,70 +72,38 @@ def get_llm() -> Tuple[Gemma3ForConditionalGeneration, AutoProcessor]:
72
  # --------------------------------------------------------------------
73
  # Datei-Handling & Chunking
74
  # --------------------------------------------------------------------
75
-
76
- def _professional_extract_text_from_chatgpt_json(file_path: str) -> str:
77
- """
78
- Verwendet `ijson`, um eine JSON-Datei iterativ zu parsen. Dies ist extrem robust
79
- gegenüber Dateigröße und strukturellen Fehlern wie einer fehlenden schließenden Klammer.
80
- """
81
- print_debug(f"Starte professionelles Parsen (ijson) der Datei: {file_path}")
82
- all_conversations_text = []
83
 
84
  try:
85
- with open(file_path, 'rb') as f: # ijson arbeitet am besten mit Binärdateien
86
- # 'item' ist der Präfix für jedes Element in der Top-Level-Liste
87
- parser = ijson.items(f, 'item')
88
- for i, conversation in enumerate(parser):
89
- try:
90
- conversation_parts = []
91
- title = conversation.get('title', f'Unbenannte Konversation {i+1}')
92
- mapping = conversation.get('mapping')
93
-
94
- if not mapping or not isinstance(mapping, dict): continue
95
-
96
- for node_id, node in mapping.items():
97
- message = node.get('message')
98
- if not message: continue
99
- try:
100
- role = message['author']['role']
101
- content_node = message.get('content', {})
102
- parts = content_node.get('parts', [])
103
-
104
- if role in ['user', 'assistant'] and parts and isinstance(parts, list) and parts[0]:
105
- text_content = ""
106
- if isinstance(parts[0], str):
107
- text_content = parts[0]
108
- elif isinstance(parts[0], dict) and 'text' in parts[0]:
109
- text_content = parts[0].get('text', "")
110
-
111
- if text_content and text_content.strip():
112
- conversation_parts.append(f"{role.capitalize()}: {text_content.strip()}")
113
- except (KeyError, TypeError, IndexError):
114
- continue
115
-
116
- if conversation_parts:
117
- full_conversation = "\n\n".join(conversation_parts)
118
- all_conversations_text.append(f"--- Konversation Start: {title} ---\n\n{full_conversation}\n\n--- Konversation Ende: {title} ---")
119
- print_debug(f"Konversation '{title}' erfolgreich via ijson extrahiert.")
120
-
121
- except Exception as e:
122
- print_debug(f"Unerwarteter Fehler bei der Verarbeitung einer Konversation von ijson. Überspringe. Fehler: {e}")
123
- continue
124
- except ijson.JSONError as e:
125
- print_debug(f"Schwerwiegender JSON-Fehler, ijson konnte nicht parsen: {e}")
126
- # Selbst wenn ein schwerer Fehler auftritt, haben wir vielleicht schon Konversationen gesammelt
127
  except Exception as e:
128
- print_debug(f"Konnte Datei nicht lesen oder verarbeiten: {e}")
 
 
 
 
 
 
 
 
 
 
 
129
  return ""
130
 
131
- final_text = "\n\n\n".join(all_conversations_text)
132
- print_debug(f"Gesamter Text aus JSON extrahiert ({len(final_text)} Zeichen aus {len(all_conversations_text)} Konversationen).")
 
 
 
133
  return final_text
134
 
135
  def extract_text_from_file(path: str) -> str:
136
  ext = os.path.splitext(path)[1].lower()
137
  if ext == ".json":
138
- return _professional_extract_text_from_chatgpt_json(path)
139
  if ext in [".txt", ".md", ".markdown"]:
140
  with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
141
  if ext == ".pdf":
@@ -152,9 +120,14 @@ def extract_text_from_file(path: str) -> str:
152
  with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
153
  except Exception: return ""
154
 
155
- # ... [Rest des Codes für Indexing, RAG und UI bleibt identisch] ...
156
  def get_text_splitter() -> RecursiveCharacterTextSplitter:
157
- return RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200, length_function=len)
 
 
 
 
 
 
158
 
159
  def index_files(file_paths: List[str], progress=gr.Progress(track_tqdm=True)) -> str:
160
  global VECTOR_STORE
@@ -195,12 +168,23 @@ def clear_index() -> str:
195
  return "Index geleert."
196
 
197
  def retrieve_relevant_chunks(query: str, top_k: int = 5) -> List[Dict]:
198
- if VECTOR_STORE is None: return []
 
 
 
 
199
  results_with_scores = VECTOR_STORE.similarity_search_with_score(query, k=top_k)
200
- return [{
 
201
  "content": doc.page_content, "source": doc.metadata.get("source", "Unbekannt"), "score": 1 - score
202
  } for doc, score in results_with_scores]
203
 
 
 
 
 
 
 
204
  def build_rag_prompt(user_question: str, retrieved_chunks: List[Dict]) -> str:
205
  if not retrieved_chunks: context_str = "Es wurden keine relevanten Dokumente im Kontext gefunden."
206
  else:
@@ -218,30 +202,41 @@ def build_rag_prompt(user_question: str, retrieved_chunks: List[Dict]) -> str:
218
  return prompt
219
 
220
  def answer_with_rag(question: str, history: list) -> Generator[str, None, None]:
 
221
  model, processor = get_llm()
222
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
 
223
  retrieved = retrieve_relevant_chunks(question, top_k=5)
224
  prompt = build_rag_prompt(question, retrieved)
 
 
 
225
  messages = [{"role": "user", "content": [{"type": "text", "text": prompt}]}]
226
  input_ids = processor.apply_chat_template(
227
  messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
228
  ).to(model.device)
 
229
  generation_kwargs = {
230
  "input_ids": input_ids, "streamer": streamer, "max_new_tokens": 1024,
231
  "do_sample": True, "temperature": 0.7, "top_p": 0.9,
232
  }
233
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
234
  thread.start()
 
 
235
  for new_text in streamer:
 
236
  yield new_text
237
 
 
 
238
  def build_demo() -> gr.Blocks:
239
- with gr.Blocks(title="Gemma 3 RAG v2.5", theme="soft") as demo:
240
  gr.Markdown(
241
  """
242
- # 🔍 Gemma 3 RAG v2.5Professional JSON Parsing
243
  **Analysiere deine (auch unvollständigen) ChatGPT `conversations.json` oder andere Dokumente.**
244
- Verwendet die `ijson`-Bibliothek für maximale Robustheit.
245
  """
246
  )
247
  with gr.Row():
@@ -260,7 +255,6 @@ def build_demo() -> gr.Blocks:
260
  sample_path = "sample.json"
261
  if os.path.exists(sample_path):
262
  print_debug(f"'{sample_path}' gefunden. Starte Indexierung via Button.")
263
- # Zeige dem User, dass etwas passiert
264
  yield "Indexiere `sample.json`..."
265
  status = index_files([sample_path], progress=gr.Progress(track_tqdm=True))
266
  yield status
 
1
+ # app.py - v3.2 (The Final Production Version)
2
+ # Beschreibung: Die finale, korrigierte und produktionsreife Version.
3
+ # Behebt den kritischen Chunking-Fehler, indem auf eine natürliche
4
+ # Textformatierung und die Standard-Splitting-Strategie zurückgegriffen wird.
5
+ # Behält die volle Debug-Ausgabe für Transparenz.
6
 
7
  import os
8
  import torch
9
  import gradio as gr
10
  import time
11
+ import re
 
12
 
13
  from typing import List, Tuple, Generator, Dict
14
  from threading import Thread
 
72
  # --------------------------------------------------------------------
73
  # Datei-Handling & Chunking
74
  # --------------------------------------------------------------------
75
+ def _regex_based_extractor(file_path: str) -> str:
76
+ print_debug(f"Starte Regex-basierten Parser (v3.2) für: {file_path}")
 
 
 
 
 
 
77
 
78
  try:
79
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
80
+ content = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  except Exception as e:
82
+ print_debug(f"Konnte Datei nicht lesen: {e}")
83
+ return ""
84
+
85
+ pattern = re.compile(
86
+ r'"role":\s*"(user|assistant)".*?"parts":\s*\[\s*"(.*?)"',
87
+ re.DOTALL
88
+ )
89
+
90
+ matches = pattern.findall(content)
91
+
92
+ if not matches:
93
+ print_debug("Keine passenden Textteile über Regex gefunden.")
94
  return ""
95
 
96
+ # KORREKTUR: Wir erstellen einen natürlich formatierten Text.
97
+ full_text_parts = [f"{role.capitalize()}: {text.strip()}" for role, text in matches if text.strip()]
98
+ final_text = "\n\n".join(full_text_parts) # <-- EINFACHER, ROBUSTER SEPARATOR
99
+
100
+ print_debug(f"Gesamter Text aus JSON via Regex extrahiert ({len(final_text)} Zeichen aus {len(matches)} Treffern).")
101
  return final_text
102
 
103
  def extract_text_from_file(path: str) -> str:
104
  ext = os.path.splitext(path)[1].lower()
105
  if ext == ".json":
106
+ return _regex_based_extractor(path)
107
  if ext in [".txt", ".md", ".markdown"]:
108
  with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
109
  if ext == ".pdf":
 
120
  with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
121
  except Exception: return ""
122
 
 
123
  def get_text_splitter() -> RecursiveCharacterTextSplitter:
124
+ # KORREKTUR: Wir kehren zu den Standard-Separatoren zurück, die für
125
+ # natürlich formatierten Text optimiert sind.
126
+ return RecursiveCharacterTextSplitter(
127
+ chunk_size=1000,
128
+ chunk_overlap=200,
129
+ length_function=len
130
+ )
131
 
132
  def index_files(file_paths: List[str], progress=gr.Progress(track_tqdm=True)) -> str:
133
  global VECTOR_STORE
 
168
  return "Index geleert."
169
 
170
  def retrieve_relevant_chunks(query: str, top_k: int = 5) -> List[Dict]:
171
+ if VECTOR_STORE is None:
172
+ print_debug("Retrieval versucht, aber Vektor-Index ist leer.")
173
+ return []
174
+
175
+ print_debug(f"Suche nach {top_k} relevanten Chunks für die Anfrage: '{query}'")
176
  results_with_scores = VECTOR_STORE.similarity_search_with_score(query, k=top_k)
177
+
178
+ formatted_results = [{
179
  "content": doc.page_content, "source": doc.metadata.get("source", "Unbekannt"), "score": 1 - score
180
  } for doc, score in results_with_scores]
181
 
182
+ print_debug(f"{len(formatted_results)} Chunks gefunden. Details:")
183
+ for i, chunk in enumerate(formatted_results):
184
+ print_debug(f" [Chunk {i+1}] Score: {chunk['score']:.4f}, Source: {chunk['source']}\n Content: '{chunk['content'][:200].replace(chr(10), ' ')}...'")
185
+
186
+ return formatted_results
187
+
188
  def build_rag_prompt(user_question: str, retrieved_chunks: List[Dict]) -> str:
189
  if not retrieved_chunks: context_str = "Es wurden keine relevanten Dokumente im Kontext gefunden."
190
  else:
 
202
  return prompt
203
 
204
  def answer_with_rag(question: str, history: list) -> Generator[str, None, None]:
205
+ print_debug(f"--- RAG Pipeline Start für Frage: '{question}' ---")
206
  model, processor = get_llm()
207
  streamer = TextIteratorStreamer(processor, skip_prompt=True, skip_special_tokens=True)
208
+
209
  retrieved = retrieve_relevant_chunks(question, top_k=5)
210
  prompt = build_rag_prompt(question, retrieved)
211
+
212
+ print_debug(f"--- Vollständiger RAG-Prompt, der an das LLM gesendet wird ---\n{prompt}\n--- Ende des RAG-Prompts ---")
213
+
214
  messages = [{"role": "user", "content": [{"type": "text", "text": prompt}]}]
215
  input_ids = processor.apply_chat_template(
216
  messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
217
  ).to(model.device)
218
+
219
  generation_kwargs = {
220
  "input_ids": input_ids, "streamer": streamer, "max_new_tokens": 1024,
221
  "do_sample": True, "temperature": 0.7, "top_p": 0.9,
222
  }
223
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
224
  thread.start()
225
+
226
+ full_response = ""
227
  for new_text in streamer:
228
+ full_response += new_text
229
  yield new_text
230
 
231
+ print_debug(f"--- Vollständige Antwort vom LLM (gestreamt) ---\n{full_response}\n--- Ende der LLM-Antwort ---")
232
+
233
  def build_demo() -> gr.Blocks:
234
+ with gr.Blocks(title="Gemma 3 RAG v3.2", theme="soft") as demo:
235
  gr.Markdown(
236
  """
237
+ # 🔍 Gemma 3 RAG v3.2The Final Production Version
238
  **Analysiere deine (auch unvollständigen) ChatGPT `conversations.json` oder andere Dokumente.**
239
+ Verwendet einen robusten Regex-Extraktor und korrektes Chunking für maximale Zuverlässigkeit.
240
  """
241
  )
242
  with gr.Row():
 
255
  sample_path = "sample.json"
256
  if os.path.exists(sample_path):
257
  print_debug(f"'{sample_path}' gefunden. Starte Indexierung via Button.")
 
258
  yield "Indexiere `sample.json`..."
259
  status = index_files([sample_path], progress=gr.Progress(track_tqdm=True))
260
  yield status
requirements.txt CHANGED
@@ -18,4 +18,3 @@ 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
21
- ijson>=3.2.3
 
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