From f68d7a6e25b701d5c15ddfd67cdfce99aa8b2fa6 Mon Sep 17 00:00:00 2001
From: Philipp Mock
Date: Thu, 8 Oct 2026 16:08:00 +0200
Subject: [PATCH] UI and chatbot updates
---
.gitignore | 1 +
local_rag.py | 12 +--
personas.py | 72 +++++++++++-------
requirements.txt | 1 +
server.py | 5 ++
templates/chat.html | 3 +-
templates/visuals/contemporary_avatar.jpg | Bin 17072 -> 17320 bytes
.../visuals/kafka_contemporary_office.jpg | Bin 299166 -> 306570 bytes
.../visuals/kafka_contemporary_office_2.png | Bin 0 -> 2332606 bytes
9 files changed, 62 insertions(+), 32 deletions(-)
create mode 100644 templates/visuals/kafka_contemporary_office_2.png
diff --git a/.gitignore b/.gitignore
index 9eeed1a..277a919 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ ENV/
# Korpus-PDFs (urheberrechtlich geschützt, nicht ins Repository)
data/
data_test/
+data_primary/
# Vector store
vectorstore/
diff --git a/local_rag.py b/local_rag.py
index 3e2cee7..789a0b6 100644
--- a/local_rag.py
+++ b/local_rag.py
@@ -79,9 +79,9 @@ MAX_TOOL_ROUNDS = 3 # searches per message before the LLM must answer
# System prompt for every chat turn (German, like the corpus and the questions).
SYSTEM_PROMPT = f"""Du beantwortest Fragen zu Franz Kafka, seinen Werken, seinem Leben und der Forschung.
-Du hast ein Werkzeug, {SEARCH_TOOL_NAME}, das eine Bibliothek aus Kafkas Texten und Forschungsliteratur durchsucht.
+Du hast ein Tool, {SEARCH_TOOL_NAME}, das eine Bibliothek aus Kafkas Texten und Forschungsliteratur durchsucht.
- Für jede inhaltliche Frage zu Kafka, seinen Werken, Figuren, seiner Biografie, Sprache oder zur Forschung: Rufe zuerst das Werkzeug auf und stütze die Antwort auf die gefundenen Auszüge. Antworte nicht aus dem Gedächtnis.
-- Bei Begrüßung, Dank, Smalltalk oder Fragen zum Gespräch selbst: Antworte direkt, ohne Werkzeug.
+- Bei Begrüßung, Dank, Smalltalk oder Fragen zum Gespräch selbst: Antworte direkt, ohne Tool.
- Formuliere die Suchanfrage so, dass sie ohne den Gesprächsverlauf verständlich ist.
- Finden sich in den Auszügen keine Belege, sage das offen, statt zu raten.
- Nenne bei Aussagen aus den Auszügen die Quelle (Autor, Seite)."""
@@ -387,10 +387,12 @@ class LocalRAG:
content = "".join(part.get("text", "") if isinstance(part, dict) else str(part) for part in content)
return (content or "").strip()
- def query_with_history(self, question, chat_history=None, k=None, system_prompt=None):
+ def query_with_history(self, question, chat_history=None, k=None, system_prompt=None,
+ tool_description=None):
"""Answer a chat message. The LLM decides whether to search the corpus: the search is
offered as a tool (SEARCH_TOOL_NAME), which the LLM calls with its own query, up to
- MAX_TOOL_ROUNDS times. system_prompt defaults to SYSTEM_PROMPT (see personas.py for others).
+ MAX_TOOL_ROUNDS times. system_prompt defaults to SYSTEM_PROMPT and tool_description to
+ SEARCH_TOOL_DESCRIPTION (see personas.py for others).
Returns dict with 'answer', 'retrieved' (chunks the LLM saw, with content, source, page,
score) and 'searches' (the queries it used)."""
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
@@ -418,7 +420,7 @@ class LocalRAG:
for i, (d, _) in enumerate(hits, start=1))
tool = StructuredTool.from_function(func=search_kafka_corpus, name=SEARCH_TOOL_NAME,
- description=SEARCH_TOOL_DESCRIPTION)
+ description=tool_description or SEARCH_TOOL_DESCRIPTION)
llm_with_tools = self.llm.bind_tools([tool])
messages = [SystemMessage(system_prompt or SYSTEM_PROMPT)]
diff --git a/personas.py b/personas.py
index 47b07ab..73b5418 100644
--- a/personas.py
+++ b/personas.py
@@ -2,9 +2,12 @@
Gesprächspartner des Chats: Oberflächentexte, Systemprompt und Vektorspeicher je Figur.
Die Oberfläche lädt die Einträge über /api/personas (ohne Systemprompt und Index);
-server.py wählt pro Anfrage den Systemprompt und den Vektorspeicher der Figur.
-Alle Figuren nutzen vorerst denselben Vektorspeicher (VECTORSTORE_PATH); für eigene
-Korpora je Figur genügt es, "vectorstore" hier zu ändern.
+server.py wählt pro Anfrage den Systemprompt, die Beschreibung des Such-Werkzeugs und
+den Vektorspeicher der Figur.
+
+Vektorspeicher:
+ VECTORSTORE_PATH ganzes Kafka-Korpus (Primärtext + Forschung), für die Forscherin
+ PRIMARY_VECTORSTORE_PATH nur der Primärtext "Die Verwandlung", für Zeitgenosse und Autor
"""
from local_rag import SEARCH_TOOL_NAME, SYSTEM_PROMPT
@@ -14,21 +17,35 @@ from local_rag import SEARCH_TOOL_NAME, SYSTEM_PROMPT
# Gebaut mit: add_pdfs.py --data data --index ./vectorstore_pypdf_2000 --loader pypdf --no-headers
VECTORSTORE_PATH = "./vectorstore_pypdf_2000"
-# Gemeinsame Regeln für das Such-Werkzeug (wie im Systemprompt der Forscherin)
-_TOOL_RULES = f"""Du hast ein Werkzeug, {SEARCH_TOOL_NAME}, das eine Bibliothek aus Kafkas Texten und Forschungsliteratur durchsucht.
-- Für jede inhaltliche Frage zu Kafka, seinen Werken, Figuren, seiner Biografie, Sprache oder zur Forschung: Rufe zuerst das Werkzeug auf und stütze die Antwort auf die gefundenen Auszüge. Antworte nicht aus dem Gedächtnis.
-- Bei Begrüßung, Dank, Smalltalk oder Fragen zum Gespräch selbst: Antworte direkt, ohne Werkzeug.
+# Nur der Primärtext, gleiche Einstellungen. Gebaut mit (data_primary/ enthält nur einen
+# Verweis auf data/Kafka_Die_Verwandlung_.pdf):
+# add_pdfs.py --data data_primary --index ./vectorstore_primary_pypdf_2000 --loader pypdf --no-headers
+PRIMARY_VECTORSTORE_PATH = "./vectorstore_primary_pypdf_2000"
+
+# Beschreibung des Such-Werkzeugs für Figuren, die nur den Primärtext durchsuchen
+PRIMARY_TOOL_DESCRIPTION = (
+ "Durchsucht den Text von Kafkas Erzählung \"Die Verwandlung\" und liefert die passendsten "
+ "Textauszüge mit Seite. Die Suchanfrage sollte eine eigenständige, präzise Frage oder "
+ "Stichwortfolge auf Deutsch sein."
+)
+
+# Regeln für das Such-Werkzeug bei Figuren, die nur den Primärtext durchsuchen
+_TOOL_RULES = f"""Du hast ein Tool, {SEARCH_TOOL_NAME}, das den Text von Kafkas Erzählung "Die Verwandlung" durchsucht.
+- Für jede inhaltliche Frage zur Verwandlung, ihren Figuren, Szenen und Motiven: Rufe zuerst das Tool auf und stütze die Antwort auf die gefundenen Textstellen. Antworte nicht aus dem Gedächtnis.
+- Bei Begrüßung, Dank, Smalltalk oder Fragen zum Gespräch selbst: Antworte direkt, ohne Tool.
+- Bei Fragen zu dir selbst oder zu Personen aus deinem Umfeld, die keine fiktionalen Charaktere aus der Verwandlung sind: Antworte direkt, ohne Tool.
- Formuliere die Suchanfrage so, dass sie ohne den Gesprächsverlauf verständlich ist.
- Finden sich in den Auszügen keine Belege, sage das offen, statt zu raten.
-- Nenne bei Aussagen aus den Auszügen die Quelle (Autor, Seite) knapp in Klammern."""
+- Nenne bei Aussagen aus dem Text die Seite knapp in Klammern."""
PERSONAS = {
# Die Forscherin: unverändert gegenüber der ersten Fassung der Oberfläche
"researcher": {
"label": "Forscherin",
+ "topic": "Gespräch über die Verwandlung und Franz Kafka",
"name": "Dr. Klara Weiß",
"role": "Literaturwissenschaftlerin, Kafka-Forschung",
- "place": "Arbeitszimmer · Prag, am Abend",
+ "place": "Arbeitszimmer · Gegenwart",
"status": "antwortet aus ihrer Kafka-Bibliothek",
"image": "/visuals/kafka_researcher_office.jpg",
"image_alt": "Eine Literaturwissenschaftlerin arbeitet am Abend in ihrem Arbeitszimmer in Prag: "
@@ -37,10 +54,10 @@ PERSONAS = {
"image_position": "60% 40%",
"avatar": "/visuals/researcher_avatar.jpg",
"greeting": (
- "Guten Abend. Kommen Sie herein, und schieben Sie die Bücher ruhig ein wenig zur Seite.\n\n"
+ "Guten Abend. Kommen Sie herein und setzen Sie sich.\n\n"
"Ich arbeite seit vielen Jahren zu Kafka, vor allem zur *Verwandlung*, zu seiner Sprache und zu "
"dem, was die Forschung daraus gemacht hat. Fragen Sie mich, was Sie interessiert: Ich antworte "
- "aus den Texten, die hier auf meinem Schreibtisch liegen, und sage Ihnen, wo es steht."
+ "auf Basis der Texte, die hier auf meinem Schreibtisch liegen."
),
"suggestions": [
"Was bedeutet der Name Kafka auf Tschechisch?",
@@ -55,15 +72,16 @@ PERSONAS = {
# Ein erfundener Zeitgenosse: Prager Feuilletonist um 1915
"contemporary": {
"label": "Zeitgenosse",
+ "topic": "Gespräch über die Verwandlung und Franz Kafka",
"name": "Viktor Hollmann",
"role": "Feuilletonist, ein Zeitgenosse Kafkas",
- "place": "Redaktionsstube · Prag, um 1915",
+ "place": "Redaktionsstube · Prag, um 1920",
"status": "erzählt aus dem Prag seiner Zeit",
"image": "/visuals/kafka_contemporary_office.jpg",
- "image_alt": "Ein Herr im Tweedanzug mit runder Brille und Schnurrbart sitzt am Abend an seinem "
- "Schreibtisch in Prag und erzählt gestikulierend; hinter ihm durchs Fenster die "
- "Karlsbrücke und die Burg in der Dämmerung.",
- "image_position": "62% 38%",
+ "image_alt": "Ein Herr mit Schnurrbart und dunklem Anzug sitzt am Abend rauchend an seinem "
+ "Schreibtisch voller Manuskripte in Prag, den Kopf in die Hand gestützt; durchs "
+ "Fenster die Karlsbrücke und die Burg in der Dämmerung.",
+ "image_position": "64% 36%",
"avatar": "/visuals/contemporary_avatar.jpg",
"greeting": (
"Guten Abend, treten Sie nur ein! Nehmen Sie Platz, der Kaffee ist noch warm.\n\n"
@@ -77,17 +95,19 @@ PERSONAS = {
"Was war Kafka für ein Mensch?",
"Welche Sprachen sprach man in Kafkas Familie?",
],
- "system_prompt": f"""Du bist Viktor Hollmann, ein erfundener Prager Feuilletonist um 1915 und Zeitgenosse Franz Kafkas.
-Du sprichst in der Ich-Form, höflich und ein wenig altmodisch, im Ton des frühen 20. Jahrhunderts, und siezt dein Gegenüber.
-Du kennst Prag, seine Kaffeehäuser und den Literaturbetrieb jener Jahre.
+ "system_prompt": f"""Du bist Viktor Hollmann, ein erfundener Prager Feuilletonist um 1920 und Zeitgenosse Franz Kafkas.
+Du sprichst in der Ich-Form, höflich und klar, im Ton des frühen 20. Jahrhunderts, und siezt dein Gegenüber.
+Du kennst Prag und den Literaturbetrieb zu dieser Zeit. Du bist ein Zeitgenosse Kafkas und kannst keine Aussagen zu Ereignissen und Personen nach dieser Zeit machen.
{_TOOL_RULES}
-- Die Auszüge stammen teils aus viel späterer Forschung. Gib ihren Inhalt wieder, als hättest du davon gehört oder gelesen. Erfinde keine Begegnungen, Zitate oder Ereignisse, die nicht in den Auszügen stehen.""",
- "vectorstore": VECTORSTORE_PATH,
+- Du kennst die Verwandlung als Leser ihrer Zeit. Erfinde keine Begegnungen, Zitate oder Ereignisse, die nicht im Text stehen.""",
+ "tool_description": PRIMARY_TOOL_DESCRIPTION,
+ "vectorstore": PRIMARY_VECTORSTORE_PATH,
},
# Kafka selbst: eine Annäherung aus seinen Texten, keine echte Stimme
"kafka": {
"label": "Autor",
+ "topic": "Gespräch mit dem Autor",
"name": "Franz Kafka",
"role": "Schriftsteller, Prag",
"place": "Arbeitszimmer · Prag, in der Nacht",
@@ -111,16 +131,16 @@ Du kennst Prag, seine Kaffeehäuser und den Literaturbetrieb jener Jahre.
"Warum wird Gregor zu einem Ungeziefer?",
],
"system_prompt": f"""Du sprichst als Franz Kafka (1883–1924), in einer behutsamen Annäherung aus seinen Texten.
-Du sprichst in der Ich-Form, ruhig, genau, zweifelnd und selbstironisch, im Ton seiner Briefe und Tagebücher, und siezt dein Gegenüber.
+Du sprichst in der Ich-Form, ruhig, genau, zweifelnd und selbstironisch, im Ton seiner Briefe und Tagebücher, und siezt dein Gegenüber. Fragen zu deiner Person, deinem Werk und deinem Umfeld beantwortest du im Präsenz, es sei denn du sprichst über Ereignisse, die in der Vergangenheit liegen.
{_TOOL_RULES}
-- Stütze Aussagen über dein Leben und Werk auf die gefundenen Auszüge. Wo sie nichts hergeben, sag, dass du es nicht weißt oder nicht sagen kannst. Erfinde keine Zitate.
-- Die Auszüge stammen teils aus der Forschung über dich. Du darfst darauf mit Befremden oder Ironie reagieren ("man schreibt, ich hätte …").""",
- "vectorstore": VECTORSTORE_PATH,
+- Stütze Aussagen über dein Werk auf die gefundenen Textstellen. Wo sie nichts hergeben, sag, dass du es nicht weißt oder nicht sagen kannst. Erfinde keine Zitate.""",
+ "tool_description": PRIMARY_TOOL_DESCRIPTION,
+ "vectorstore": PRIMARY_VECTORSTORE_PATH,
},
}
DEFAULT_PERSONA = "researcher"
# Felder, die die Oberfläche bekommt (Systemprompt und Index bleiben auf dem Server)
-PUBLIC_FIELDS = ("label", "name", "role", "place", "status", "image", "image_alt", "image_position",
+PUBLIC_FIELDS = ("label", "topic", "name", "role", "place", "status", "image", "image_alt", "image_position",
"avatar", "greeting", "suggestions")
diff --git a/requirements.txt b/requirements.txt
index cb0bc3c..c779334 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,3 +12,4 @@ fastapi
uvicorn[standard]
rank_bm25
snowballstemmer
+python-dotenv
diff --git a/server.py b/server.py
index 1c60217..001d872 100644
--- a/server.py
+++ b/server.py
@@ -8,11 +8,15 @@ from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
+from dotenv import load_dotenv
from pydantic import BaseModel
from local_rag import LocalRAG
from personas import DEFAULT_PERSONA, PERSONAS, PUBLIC_FIELDS
+# OPENAI_API_KEY may come from a local .env (git-ignored); an exported variable takes precedence.
+load_dotenv()
+
OLLAMA_MODEL = "gemma4:12b"
OPENAI_MODEL = "gpt-6-luna"
# One RAG instance per (LLM provider, vector store), for on-the-fly provider switching.
@@ -101,6 +105,7 @@ def chat(request: ChatRequest):
request.message.strip(),
chat_history=chat_history,
system_prompt=persona["system_prompt"],
+ tool_description=persona.get("tool_description"),
)
answer = result["answer"]
retrieved = result.get("retrieved", [])
diff --git a/templates/chat.html b/templates/chat.html
index bf99015..0d367f0 100644
--- a/templates/chat.html
+++ b/templates/chat.html
@@ -463,7 +463,7 @@