Primary-text index for personas, faster retrieval, OpenAI model options

- Contemporary and Kafka personas search only "Die Verwandlung" (own index and tool description); per-persona subtitle
- New contemporary portrait and avatar alt text
- ChatGPT: gpt-6.1-sol, reasoning_effort low via Responses API; default in the model menu
- GPT-Realtime-2.1 Mini as third option (realtime_chat.py, WebSocket, text only)
- Load .env for OPENAI_API_KEY
- Reranker in fp16 on GPU with 25 candidates; reranker, embeddings and BM25 shared and built at server start
- BM25 stem cache (first search ~31s -> ~1.7s)
- Timing output per LLM call and search

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 07:04:59 +01:00
co-authored by Claude Opus 5.5
parent f68d7a6e25
commit e4b7f0ce34
6 changed files with 243 additions and 46 deletions
+27 -17
View File
@@ -18,7 +18,10 @@ from personas import DEFAULT_PERSONA, PERSONAS, PUBLIC_FIELDS
load_dotenv()
OLLAMA_MODEL = "gemma4:12b"
OPENAI_MODEL = "gpt-6-luna"
OPENAI_MODEL = "gpt-6.1-sol"
REALTIME_MODEL = "gpt-realtime-2.1-mini" # OpenAI Realtime API, text only (realtime_chat.py)
# Model name per provider, for the console trace
MODEL_NAMES = {"ollama": OLLAMA_MODEL, "openai": OPENAI_MODEL, "realtime": REALTIME_MODEL}
# One RAG instance per (LLM provider, vector store), for on-the-fly provider switching.
# Each persona names its vector store in personas.py; personas sharing one share the instance.
rags = {}
@@ -30,16 +33,23 @@ for vectorstore in sorted({p["vectorstore"] for p in PERSONAS.values()}):
openai_model=OPENAI_MODEL,
)
if os.environ.get("OPENAI_API_KEY"):
try:
rags[("openai", vectorstore)] = LocalRAG(
vectorstore_path=vectorstore,
llm_provider="openai",
ollama_model=OLLAMA_MODEL,
openai_model=OPENAI_MODEL,
)
except Exception as e:
print(f"OpenAI RAG not available for {vectorstore}: {e}")
openai_available = any(provider == "openai" for provider, _ in rags)
for provider in ("openai", "realtime"):
try:
rags[(provider, vectorstore)] = LocalRAG(
vectorstore_path=vectorstore,
llm_provider=provider,
ollama_model=OLLAMA_MODEL,
openai_model=OPENAI_MODEL,
realtime_model=REALTIME_MODEL,
)
except Exception as e:
print(f"{provider} RAG not available for {vectorstore}: {e}")
available = {provider for provider, _ in rags}
openai_available = "openai" in available
# Load the reranker and build the BM25 indexes at startup (shared copies) so the first search
# is not delayed
for rag in rags.values():
rag.preload()
app = FastAPI(title="Local RAG Chat", version="1.0.0")
# Images for the chat GUI (researcher portrait, avatar)
@@ -54,7 +64,7 @@ class ChatMessage(BaseModel):
class ChatRequest(BaseModel):
message: str
history: list[ChatMessage] = [] # previous turns for conversation context
llm_provider: str = "ollama" # "ollama" | "openai"
llm_provider: str = "ollama" # "ollama" | "openai" | "realtime"
persona: str = DEFAULT_PERSONA # key in personas.PERSONAS
@@ -83,7 +93,7 @@ def chat_view():
def _get_rag(provider: str, persona: str):
"""Return the RAG instance for the provider and the persona's vector store.
Fall back to Ollama if OpenAI is unavailable."""
Fall back to Ollama if the provider is unavailable."""
vectorstore = PERSONAS[persona]["vectorstore"]
return rags.get((provider, vectorstore)) or rags[("ollama", vectorstore)]
@@ -93,7 +103,7 @@ def chat(request: ChatRequest):
"""Handle a chat message and return the RAG answer."""
if not request.message or not request.message.strip():
return ChatResponse(answer="", error="Message cannot be empty")
if request.llm_provider == "openai" and not openai_available:
if request.llm_provider in ("openai", "realtime") and request.llm_provider not in available:
return ChatResponse(answer="", error="OpenAI not configured. Set OPENAI_API_KEY.")
if request.persona not in PERSONAS:
return ChatResponse(answer="", error=f"Unknown persona: {request.persona}")
@@ -120,8 +130,8 @@ def chat(request: ChatRequest):
print(f" [{i + 1}] {chunk.get('source', '')} p.{chunk.get('page', '?')} s={chunk.get('score')} | {preview!r}")
else:
print(f"\n[RAG] No corpus search for this message")
provider_label = "OpenAI" if request.llm_provider == "openai" else "Ollama"
model_name = OPENAI_MODEL if request.llm_provider == "openai" else OLLAMA_MODEL
provider_label = {"openai": "OpenAI", "realtime": "OpenAI Realtime"}.get(request.llm_provider, "Ollama")
model_name = MODEL_NAMES.get(request.llm_provider, OLLAMA_MODEL)
print(f"[RAG] LLM response ({provider_label} / {model_name}, {request.persona}):\n{answer}")
return ChatResponse(answer=answer, retrieved=retrieved, searches=searches)
@@ -139,7 +149,7 @@ def health():
@app.get("/api/providers")
def providers():
"""Return which LLM providers are available."""
return {"ollama": True, "openai": openai_available}
return {"ollama": True, "openai": openai_available, "realtime": "realtime" in available}
@app.get("/api/personas")