Nguyen5 commited on
Commit
9fd5591
·
1 Parent(s): 94cd817
Files changed (2) hide show
  1. load_documents.py +135 -91
  2. rag_pipeline.py +122 -53
load_documents.py CHANGED
@@ -1,116 +1,160 @@
1
- # load_documents.py – Supabase + statischer HTML-Viewer
 
 
 
2
 
3
- import os
4
- import requests
5
- import tempfile
6
- from supabase import create_client
7
- from langchain_core.documents import Document
 
8
  from langchain_community.document_loaders import PyPDFLoader
 
 
9
 
10
- # ---------------------------------------------------------
11
- # ENV Variablen aus HuggingFace Space
12
- # ---------------------------------------------------------
13
- SUPABASE_URL = os.getenv("SUPABASE_URL")
14
- SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
15
 
16
- if not SUPABASE_URL or not SUPABASE_ANON_KEY:
17
- raise RuntimeError("Missing SUPABASE_URL / SUPABASE_ANON_KEY in environment.")
 
 
 
 
 
 
 
 
 
 
18
 
19
- supabase = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
 
20
 
21
- # ---------------------------------------------------------
22
- # Prüfungsordnung PDF liegt in Supabase Storage (public)
23
- # ---------------------------------------------------------
24
- PDF_FILE = "f10_bpo_ifb_tei_mif_wii_2021-01-04.pdf"
25
- PDF_URL = f"{SUPABASE_URL}/storage/v1/object/public/File%20PDF/{PDF_FILE}"
26
 
27
- # ---------------------------------------------------------
28
- # Statischer Paragraph-Viewer in HuggingFace Space
29
- # -> hg_clean.html liegt als Datei im Repo!
30
- # -> in der App: iframe src="file=hg_clean.html"
31
- # -> für Links: "file=hg_clean.html#para_123"
32
- # ---------------------------------------------------------
33
- HG_HTML_URL = "file=hg_clean.html" # WICHTIG: nicht absolut, Space kümmert sich
34
 
35
- def load_hg_nrw():
36
- """
37
- Holt alle Paragraphen aus Tabelle public.hg_nrw und baut
38
- LangChain-Dokumente. Jeder Paragraph:
39
- - title : z.B. "§ 64 (Fn 35) Prüfungsordnungen"
40
- - content: Volltext inkl. Fußnoten
41
- - abs_id : para_1, para_2, ...
42
- """
43
- print(">>> Lade Hochschulgesetz NRW (§) aus Supabase…")
44
 
45
- rows = (
46
- supabase.table("hg_nrw")
47
- .select("*")
48
- .order("order_index")
49
- .execute()
50
- ).data or []
51
 
52
- print(f" - {len(rows)} Paragraphen geladen.")
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  docs = []
55
- for r in rows:
56
- abs_id = r["abs_id"] # z.B. "para_64"
57
- title = r.get("title", "")
58
- content = r.get("content", "")
59
-
60
- # Anker im Viewer – IDs in hg_clean.html sind identisch (id="para_64")
61
- viewer_url = f"{HG_HTML_URL}#{abs_id}"
62
-
63
- docs.append(
64
- Document(
65
- page_content=f"{title}\n{content}",
66
- metadata={
67
- "source": "Hochschulgesetz NRW",
68
- "paragraph": title,
69
- "abs_id": abs_id,
70
- "url": viewer_url,
71
- },
72
- )
73
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
 
 
 
75
  return docs
76
 
77
- def load_pdf():
78
- """
79
- Lädt Prüfungsordnung aus Supabase Storage, speichert temporär,
80
- splitten erfolgt später in split_documents.py.
81
- """
82
- print(">>> Lade Prüfungsordnung PDF …")
83
 
84
- resp = requests.get(PDF_URL)
85
- resp.raise_for_status()
 
86
 
87
- with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
88
- tmp.write(resp.content)
89
- path = tmp.name
90
 
91
- pages = PyPDFLoader(path).load()
 
92
 
93
- for i, p in enumerate(pages):
94
- p.metadata["source"] = "Prüfungsordnung (PDF)"
95
- p.metadata["page"] = i # 0-basiert
96
- p.metadata["pdf_url"] = PDF_URL
97
 
98
- print(f" - {len(pages)} PDF-Seiten geladen.")
99
- return pages
100
 
101
- def load_documents():
102
- """
103
- Master-Funktion:
104
- - Hochschulgesetz NRW (Supabase Tabelle hg_nrw)
105
- - Prüfungsordnung (PDF)
106
- """
107
- docs = []
108
- docs.extend(load_hg_nrw())
109
- docs.extend(load_pdf())
110
- print(f" DOCUMENTS LOADED: {len(docs)}")
111
- return docs
 
 
112
 
113
  if __name__ == "__main__":
 
114
  docs = load_documents()
115
- print(docs[0])
116
- print("Total:", len(docs))
 
 
1
+ """
2
+ BƯỚC 1: LOAD DOCUMENTS
3
+ -----------------------
4
+ Debug-full version
5
 
6
+ - Lädt Prüfungsordnung (PDF) seitenweise.
7
+ - Lädt Hochschulgesetz NRW aus dem im Dataset gespeicherten HTML,
8
+ und zerlegt es in einzelne Absätze (Document pro <p>).
9
+ """
10
+
11
+ from huggingface_hub import hf_hub_download, list_repo_files
12
  from langchain_community.document_loaders import PyPDFLoader
13
+ from langchain_core.documents import Document
14
+ from bs4 import BeautifulSoup
15
 
16
+ DATASET = "Nguyen5/docs"
17
+ PDF_FILE = "f10_bpo_ifb_tei_mif_wii_2021-01-04.pdf"
18
+ HTML_FILE = "Hochschulgesetz_NRW.html" # konsistent mit hg_nrw.py
 
 
19
 
20
+ def _load_hg_paragraph_documents(html_path: str):
21
+ """
22
+ Liest das generierte Hochschulgesetz-HTML ein und erzeugt
23
+ pro <p>-Element einen LangChain-Document mit:
24
+ - page_content = Text des Absatzes
25
+ - metadata:
26
+ source = "Hochschulgesetz NRW (HTML)"
27
+ filename = HTML_FILE
28
+ paragraph_id = id-Attribut (z.B. 'hg_abs_12'), falls vorhanden
29
+ """
30
+ with open(html_path, "r", encoding="utf-8") as f:
31
+ html = f.read()
32
 
33
+ soup = BeautifulSoup(html, "html.parser")
34
+ docs = []
35
 
36
+ for p in soup.find_all("p"):
37
+ text = p.get_text(" ", strip=True)
38
+ if not text:
39
+ continue
 
40
 
41
+ pid = p.get("id")
 
 
 
 
 
 
42
 
43
+ metadata = {
44
+ "source": "Hochschulgesetz NRW (HTML)",
45
+ "filename": HTML_FILE,
46
+ }
47
+ if pid:
48
+ metadata["paragraph_id"] = pid
 
 
 
49
 
50
+ docs.append(Document(page_content=text, metadata=metadata))
 
 
 
 
 
51
 
52
+ print(f"Loaded {len(docs)} paragraph Documents from HG-HTML.\n")
53
+ return docs
54
+
55
+ def load_documents():
56
+ print("=== START: load_documents() ===\n")
57
+
58
+ # -------------------------
59
+ # Check files in dataset
60
+ # -------------------------
61
+ print(">>> Checking dataset file list from HuggingFace...")
62
+ files = list_repo_files(DATASET, repo_type="dataset")
63
+ print("Files in dataset:", files, "\n")
64
 
65
  docs = []
66
+
67
+ # -------------------------
68
+ # Load PDF
69
+ # -------------------------
70
+ print(">>> Step 1: Download PDF from HuggingFace...")
71
+ try:
72
+ pdf_path = hf_hub_download(
73
+ repo_id=DATASET,
74
+ filename=PDF_FILE,
75
+ repo_type="dataset",
 
 
 
 
 
 
 
 
76
  )
77
+ print(f"Downloaded PDF to local cache:\n{pdf_path}\n")
78
+ except Exception as e:
79
+ print("ERROR downloading PDF:", e)
80
+ return []
81
+
82
+ print(">>> Step 1.1: Loading PDF pages...")
83
+ try:
84
+ pdf_docs = PyPDFLoader(pdf_path).load()
85
+ print(f"Loaded {len(pdf_docs)} PDF pages.\n")
86
+ except Exception as e:
87
+ print("ERROR loading PDF:", e)
88
+ return []
89
+
90
+ for d in pdf_docs:
91
+ d.metadata["source"] = "Prüfungsordnung (PDF)"
92
+ d.metadata["filename"] = PDF_FILE
93
+
94
+ docs.extend(pdf_docs)
95
+
96
+ # -------------------------
97
+ # Load HTML (Hochschulgesetz NRW)
98
+ # -------------------------
99
+ print(">>> Step 2: Download HTML from HuggingFace...")
100
+ try:
101
+ html_path = hf_hub_download(
102
+ repo_id=DATASET,
103
+ filename=HTML_FILE,
104
+ repo_type="dataset",
105
+ )
106
+ print(f"Downloaded HTML to local cache:\n{html_path}\n")
107
+ except Exception as e:
108
+ print("ERROR downloading HTML:", e)
109
+ return docs
110
+
111
+ print(">>> Step 2.1: Loading HG-HTML and splitting into paragraphs...")
112
+ try:
113
+ html_docs = _load_hg_paragraph_documents(html_path)
114
+ except Exception as e:
115
+ print("ERROR loading / parsing HTML:", e)
116
+ return docs
117
 
118
+ docs.extend(html_docs)
119
+
120
+ print("=== DONE: load_documents() ===\n")
121
  return docs
122
 
123
+ if __name__ == "__main__":
124
+ print("\n=== Running load_documents.py directly ===\n")
125
+ docs = load_documents()
126
+ print(f"\n>>> TOTAL documents loaded: {len(docs)}")
 
 
127
 
128
+ if len(docs):
129
+ print("\nExample metadata from 1st document:")
130
+ print(docs[0].metadata)
131
 
 
 
 
132
 
133
+ - split_documents.py:
134
+ # split_documents.py – v2
135
 
136
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
 
 
 
137
 
138
+ CHUNK_SIZE = 1500
139
+ CHUNK_OVERLAP = 200
140
 
141
+ def split_documents(docs):
142
+ splitter = RecursiveCharacterTextSplitter(
143
+ chunk_size=CHUNK_SIZE,
144
+ chunk_overlap=CHUNK_OVERLAP,
145
+ separators=["\n\n", "\n", ". ", " ", ""],
146
+ )
147
+ chunks = splitter.split_documents(docs)
148
+
149
+ for c in chunks:
150
+ c.metadata["chunk_size"] = CHUNK_SIZE
151
+ c.metadata["chunk_overlap"] = CHUNK_OVERLAP
152
+
153
+ return chunks
154
 
155
  if __name__ == "__main__":
156
+ from load_documents import load_documents
157
  docs = load_documents()
158
+ chunks = split_documents(docs)
159
+ print("Docs:", len(docs), "Chunks:", len(chunks))
160
+ print(chunks[0].page_content[:300], chunks[0].metadata)
rag_pipeline.py CHANGED
@@ -1,98 +1,163 @@
1
- # rag_pipeline.py – OpenAI RAG mit Supabase-Dokumenten
 
 
2
 
3
  from typing import List, Dict, Any, Tuple
4
  from langchain_core.messages import SystemMessage, HumanMessage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  MAX_CHARS = 900
7
 
8
- # ------------------------------------------------------
9
- # Quellen-Metadaten
10
- # ------------------------------------------------------
11
 
12
  def build_sources_metadata(docs: List) -> List[Dict[str, Any]]:
13
  """
14
- Gibt für jeden verwendeten Chunk eine strukturierte Quelle zurück:
15
- - id
16
- - source (Dokument)
17
- - page (bei PDF)
18
- - url (PDF-Seite oder hg_clean.html#para_x)
19
- - snippet (Kurzvorschau)
 
 
 
 
 
 
20
  """
21
  srcs = []
22
-
23
  for i, d in enumerate(docs):
24
  meta = d.metadata
25
- src = meta.get("source")
26
  page = meta.get("page")
27
  snippet = d.page_content[:300].replace("\n", " ")
28
 
29
- if src == "Prüfungsordnung (PDF)":
30
- pdf_url = meta.get("pdf_url")
31
- if isinstance(page, int) and pdf_url:
32
- url = f"{pdf_url}#page={page + 1}"
 
33
  else:
34
- url = pdf_url
35
-
36
- elif src == "Hochschulgesetz NRW":
37
- url = meta.get("url")
38
- page = None
 
 
 
 
 
 
 
39
 
40
  else:
41
  url = None
42
 
43
- srcs.append({
44
- "id": i + 1,
45
- "source": src,
46
- "page": page + 1 if isinstance(page, int) else None,
47
- "url": url,
48
- "snippet": snippet,
49
- })
50
-
 
51
  return srcs
52
 
53
- # ------------------------------------------------------
54
- # Kontextformatierung
55
- # ------------------------------------------------------
56
 
57
  def format_context(docs):
58
  if not docs:
59
- return "(Kein relevanter Kontext gefunden.)"
60
 
61
- out_lines = []
62
  for i, d in enumerate(docs):
63
  txt = d.page_content[:MAX_CHARS]
64
  src = d.metadata.get("source")
65
  page = d.metadata.get("page")
66
 
67
- if src == "Prüfungsordnung (PDF)" and isinstance(page, int):
68
  src_str = f"{src}, Seite {page + 1}"
69
  else:
70
  src_str = src
71
 
72
- out_lines.append(f"[KONTEXT {i+1}] ({src_str})\n{txt}")
73
 
74
- return "\n\n".join(out_lines)
 
 
 
 
75
 
76
  SYSTEM_PROMPT = """
77
- Du bist ein juristisch präziser Chatbot für Prüfungsrecht.
78
- Du nutzt ausschließlich:
 
 
 
 
 
79
 
80
- - die Prüfungsordnung (PDF) und
81
- - das Hochschulgesetz NRW (Paragraphen aus der Datenbank / hg_clean.html)
 
 
82
 
83
- Regeln:
 
84
 
85
- 1. Antworte nur auf Basis des gelieferten Kontextes.
86
- 2. Wenn der Kontext keine sichere Antwort erlaubt, sage das klar.
87
- 3. Antworte in gut verständlichem Deutsch, in ganzen Sätzen.
88
- 4. Nenne, soweit möglich:
89
- - Paragraphen oder Überschriften,
90
  - das Dokument (Prüfungsordnung / Hochschulgesetz NRW),
91
- - Seitenzahl (bei der Prüfungsordnung).
 
 
 
 
 
 
 
 
92
  """
93
 
 
 
 
 
94
  def answer(question: str, retriever, chat_model) -> Tuple[str, List[Dict[str, Any]]]:
95
- # 1. Chunks holen
 
 
 
 
 
 
 
 
96
  docs = retriever.invoke(question)
97
  context_str = format_context(docs)
98
 
@@ -105,9 +170,13 @@ NUTZE AUSSCHLIESSLICH DIESEN KONTEXT:
105
  {context_str}
106
 
107
  AUFGABE:
108
- Erstelle eine juristisch korrekte Antwort ausschließlich auf Basis
109
- des obigen Kontextes. Wenn der Kontext keine sichere Antwort zulässt,
110
- sage das ausdrücklich und verzichte auf Spekulationen.
 
 
 
 
111
  """
112
 
113
  msgs = [
@@ -119,7 +188,7 @@ sage das ausdrücklich und verzichte auf Spekulationen.
119
  result = chat_model.invoke(msgs)
120
  answer_text = result.content.strip()
121
 
122
- # 4. Quellenliste
123
  sources = build_sources_metadata(docs)
124
 
125
  return answer_text, sources
 
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&"
23
+ "aufgehoben=N&keyword=&bes_id=28364&show_preview=1"
24
+ )
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:
93
+ return "(Kein relevanter Kontext im Dokument gefunden.)"
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
 
101
+ if "Prüfungsordnung" in (src or "") and isinstance(page, int):
102
  src_str = f"{src}, Seite {page + 1}"
103
  else:
104
  src_str = src
105
 
106
+ out.append(f"[KONTEXT {i+1}] ({src_str})\n{txt}")
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
 
 
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 = [
 
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