# app.py – Prüfungsrechts-Chatbot (RAG + Sprache, UI kiểu ChatGPT) import gradio as gr from gradio_pdf import PDF from load_documents import load_all_documents from split_documents import split_documents from vectorstore import build_vectorstore from retriever import get_retriever from llm import load_llm from rag_pipeline import answer from speech_io import transcribe_audio, synthesize_speech # ===================================================== # INITIALISIERUNG (global) # ===================================================== print("📚 Lade Dokumente…") docs = load_all_documents() print("🔪 Splitte Dokumente…") chunks = split_documents(docs) print("🔍 Erstelle VectorStore…") vs = build_vectorstore(chunks) print("🔎 Erzeuge Retriever…") retriever = get_retriever(vs) print("🤖 Lade LLM…") llm = load_llm() # ===================================================== # Quellen formatieren – Markdown für Chat # ===================================================== def format_sources(src): if not src: return "" out = ["", "## 📚 Quellen"] for s in src: line = f"- [{s['source']}]({s['url']})" if s.get("page") is not None: line += f" (Seite {s['page']})" out.append(line) return "\n".join(out) # ===================================================== # CORE CHAT-FUNKTION (MultimodalTextbox: Text + Audio) # ===================================================== def chat_fn(message, history): """ message: dict {"text": str, "files": [...]} von gr.MultimodalTextbox history: Liste von OpenAI-ähnlichen Messages (role, content) """ # 1) Text + evtl. Audio aus message holen if isinstance(message, dict): text = (message.get("text") or "").strip() files = message.get("files") or [] else: text = str(message or "").strip() files = [] # Audio-Datei (vom Mikrofon) herausziehen audio_path = None for f in files: # gr.MultimodalTextbox liefert i.d.R. Dict mit "path" if isinstance(f, dict): path = f.get("path") else: path = f if isinstance(path, str) and path: audio_path = path break # Wenn Audio vorhanden: transkribieren if audio_path: spoken = transcribe_audio(audio_path) if text: text = (text + " " + spoken).strip() else: text = spoken if not text: # Nichts zu tun return history, None, {"text": "", "files": []} # 2) RAG-Antwort berechnen ans, sources = answer(text, retriever, llm) bot_msg = ans + format_sources(sources) # 3) History aktualisieren (ChatGPT-Style) history = history + [ {"role": "user", "content": text}, {"role": "assistant", "content": bot_msg}, ] # 4) TTS für Antwort tts_audio = synthesize_speech(bot_msg) # 5) Input-Feld leeren cleared_input = {"text": "", "files": []} return history, tts_audio, cleared_input # ===================================================== # LAST ANSWER → TTS (für Button "Antwort erneut vorlesen") # ===================================================== def read_last_answer(history): if not history: return None for msg in reversed(history): if msg.get("role") == "assistant": return synthesize_speech(msg.get("content", "")) return None # ===================================================== # UI – GRADIO # ===================================================== with gr.Blocks(title="Prüfungsrechts-Chatbot (RAG + Sprache)") as demo: gr.Markdown("# 🧑‍⚖️ Prüfungsrechts-Chatbot") gr.Markdown( "Dieser Chatbot beantwortet Fragen **ausschließlich** aus der " "Prüfungsordnung (PDF) und dem Hochschulgesetz NRW. " "Du kannst Text eingeben oder direkt ins Mikrofon sprechen." ) with gr.Row(): # ===================== LINKER TEIL: Chat ===================== with gr.Column(scale=2): chatbot = gr.Chatbot( label="Chat", height=500, ) # Audio-Ausgabe (TTS) voice_out = gr.Audio(label="Vorgelesene Antwort", type="numpy") # Multimodal-Textbox mit Mikrofon in der Leiste chat_input = gr.MultimodalTextbox( label=None, placeholder="Stelle deine Frage zum Prüfungsrecht … oder sprich ins Mikrofon", show_label=False, sources=["microphone"], # nur Mikrofon (kein Upload nötig) file_types=["audio"], max_lines=6, ) # Senden bei Enter / Klick auf Icon chat_input.submit( chat_fn, [chat_input, chatbot], [chatbot, voice_out, chat_input], ) send_btn = gr.Button("Senden") send_btn.click( chat_fn, [chat_input, chatbot], [chatbot, voice_out, chat_input], ) # Button: Antwort erneut vorlesen read_btn = gr.Button("🔁 Antwort erneut vorlesen") read_btn.click( read_last_answer, [chatbot], [voice_out], ) # Chat löschen clear_btn = gr.Button("Chat zurücksetzen") clear_btn.click( lambda: ([], None, {"text": "", "files": []}), None, [chatbot, voice_out, chat_input], ) # ===================== RECHTER TEIL: Viewer ===================== with gr.Column(scale=1): # PDF-URL aus metadata holen pdf_meta = next(d.metadata for d in docs if d.metadata["type"] == "pdf") gr.Markdown("### 📄 Prüfungsordnung (PDF)") PDF(pdf_meta["pdf_url"], height=350) # HG-Viewer-URL (hg_clean.html aus Supabase Storage) hg_meta = next(d.metadata for d in docs if d.metadata["type"] == "hg") hg_url = hg_meta["viewer_url"].split("#")[0] gr.Markdown("### 📘 Hochschulgesetz NRW (Viewer)") gr.HTML( f'' ) if __name__ == "__main__": demo.queue().launch(ssr_mode=False, show_error=True)