Add LLM provider switch, markdown chat UI, and update README

- Dual RAG instances (Ollama + OpenAI) for on-the-fly provider switching
- Provider dropdown in chat UI, /api/providers endpoint
- Markdown rendering for assistant responses
- Server logs include provider and model name for each LLM response
- README: OpenAI setup, add_pdfs, API docs, provider switch

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-11 16:09:42 +01:00
co-authored by Cursor
parent 650f73a74b
commit 9abda1d867
3 changed files with 131 additions and 46 deletions
+37 -7
View File
@@ -2,6 +2,7 @@
FastAPI server for Local RAG with chat GUI.
Run with: uvicorn server:app --reload
"""
import os
from pathlib import Path
from fastapi import FastAPI, HTTPException
@@ -10,18 +11,28 @@ from pydantic import BaseModel
from local_rag import LocalRAG
# LLM provider: "ollama" or "openai"
LLM_PROVIDER = "openai"
OLLAMA_MODEL = "gpt-oss:20b"
OPENAI_MODEL = "gpt-5.2"
VECTORSTORE_PATH = "./vectorstore"
rag = LocalRAG(
# Dual RAG instances for on-the-fly provider switching
rag_ollama = LocalRAG(
vectorstore_path=VECTORSTORE_PATH,
llm_provider=LLM_PROVIDER,
llm_provider="ollama",
ollama_model=OLLAMA_MODEL,
openai_model=OPENAI_MODEL,
)
rag_openai = None
if os.environ.get("OPENAI_API_KEY"):
try:
rag_openai = LocalRAG(
vectorstore_path=VECTORSTORE_PATH,
llm_provider="openai",
ollama_model=OLLAMA_MODEL,
openai_model=OPENAI_MODEL,
)
except Exception as e:
print(f"OpenAI RAG not available: {e}")
app = FastAPI(title="Local RAG Chat", version="1.0.0")
@@ -34,6 +45,7 @@ class ChatMessage(BaseModel):
class ChatRequest(BaseModel):
message: str
history: list[ChatMessage] = [] # previous turns for conversation context
llm_provider: str = "ollama" # "ollama" | "openai"
class RetrievedChunk(BaseModel):
@@ -58,11 +70,21 @@ def chat_view():
return HTMLResponse(content=html_path.read_text(encoding="utf-8"))
def _get_rag(provider: str):
"""Return the RAG instance for the given provider. Fall back to Ollama if OpenAI unavailable."""
if provider == "openai" and rag_openai is not None:
return rag_openai
return rag_ollama
@app.post("/api/chat", response_model=ChatResponse)
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 rag_openai is None:
return ChatResponse(answer="", error="OpenAI not configured. Set OPENAI_API_KEY.")
rag = _get_rag(request.llm_provider)
try:
chat_history = [{"role": m.role, "content": m.content} for m in request.history]
result = rag.query_with_history(
@@ -81,7 +103,9 @@ 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] Retrieved 0 chunks")
print(f"[RAG] LLM response:\n{answer}")
provider_label = "OpenAI" if request.llm_provider == "openai" else "Ollama"
model_name = OPENAI_MODEL if request.llm_provider == "openai" else OLLAMA_MODEL
print(f"[RAG] LLM response ({provider_label} / {model_name}):\n{answer}")
return ChatResponse(answer=answer, retrieved=retrieved)
except Exception as e:
@@ -91,10 +115,16 @@ def chat(request: ChatRequest):
@app.get("/api/health")
def health():
"""Health check and vector store status."""
has_docs = rag.vectorstore is not None
has_docs = rag_ollama.vectorstore is not None
return {"status": "ok", "vectorstore_loaded": has_docs}
@app.get("/api/providers")
def providers():
"""Return which LLM providers are available."""
return {"ollama": True, "openai": rag_openai is not None}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)