| | |
| |
|
| | from typing import List, Dict, Any, Tuple |
| | from langchain_core.messages import SystemMessage, HumanMessage |
| |
|
| | MAX_CHARS = 900 |
| |
|
| |
|
| | def build_sources_metadata(docs: List) -> List[Dict[str, Any]]: |
| | 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 src == "Prüfungsordnung (PDF)": |
| | pdf_url = meta["pdf_url"] |
| | if isinstance(page, int) and pdf_url: |
| | url = f"{pdf_url}#page={page + 1}" |
| | else: |
| | url = pdf_url |
| |
|
| | elif src == "Hochschulgesetz NRW": |
| | url = meta["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 gefunden.)" |
| |
|
| | out_lines = [] |
| | for i, d in enumerate(docs): |
| | txt = d.page_content[:MAX_CHARS] |
| | src = d.metadata.get("source") |
| | page = d.metadata.get("page") |
| |
|
| | if src == "Prüfungsordnung (PDF)" and isinstance(page, int): |
| | src_str = f"{src}, Seite {page + 1}" |
| | else: |
| | src_str = src |
| |
|
| | out_lines.append(f"[KONTEXT {i+1}] ({src_str})\n{txt}") |
| |
|
| | return "\n\n".join(out_lines) |
| |
|
| |
|
| | SYSTEM_PROMPT = """ |
| | Du bist ein juristisch präziser Chatbot für Prüfungsrecht. |
| | Du nutzt ausschließlich: |
| | |
| | - die Prüfungsordnung (PDF) und |
| | - das Hochschulgesetz NRW (Absätze aus der Datenbank) |
| | |
| | Regeln: |
| | |
| | 1. Keine Halluzinationen – nur Inhalte aus dem gelieferten Kontext. |
| | 2. Wenn der Kontext unklar ist, sage ausdrücklich, dass keine sichere |
| | Aussage möglich ist. |
| | 3. Antworte immer in gut verständlichem, ganzen Sätzen. |
| | 4. Nenne, soweit im Kontext erkennbar: |
| | - Paragraphen oder Überschriften, |
| | - das Dokument (Prüfungsordnung / Hochschulgesetz NRW), |
| | - Seitenzahl (bei der Prüfungsordnung). |
| | """ |
| |
|
| |
|
| | def answer(question: str, retriever, chat_model) -> Tuple[str, List[Dict[str, Any]]]: |
| | docs = retriever.invoke(question) |
| | context_str = format_context(docs) |
| |
|
| | human = f""" |
| | FRAGE: |
| | {question} |
| | |
| | NUTZE AUSSCHLIESSLICH DIESEN KONTEXT: |
| | {context_str} |
| | |
| | AUFGABE: |
| | Erstelle eine juristisch korrekte Antwort ausschließlich auf Basis |
| | des obigen Kontextes. Wenn der Kontext keine sichere Antwort zulässt, |
| | sage das ausdrücklich und verzichte auf Spekulationen. |
| | """ |
| |
|
| | 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 |
| |
|