""" 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
…) 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