chatbot1 / rag_pipeline.py
Nguyen5's picture
commit
ed2050a
"""
RAG PIPELINE – Version 26.11 (ohne Modi, stabil, juristisch korrekt)
"""
from typing import List, Dict, Any, Tuple
from langchain_core.messages import SystemMessage, HumanMessage
from load_documents import DATASET, PDF_FILE, HTML_FILE
# -------------------------------------------------------------------
# URLs für Quellen
# -------------------------------------------------------------------
# Direktes PDF im Dataset (für #page)
PDF_BASE_URL = f"https://huggingface.co/datasets/{DATASET}/resolve/main/{PDF_FILE}"
# Hochschulgesetz-HTML im Dataset (enthält <p id="hg_abs_X"> …)
LAW_DATASET_URL = f"https://huggingface.co/datasets/{DATASET}/resolve/main/{HTML_FILE}"
# Offizielle Recht.NRW-Druckversion (für Viewer im Frontend)
LAW_URL = (
"https://recht.nrw.de/lmi/owa/br_bes_text?"
"print=1&anw_nr=2&gld_nr=2&ugl_nr=221&val=28364&ver=0&"
"aufgehoben=N&keyword=&bes_id=28364&show_preview=1"
)
MAX_CHARS = 900
# -----------------------------
# Quellen formatieren
# -----------------------------
def build_sources_metadata(docs: List) -> List[Dict[str, Any]]:
"""
Erzeugt eine Liste strukturierter Quellen-Infos:
[
{
"id": 1,
"source": "Prüfungsordnung (PDF)" / "Hochschulgesetz NRW (HTML)",
"page": 3, # nur bei PDF
"url": "...", # direkter Klick-Link
"snippet": "Erste 300 Zeichen des Chunks..."
},
...
]
"""
srcs = []
for i, d in enumerate(docs):
meta = d.metadata
src = meta.get("source", "")
page = meta.get("page")
snippet = d.page_content[:300].replace("\n", " ")
# PDF-Link
if "Prüfungsordnung" in src:
if isinstance(page, int):
# PyPDFLoader: page ist 0-basiert, Anzeige 1-basiert
url = f"{PDF_BASE_URL}#page={page + 1}"
else:
url = PDF_BASE_URL
# NRW-Gesetz (HTML im Dataset mit Absatz-IDs)
elif "Hochschulgesetz" in src:
para_id = meta.get("paragraph_id")
if para_id:
# Klick führt direkt zum Absatz im Dataset-HTML
url = f"{LAW_DATASET_URL}#{para_id}"
else:
# Fallback: offizielle Druckversion (ohne Absatz-Anker)
url = LAW_URL
page = None # keine Seitenangabe für Gesetz-HTML
else:
url = None
srcs.append(
{
"id": i + 1,
"source": src,
"page": page + 1 if isinstance(page, int) else None,
"url": url,
"snippet": snippet,
}
)
return srcs
# -----------------------------
# Kontext formatieren
# -----------------------------
def format_context(docs):
if not docs:
return "(Kein relevanter Kontext im Dokument gefunden.)"
out = []
for i, d in enumerate(docs):
txt = d.page_content[:MAX_CHARS]
src = d.metadata.get("source")
page = d.metadata.get("page")
if "Prüfungsordnung" in (src or "") and isinstance(page, int):
src_str = f"{src}, Seite {page + 1}"
else:
src_str = src
out.append(f"[KONTEXT {i+1}] ({src_str})\n{txt}")
return "\n\n".join(out)
# -----------------------------
# Systemprompt — verschärft
# -----------------------------
SYSTEM_PROMPT = """
Du bist ein hochpräziser juristischer Chatbot für Prüfungsrecht
mit Zugriff nur auf:
- die Prüfungsordnung (als PDF) und
- das Hochschulgesetz NRW (als HTML aus der offiziellen Druckversion).
Strenge Regeln:
1. Antworte ausschließlich anhand des bereitgestellten Kontextes
(KONTEXT-Abschnitte). Wenn die Information nicht im Kontext steht,
sage ausdrücklich, dass dies aus den vorliegenden Dokumenten nicht
hervorgeht und du dazu nichts Sicheres sagen kannst.
2.
Keine Spekulationen, keine Vermutungen.
3. Antworte in zusammenhängenden, ganzen Sätzen. Verwende keine Mischung aus Deutsch und Englisch.
4. Nenne, soweit aus dem Kontext erkennbar,
- die rechtliche Grundlage (z.B. Paragraph, Artikel),
- das Dokument (Prüfungsordnung / Hochschulgesetz NRW),
- die Seite (bei der Prüfungsordnung), wenn im Kontext vorhanden.
5. Füge KEINE externen Informationen hinzu, z.B. aus anderen Gesetzen,
Webseiten oder allgemeinem Wissen. Nur das, was im Kontext steht,
darf in der Antwort verwendet werden.
Wenn der Kontext keine eindeutige Antwort zulässt, erkläre klar,
warum keine sichere Antwort möglich ist und welche Informationen
im Dokument fehlen.
"""
# -----------------------------
# Hauptfunktion
# -----------------------------
def answer(question: str, retriever, chat_model) -> Tuple[str, List[Dict[str, Any]]]:
"""
Haupt-RAG-Funktion:
- ruft retriever.invoke(question) auf,
- baut einen präzisen Prompt mit KONTEXT,
- ruft LLM auf,
- gibt Antworttext + Quellenliste zurück.
"""
# 1. Dokumente holen
docs = retriever.invoke(question)
context_str = format_context(docs)
# 2. Prompt bauen
human = f"""
FRAGE:
{question}
NUTZE AUSSCHLIESSLICH DIESEN KONTEXT:
{context_str}
AUFGABE:
Formuliere eine juristisch korrekte, gut verständliche Antwort
ausschließlich anhand des obigen Kontextes.
- Wenn der Kontext aus den Dokumenten eine klare Antwort erlaubt,
erläutere diese strukturiert und in vollständigen Sätzen.
- Wenn der Kontext KEINE klare Antwort erlaubt oder wichtige Informationen
fehlen, erkläre das offen und formuliere KEINE Vermutung.
"""
msgs = [
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(content=human),
]
# 3. LLM aufrufen
result = chat_model.invoke(msgs)
answer_text = result.content.strip()
# 4. Quellenliste bauen
sources = build_sources_metadata(docs)
return answer_text, sources