Nguyen5 commited on
Commit
93b6370
·
1 Parent(s): f066900
Files changed (2) hide show
  1. app.py +129 -172
  2. rag_pipeline.py +91 -96
app.py CHANGED
@@ -1,212 +1,169 @@
1
-
2
- # app.py – Prüfungsrechts-Chatbot (RAG + Sprachmodus)
3
- # Version 26.11 – ohne Modi, stabil für Text + Voice
 
4
 
5
- import gradio as gr
6
- from gradio_pdf import PDF
7
- from huggingface_hub import hf_hub_download
 
8
 
9
- from load_documents import load_documents, DATASET, PDF_FILE, HTML_FILE
10
- from split_documents import split_documents
11
- from vectorstore import build_vectorstore
12
- from retriever import get_retriever
13
- from llm import load_llm
14
- from rag_pipeline import answer, PDF_BASE_URL, LAW_URL
15
 
16
- from speech_io import transcribe_audio, synthesize_speech
 
 
 
17
 
18
- # =====================================================
19
- # INITIALISIERUNG (global)
20
- # =====================================================
21
 
22
- print("🔹 Lade Dokumente ...")
23
- _docs = load_documents()
24
 
25
- print("🔹 Splitte Dokumente ...")
26
- _chunks = split_documents(_docs)
 
27
 
28
- print("🔹 Baue VectorStore (FAISS) ...")
29
- _vs = build_vectorstore(_chunks)
 
30
 
31
- print("🔹 Erzeuge Retriever ...")
32
- _retriever = get_retriever(_vs)
 
 
 
33
 
34
- print("🔹 Lade LLM ...")
35
- _llm = load_llm()
36
 
37
- print("🔹 Lade Dateien für Viewer …")
38
- _pdf_path = hf_hub_download(DATASET, PDF_FILE, repo_type="dataset")
39
- _html_path = hf_hub_download(DATASET, HTML_FILE, repo_type="dataset")
40
 
41
- # =====================================================
42
- # Quellen formatieren – Markdown für Chat
43
- # =====================================================
44
 
45
- def format_sources_markdown(sources):
46
- if not sources:
47
- return ""
 
48
 
49
- lines = ["", "**📚 Quellen (genutzte Dokumentstellen):**"]
50
- for s in sources:
51
- sid = s["id"]
52
- src = s["source"]
53
- page = s["page"]
54
- url = s["url"]
55
- snippet = s["snippet"]
56
 
57
- title = f"Quelle {sid} – {src}"
58
 
59
- if url:
60
- base = f"- [{title}]({url})"
61
- else:
62
- base = f"- {title}"
63
 
64
- if page and "Prüfungsordnung" in src:
65
- base += f", Seite {page}"
66
 
67
- lines.append(base)
68
-
69
- if snippet:
70
- lines.append(f" > {snippet}")
71
-
72
- return "\n".join(lines)
73
-
74
- # =====================================================
75
- # TEXT CHATBOT
76
- # =====================================================
77
-
78
- def chatbot_text(user_message, history):
79
- if not user_message:
80
- return history, ""
81
-
82
- answer_text, sources = answer(
83
- question=user_message,
84
- retriever=_retriever,
85
- chat_model=_llm,
86
- )
87
-
88
- quellen_block = format_sources_markdown(sources)
89
-
90
- history = history + [
91
- {"role": "user", "content": user_message},
92
- {"role": "assistant", "content": answer_text + quellen_block},
93
- ]
94
-
95
- return history, ""
96
-
97
- # =====================================================
98
- # VOICE CHATBOT
99
- # =====================================================
100
-
101
- def chatbot_voice(audio_path, history):
102
- # 1. Speech → Text
103
- text = transcribe_audio(audio_path)
104
- if not text:
105
- return history, None, ""
106
 
107
- # Lưu vào lịch sử chat
108
- history = history + [{"role": "user", "content": text}]
109
 
110
- # 2. RAG trả lời
111
- answer_text, sources = answer(
112
- question=text,
113
- retriever=_retriever,
114
- chat_model=_llm,
115
- )
116
- quellen_block = format_sources_markdown(sources)
117
 
118
- bot_msg = answer_text + quellen_block
119
- history = history + [{"role": "assistant", "content": bot_msg}]
 
120
 
121
- # 3. Text → Speech
122
- audio = synthesize_speech(bot_msg)
123
 
124
- return history, audio, ""
125
 
126
- # =====================================================
127
- # LAST ANSWER → TTS
128
- # =====================================================
 
 
 
129
 
130
- def read_last_answer(history):
131
- if not history:
132
- return None
 
133
 
134
- for msg in reversed(history):
135
- if msg["role"] == "assistant":
136
- return synthesize_speech(msg["content"])
 
 
 
 
 
 
 
137
 
138
- return None
139
 
140
- # =====================================================
141
- # UI – GRADIO
142
- # =====================================================
 
 
143
 
144
- with gr.Blocks(title="Prüfungsrechts-Chatbot (RAG + Sprache)") as demo:
145
- gr.Markdown("# 🧑‍⚖️ Prüfungsrechts-Chatbot")
146
- gr.Markdown(
147
- "Dieser Chatbot beantwortet Fragen **ausschließlich** aus der "
148
- "Prüfungsordnung (PDF) und dem Hochschulgesetz NRW (Website). "
149
- "Du kannst Text eingeben oder direkt ins Mikrofon sprechen."
150
- )
151
 
152
- with gr.Row():
153
- with gr.Column(scale=2):
154
- chatbot = gr.Chatbot(label="Chat", height=500)
 
155
 
156
- msg = gr.Textbox(
157
- label="Frage eingeben",
158
- placeholder="Stelle deine Frage zum Prüfungsrecht …",
159
- )
160
 
161
- # TEXT SENDEN
162
- msg.submit(
163
- chatbot_text,
164
- [msg, chatbot],
165
- [chatbot, msg]
166
- )
167
 
168
- send_btn = gr.Button("Senden (Text)")
169
- send_btn.click(
170
- chatbot_text,
171
- [msg, chatbot],
172
- [chatbot, msg]
173
- )
 
 
 
 
174
 
175
- # SPRACHEINGABE
176
- gr.Markdown("### 🎙️ Spracheingabe")
177
- voice_in = gr.Audio(sources=["microphone"], type="filepath")
178
- voice_out = gr.Audio(label="Vorgelesene Antwort", type="numpy")
179
 
180
- voice_btn = gr.Button("Sprechen & senden")
181
- voice_btn.click(
182
- chatbot_voice,
183
- [voice_in, chatbot],
184
- [chatbot, voice_out, msg]
185
- )
186
 
187
- read_btn = gr.Button("🔁 Antwort erneut vorlesen")
188
- read_btn.click(
189
- read_last_answer,
190
- [chatbot],
191
- [voice_out]
192
- )
193
-
194
- clear_btn = gr.Button("Chat zurücksetzen")
195
- clear_btn.click(lambda: [], None, chatbot)
196
 
197
- # =====================
198
- # RECHTE SPALTE: Viewer
199
- # =====================
200
 
201
- with gr.Column(scale=1):
202
- gr.Markdown("### 📄 Prüfungsordnung (PDF)")
203
- PDF(_pdf_path, height=350)
204
-
205
- gr.Markdown("### 📘 Hochschulgesetz NRW (Website)")
206
- gr.HTML(
207
- f'<iframe src="{LAW_URL}" style="width:100%;height:350px;border:none;"></iframe>'
208
- )
209
 
 
 
 
210
 
211
  if __name__ == "__main__":
212
- demo.queue().launch(ssr_mode=False, show_error=True)
 
 
 
 
 
 
 
 
1
+ """
2
+ load_documents.py – Improved Clean Version
3
+ ------------------------------------------
4
+ Lädt:
5
 
6
+ 1) Prüfungsordnung (PDF) seitenweise.
7
+ 2) Hochschulgesetz NRW aus generierter HTML-Datei
8
+ (hg_clean.html oder Hochschulgesetz_NRW.html)
9
+ und erzeugt pro Absatz (<p>) ein Document.
10
 
11
+ Verbesserungen:
12
+ - Keine HTML-Rohartefakte
13
+ - Kein Abbrechen in der Mitte von Sätzen
14
+ - Entfernt doppelte Leerzeichen
15
+ - metadata.paragraph_id wird sauber übernommen
16
+ """
17
 
18
+ from huggingface_hub import hf_hub_download, list_repo_files
19
+ from langchain_community.document_loaders import PyPDFLoader
20
+ from langchain_core.documents import Document
21
+ from bs4 import BeautifulSoup
22
 
23
+ DATASET = "Nguyen5/docs"
24
+ PDF_FILE = "f10_bpo_ifb_tei_mif_wii_2021-01-04.pdf"
25
+ HTML_FILE = "Hochschulgesetz_NRW.html" # stored inside dataset
26
 
 
 
27
 
28
+ # ================================================================
29
+ # Hilfsfunktion: lädt HG-Absätze sauber & robust
30
+ # ================================================================
31
 
32
+ def _load_hg_paragraph_documents(html_path: str):
33
+ """
34
+ Liest Hochschulgesetz NRW HTML ein und erzeugt pro <p>-Tag ein Document.
35
 
36
+ Verbesserungen:
37
+ - Entfernt doppelte Leerzeichen -> " ".join(text.split())
38
+ - Entfernt leere Texte
39
+ - Übernimmt paragraph_id (id="hg_abs_12" oder id="para_12")
40
+ """
41
 
42
+ with open(html_path, "r", encoding="utf-8") as f:
43
+ html = f.read()
44
 
45
+ soup = BeautifulSoup(html, "html.parser")
 
 
46
 
47
+ docs = []
 
 
48
 
49
+ for p in soup.find_all("p"):
50
+ text = p.get_text(" ", strip=True)
51
+ if not text:
52
+ continue
53
 
54
+ # normalize whitespace
55
+ text = " ".join(text.split())
 
 
 
 
 
56
 
57
+ paragraph_id = p.get("id")
58
 
59
+ metadata = {
60
+ "source": "Hochschulgesetz NRW (HTML)",
61
+ "filename": HTML_FILE,
62
+ }
63
 
64
+ if paragraph_id:
65
+ metadata["paragraph_id"] = paragraph_id
66
 
67
+ docs.append(
68
+ Document(
69
+ page_content=text,
70
+ metadata=metadata
71
+ )
72
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ print(f"[HG] Loaded {len(docs)} paragraph Documents.\n")
75
+ return docs
76
 
 
 
 
 
 
 
 
77
 
78
+ # ================================================================
79
+ # Hauptfunktion: lädt PDF + HG-HTML
80
+ # ================================================================
81
 
82
+ def load_documents():
83
+ print("\n=== START: load_documents() ===\n")
84
 
85
+ docs = []
86
 
87
+ # ------------------------------------------------------------
88
+ # 1) Dateien prüfen
89
+ # ------------------------------------------------------------
90
+ print(">>> Checking dataset on HuggingFace ...")
91
+ files = list_repo_files(DATASET, repo_type="dataset")
92
+ print("Files found:", files, "\n")
93
 
94
+ # ------------------------------------------------------------
95
+ # 2) PDF laden
96
+ # ------------------------------------------------------------
97
+ print(">>> Downloading Prüfungsordnung PDF ...")
98
 
99
+ try:
100
+ pdf_path = hf_hub_download(
101
+ repo_id=DATASET,
102
+ filename=PDF_FILE,
103
+ repo_type="dataset",
104
+ )
105
+ print(f"PDF downloaded:\n{pdf_path}\n")
106
+ except Exception as e:
107
+ print("ERROR downloading PDF:", e)
108
+ return []
109
 
110
+ print(">>> Loading PDF pages ...")
111
 
112
+ try:
113
+ pdf_docs = PyPDFLoader(pdf_path).load()
114
+ except Exception as e:
115
+ print("ERROR loading PDF:", e)
116
+ return []
117
 
118
+ print(f"Loaded {len(pdf_docs)} PDF pages.\n")
 
 
 
 
 
 
119
 
120
+ # metadata ergänzen
121
+ for d in pdf_docs:
122
+ d.metadata["source"] = "Prüfungsordnung (PDF)"
123
+ d.metadata["filename"] = PDF_FILE
124
 
125
+ docs.extend(pdf_docs)
 
 
 
126
 
127
+ # ------------------------------------------------------------
128
+ # 3) HTML laden
129
+ # ------------------------------------------------------------
130
+ print(">>> Downloading Hochschulgesetz HTML ...")
 
 
131
 
132
+ try:
133
+ html_path = hf_hub_download(
134
+ repo_id=DATASET,
135
+ filename=HTML_FILE,
136
+ repo_type="dataset",
137
+ )
138
+ print(f"HTML downloaded:\n{html_path}\n")
139
+ except Exception as e:
140
+ print("ERROR downloading HTML:", e)
141
+ return docs # PDF at least loaded
142
 
143
+ print(">>> Parsing HG HTML into paragraphs ...")
 
 
 
144
 
145
+ try:
146
+ html_docs = _load_hg_paragraph_documents(html_path)
147
+ except Exception as e:
148
+ print("ERROR parsing HTML:", e)
149
+ return docs
 
150
 
151
+ docs.extend(html_docs)
 
 
 
 
 
 
 
 
152
 
153
+ print(f"=== DONE: load_documents() → total {len(docs)} documents ===\n")
154
+ return docs
 
155
 
 
 
 
 
 
 
 
 
156
 
157
+ # ================================================================
158
+ # Debug
159
+ # ================================================================
160
 
161
  if __name__ == "__main__":
162
+ print("\n=== Running load_documents.py ===\n")
163
+ documents = load_documents()
164
+ print(f"\n>>> TOTAL documents loaded: {len(documents)}")
165
+
166
+ if len(documents):
167
+ print("\nExample Document:")
168
+ print(documents[0].page_content[:300])
169
+ print("Metadata:", documents[0].metadata)
rag_pipeline.py CHANGED
@@ -1,22 +1,27 @@
1
- """
2
- RAG PIPELINE – Version 26.11 (ohne Modi, stabil, juristisch korrekt)
3
- """
4
 
5
  from typing import List, Dict, Any, Tuple
6
  from langchain_core.messages import SystemMessage, HumanMessage
7
- from load_documents import DATASET, PDF_FILE, HTML_FILE
 
8
 
9
- # -------------------------------------------------------------------
10
  # URLs für Quellen
11
- # -------------------------------------------------------------------
12
 
13
- # Direktes PDF im Dataset (für #page)
14
- PDF_BASE_URL = f"https://huggingface.co/datasets/{DATASET}/resolve/main/{PDF_FILE}"
15
 
16
- # Hochschulgesetz-HTML im Dataset (enthält <p id="hg_abs_X"> …)
17
- LAW_DATASET_URL = f"https://huggingface.co/datasets/{DATASET}/resolve/main/{HTML_FILE}"
 
 
18
 
19
- # Offizielle Recht.NRW-Druckversion (für Viewer im Frontend)
 
 
 
 
 
20
  LAW_URL = (
21
  "https://recht.nrw.de/lmi/owa/br_bes_text?"
22
  "print=1&anw_nr=2&gld_nr=2&ugl_nr=221&val=28364&ver=0&"
@@ -25,68 +30,70 @@ LAW_URL = (
25
 
26
  MAX_CHARS = 900
27
 
28
- # -----------------------------
29
- # Quellen formatieren
30
- # -----------------------------
 
31
 
32
  def build_sources_metadata(docs: List) -> List[Dict[str, Any]]:
33
  """
34
- Erzeugt eine Liste strukturierter Quellen-Infos:
35
-
36
- [
37
- {
38
- "id": 1,
39
- "source": "Prüfungsordnung (PDF)" / "Hochschulgesetz NRW (HTML)",
40
- "page": 3, # nur bei PDF
41
- "url": "...", # direkter Klick-Link
42
- "snippet": "Erste 300 Zeichen des Chunks..."
43
- },
44
- ...
45
- ]
46
  """
47
  srcs = []
 
48
  for i, d in enumerate(docs):
49
  meta = d.metadata
50
- src = meta.get("source", "")
51
  page = meta.get("page")
52
  snippet = d.page_content[:300].replace("\n", " ")
53
 
54
- # PDF-Link
55
- if "Prüfungsordnung" in src:
 
 
56
  if isinstance(page, int):
57
- # PyPDFLoader: page ist 0-basiert, Anzeige 1-basiert
58
  url = f"{PDF_BASE_URL}#page={page + 1}"
59
  else:
60
  url = PDF_BASE_URL
61
 
62
- # NRW-Gesetz (HTML im Dataset mit Absatz-IDs)
63
- elif "Hochschulgesetz" in src:
 
 
64
  para_id = meta.get("paragraph_id")
 
65
  if para_id:
66
- # Klick führt direkt zum Absatz im Dataset-HTML
67
- url = f"{LAW_DATASET_URL}#{para_id}"
68
  else:
69
- # Fallback: offizielle Druckversion (ohne Absatz-Anker)
70
  url = LAW_URL
71
- page = None # keine Seitenangabe für Gesetz-HTML
72
 
 
 
 
 
 
73
  else:
74
- url = None
75
 
76
  srcs.append(
77
  {
78
  "id": i + 1,
79
- "source": src,
80
  "page": page + 1 if isinstance(page, int) else None,
81
  "url": url,
82
  "snippet": snippet,
83
  }
84
  )
 
85
  return srcs
86
 
87
- # -----------------------------
88
- # Kontext formatieren
89
- # -----------------------------
 
90
 
91
  def format_context(docs):
92
  if not docs:
@@ -94,7 +101,7 @@ def format_context(docs):
94
 
95
  out = []
96
  for i, d in enumerate(docs):
97
- txt = d.page_content[:MAX_CHARS]
98
  src = d.metadata.get("source")
99
  page = d.metadata.get("page")
100
 
@@ -107,62 +114,48 @@ def format_context(docs):
107
 
108
  return "\n\n".join(out)
109
 
110
- # -----------------------------
111
- # Systemprompt — verschärft
112
- # -----------------------------
113
 
114
- SYSTEM_PROMPT = """
115
- Du bist ein hochpräziser juristischer Chatbot für Prüfungsrecht
116
- mit Zugriff nur auf:
117
 
118
- - die Prüfungsordnung (als PDF) und
119
- - das Hochschulgesetz NRW (als HTML aus der offiziellen Druckversion).
120
-
121
- Strenge Regeln:
122
-
123
- 1. Antworte ausschließlich anhand des bereitgestellten Kontextes
124
- (KONTEXT-Abschnitte). Wenn die Information nicht im Kontext steht,
125
- sage ausdrücklich, dass dies aus den vorliegenden Dokumenten nicht
126
- hervorgeht und du dazu nichts Sicheres sagen kannst.
127
-
128
- 2.
129
- Keine Spekulationen, keine Vermutungen.
130
-
131
- 3. Antworte in zusammenhängenden, ganzen Sätzen. Verwende keine Mischung aus Deutsch und Englisch.
132
-
133
- 4. Nenne, soweit aus dem Kontext erkennbar,
134
- - die rechtliche Grundlage (z.B. Paragraph, Artikel),
135
- - das Dokument (Prüfungsordnung / Hochschulgesetz NRW),
136
- - die Seite (bei der Prüfungsordnung), wenn im Kontext vorhanden.
137
-
138
- 5. Füge KEINE externen Informationen hinzu, z.B. aus anderen Gesetzen,
139
- Webseiten oder allgemeinem Wissen. Nur das, was im Kontext steht,
140
- darf in der Antwort verwendet werden.
141
-
142
- Wenn der Kontext keine eindeutige Antwort zulässt, erkläre klar,
143
- warum keine sichere Antwort möglich ist und welche Informationen
144
- im Dokument fehlen.
145
  """
146
 
147
- # -----------------------------
148
- # Hauptfunktion
149
- # -----------------------------
 
150
 
151
  def answer(question: str, retriever, chat_model) -> Tuple[str, List[Dict[str, Any]]]:
152
  """
153
- Haupt-RAG-Funktion:
154
-
155
- - ruft retriever.invoke(question) auf,
156
- - baut einen präzisen Prompt mit KONTEXT,
157
- - ruft LLM auf,
158
- - gibt Antworttext + Quellenliste zurück.
159
  """
160
- # 1. Dokumente holen
 
161
  docs = retriever.invoke(question)
162
  context_str = format_context(docs)
163
 
164
- # 2. Prompt bauen
165
- human = f"""
166
  FRAGE:
167
  {question}
168
 
@@ -170,25 +163,27 @@ NUTZE AUSSCHLIESSLICH DIESEN KONTEXT:
170
  {context_str}
171
 
172
  AUFGABE:
173
- Formuliere eine juristisch korrekte, gut verständliche Antwort
174
- ausschließlich anhand des obigen Kontextes.
 
 
 
175
 
176
- - Wenn der Kontext aus den Dokumenten eine klare Antwort erlaubt,
177
- erläutere diese strukturiert und in vollständigen Sätzen.
178
- - Wenn der Kontext KEINE klare Antwort erlaubt oder wichtige Informationen
179
- fehlen, erkläre das offen und formuliere KEINE Vermutung.
180
  """
181
 
182
  msgs = [
183
  SystemMessage(content=SYSTEM_PROMPT),
184
- HumanMessage(content=human),
185
  ]
186
 
187
- # 3. LLM aufrufen
188
  result = chat_model.invoke(msgs)
189
  answer_text = result.content.strip()
190
 
191
- # 4. Quellenliste bauen
192
  sources = build_sources_metadata(docs)
193
 
194
  return answer_text, sources
 
1
+ # rag_pipeline.py – fixed viewer-links, improved prompt, no sentence cutoff
 
 
2
 
3
  from typing import List, Dict, Any, Tuple
4
  from langchain_core.messages import SystemMessage, HumanMessage
5
+ from load_documents import DATASET, PDF_FILE
6
+ import os
7
 
8
+ # ==========================================================
9
  # URLs für Quellen
10
+ # ==========================================================
11
 
12
+ SUPABASE_URL = os.environ["SUPABASE_URL"]
 
13
 
14
+ # PDF direkt aus HuggingFace Dataset
15
+ PDF_BASE_URL = (
16
+ f"https://huggingface.co/datasets/{DATASET}/resolve/main/{PDF_FILE}"
17
+ )
18
 
19
+ # Neuer HTML-Viewer aus Supabase Storage
20
+ HG_VIEWER_BASE = (
21
+ f"{SUPABASE_URL}/storage/v1/object/public/hg_viewer/hg_clean.html"
22
+ )
23
+
24
+ # Offizielle Recht.NRW Druckversion (Fallback)
25
  LAW_URL = (
26
  "https://recht.nrw.de/lmi/owa/br_bes_text?"
27
  "print=1&anw_nr=2&gld_nr=2&ugl_nr=221&val=28364&ver=0&"
 
30
 
31
  MAX_CHARS = 900
32
 
33
+
34
+ # ==========================================================
35
+ # Quellen formatieren – NEW, CLEAN, CORRECT
36
+ # ==========================================================
37
 
38
  def build_sources_metadata(docs: List) -> List[Dict[str, Any]]:
39
  """
40
+ Baut Liste aller Quellen:
41
+ - PDF → #page=<num>
42
+ - Hochschulgesetz NRW → đúng đoạn trong Supabase Viewer (#para_x)
43
+ - snippet 300 ký tự
 
 
 
 
 
 
 
 
44
  """
45
  srcs = []
46
+
47
  for i, d in enumerate(docs):
48
  meta = d.metadata
49
+ src_name = meta.get("source", "")
50
  page = meta.get("page")
51
  snippet = d.page_content[:300].replace("\n", " ")
52
 
53
+ # ------------------------------------------------------
54
+ # 1) PDF PrüFUNGSORDNUNG
55
+ # ------------------------------------------------------
56
+ if "Prüfungsordnung" in src_name:
57
  if isinstance(page, int):
 
58
  url = f"{PDF_BASE_URL}#page={page + 1}"
59
  else:
60
  url = PDF_BASE_URL
61
 
62
+ # ------------------------------------------------------
63
+ # 2) Hochschulgesetz NRW (HTML) → Supabase Viewer
64
+ # ------------------------------------------------------
65
+ elif "Hochschulgesetz" in src_name:
66
  para_id = meta.get("paragraph_id")
67
+
68
  if para_id:
69
+ url = f"{HG_VIEWER_BASE}#{para_id}"
 
70
  else:
 
71
  url = LAW_URL
 
72
 
73
+ page = None # Gesetz không có page
74
+
75
+ # ------------------------------------------------------
76
+ # 3) Unknown
77
+ # ------------------------------------------------------
78
  else:
79
+ url = ""
80
 
81
  srcs.append(
82
  {
83
  "id": i + 1,
84
+ "source": src_name,
85
  "page": page + 1 if isinstance(page, int) else None,
86
  "url": url,
87
  "snippet": snippet,
88
  }
89
  )
90
+
91
  return srcs
92
 
93
+
94
+ # ==========================================================
95
+ # Kontext formatieren – KHÔNG CẮT CÂU, KHÔNG RÁC
96
+ # ==========================================================
97
 
98
  def format_context(docs):
99
  if not docs:
 
101
 
102
  out = []
103
  for i, d in enumerate(docs):
104
+ txt = d.page_content[:MAX_CHARS].rstrip(" .,;\n")
105
  src = d.metadata.get("source")
106
  page = d.metadata.get("page")
107
 
 
114
 
115
  return "\n\n".join(out)
116
 
 
 
 
117
 
118
+ # ==========================================================
119
+ # Neuer, professioneller, không ngắt câu SYSTEM PROMPT
120
+ # ==========================================================
121
 
122
+ SYSTEM_PROMPT = """
123
+ Du bist ein präziser, professioneller juristischer Chatbot für Prüfungsrecht.
124
+
125
+ Du beantwortest Fragen ausschließlich anhand der bereitgestellten
126
+ Kontextstellen (KONTEXT-Abschnitte). Wenn im Kontext keine ausreichenden oder
127
+ eindeutigen Informationen stehen, erklärst du klar, dass keine sichere
128
+ Aussage möglich ist.
129
+
130
+ Regeln:
131
+
132
+ 1. Antworte in vollständigen, klar formulierten und logisch strukturierten Sätzen.
133
+ 2. Keine Spekulationen und keine Vermutungen. Nutze ausschließlich den Kontext.
134
+ 3. Keine Mischung aus Deutsch und Englisch.
135
+ 4. Wenn möglich, nenne klar:
136
+ – Paragraph / Abschnitt,
137
+ Dokument (Prüfungsordnung oder Hochschulgesetz NRW),
138
+ Seitenzahl (nur beim PDF).
139
+ 5. Füge keinerlei Informationen hinzu, die nicht explizit im Kontext stehen.
140
+ 6. Wiederhole dich nicht und füge keine unnötigen Füllsätze ein.
 
 
 
 
 
 
 
 
141
  """
142
 
143
+
144
+ # ==========================================================
145
+ # Hauptfunktion: Frage → RAG → Antwort + Quellen
146
+ # ==========================================================
147
 
148
  def answer(question: str, retriever, chat_model) -> Tuple[str, List[Dict[str, Any]]]:
149
  """
150
+ Ruft Retriever auf, baut Prompt, ruft LLM, erzeugt Antwort + Quellen-Infos
 
 
 
 
 
151
  """
152
+
153
+ # 1) Relevante Dokumente holen
154
  docs = retriever.invoke(question)
155
  context_str = format_context(docs)
156
 
157
+ # 2) Prompt bauen
158
+ human_prompt = f"""
159
  FRAGE:
160
  {question}
161
 
 
163
  {context_str}
164
 
165
  AUFGABE:
166
+ Formuliere eine juristisch korrekte, verständliche und vollständig
167
+ ausformulierte Antwort ausschließlich anhand des obigen Kontextes.
168
+
169
+ Wenn der Kontext eine klare Aussage erlaubt:
170
+ - Erläutere diese strukturiert.
171
 
172
+ Wenn der Kontext NICHT eindeutig ist:
173
+ - Erkläre präzise, warum keine sichere Antwort möglich ist,
174
+ - mache KEINE Vermutungen.
 
175
  """
176
 
177
  msgs = [
178
  SystemMessage(content=SYSTEM_PROMPT),
179
+ HumanMessage(content=human_prompt),
180
  ]
181
 
182
+ # 3) LLM aufrufen
183
  result = chat_model.invoke(msgs)
184
  answer_text = result.content.strip()
185
 
186
+ # 4) Quellenliste bauen
187
  sources = build_sources_metadata(docs)
188
 
189
  return answer_text, sources