UI and chatbot updates

This commit is contained in:
2026-10-08 16:08:00 +02:00
parent 30fd55da21
commit f68d7a6e25
9 changed files with 62 additions and 32 deletions
+1
View File
@@ -11,6 +11,7 @@ ENV/
# Korpus-PDFs (urheberrechtlich geschützt, nicht ins Repository)
data/
data_test/
data_primary/
# Vector store
vectorstore/
+7 -5
View File
@@ -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)]
+46 -26
View File
@@ -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")
+1
View File
@@ -12,3 +12,4 @@ fastapi
uvicorn[standard]
rank_bm25
snowballstemmer
python-dotenv
+5
View File
@@ -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", [])
+2 -1
View File
@@ -463,7 +463,7 @@
<div class="avatar" aria-hidden="true"></div>
<div class="who">
<strong id="head-name"></strong>
<span>Gespräch über Franz Kafka</span>
<span id="head-topic"></span>
</div>
<div class="selectors">
<label class="model">
@@ -519,6 +519,7 @@
document.getElementById('persona-role').textContent = PERSONA.role;
document.getElementById('persona-status').textContent = PERSONA.status;
document.getElementById('head-name').textContent = PERSONA.name;
document.getElementById('head-topic').textContent = PERSONA.topic;
inputEl.placeholder = 'Ihre Frage an ' + PERSONA.name + ' …';
portraitEl.classList.add('switching');
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB