| """ |
| 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 |
|
|
| |
| |
| |
|
|
| |
| PDF_BASE_URL = f"https://huggingface.co/datasets/{DATASET}/resolve/main/{PDF_FILE}" |
|
|
| |
| LAW_DATASET_URL = f"https://huggingface.co/datasets/{DATASET}/resolve/main/{HTML_FILE}" |
|
|
| |
| 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 |
|
|
| |
| |
| |
|
|
| 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", " ") |
|
|
| |
| if "Prüfungsordnung" in src: |
| if isinstance(page, int): |
| |
| url = f"{PDF_BASE_URL}#page={page + 1}" |
| else: |
| url = PDF_BASE_URL |
|
|
| |
| elif "Hochschulgesetz" in src: |
| para_id = meta.get("paragraph_id") |
| if para_id: |
| |
| url = f"{LAW_DATASET_URL}#{para_id}" |
| else: |
| |
| url = LAW_URL |
| page = None |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| 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) |
|
|
| |
| |
| |
|
|
| 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. |
| """ |
|
|
| |
| |
| |
|
|
| 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. |
| """ |
| |
| docs = retriever.invoke(question) |
| context_str = format_context(docs) |
|
|
| |
| 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), |
| ] |
|
|
| |
| result = chat_model.invoke(msgs) |
| answer_text = result.content.strip() |
|
|
| |
| sources = build_sources_metadata(docs) |
|
|
| return answer_text, sources |
|
|