- eval mit gpt-6-luna
- new chat interface with personas
This commit is contained in:
@@ -7,37 +7,39 @@ from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
from local_rag import LocalRAG
|
||||
from personas import DEFAULT_PERSONA, PERSONAS, PUBLIC_FIELDS
|
||||
|
||||
OLLAMA_MODEL = "gpt-oss:20b"
|
||||
OLLAMA_MODEL = "gemma4:12b"
|
||||
OPENAI_MODEL = "gpt-6-luna"
|
||||
# Kafka corpus: PyPDFLoader, chunks 2000/400 without headers, bge-m3, hybrid search + reranker.
|
||||
# Docling was not better at retrieval (eval/kafka_human_judge_*.txt), so pypdf stays for now.
|
||||
# built with: add_pdfs.py --data data --index ./vectorstore_pypdf_2000 --loader pypdf --no-headers
|
||||
VECTORSTORE_PATH = "./vectorstore_pypdf_2000"
|
||||
|
||||
# Dual RAG instances for on-the-fly provider switching
|
||||
rag_ollama = LocalRAG(
|
||||
vectorstore_path=VECTORSTORE_PATH,
|
||||
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}")
|
||||
# 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 = {}
|
||||
for vectorstore in sorted({p["vectorstore"] for p in PERSONAS.values()}):
|
||||
rags[("ollama", vectorstore)] = LocalRAG(
|
||||
vectorstore_path=vectorstore,
|
||||
llm_provider="ollama",
|
||||
ollama_model=OLLAMA_MODEL,
|
||||
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)
|
||||
|
||||
app = FastAPI(title="Local RAG Chat", version="1.0.0")
|
||||
# Images for the chat GUI (researcher portrait, avatar)
|
||||
app.mount("/visuals", StaticFiles(directory=Path(__file__).parent / "templates" / "visuals"), name="visuals")
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
@@ -49,6 +51,7 @@ class ChatRequest(BaseModel):
|
||||
message: str
|
||||
history: list[ChatMessage] = [] # previous turns for conversation context
|
||||
llm_provider: str = "ollama" # "ollama" | "openai"
|
||||
persona: str = DEFAULT_PERSONA # key in personas.PERSONAS
|
||||
|
||||
|
||||
class RetrievedChunk(BaseModel):
|
||||
@@ -62,6 +65,7 @@ class ChatResponse(BaseModel):
|
||||
answer: str
|
||||
error: str | None = None
|
||||
retrieved: list[RetrievedChunk] | None = None
|
||||
searches: list[str] | None = None # queries the LLM sent to the corpus search tool
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
@@ -73,11 +77,11 @@ 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
|
||||
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."""
|
||||
vectorstore = PERSONAS[persona]["vectorstore"]
|
||||
return rags.get((provider, vectorstore)) or rags[("ollama", vectorstore)]
|
||||
|
||||
|
||||
@app.post("/api/chat", response_model=ChatResponse)
|
||||
@@ -85,17 +89,22 @@ 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:
|
||||
if request.llm_provider == "openai" and not openai_available:
|
||||
return ChatResponse(answer="", error="OpenAI not configured. Set OPENAI_API_KEY.")
|
||||
rag = _get_rag(request.llm_provider)
|
||||
if request.persona not in PERSONAS:
|
||||
return ChatResponse(answer="", error=f"Unknown persona: {request.persona}")
|
||||
persona = PERSONAS[request.persona]
|
||||
rag = _get_rag(request.llm_provider, request.persona)
|
||||
try:
|
||||
chat_history = [{"role": m.role, "content": m.content} for m in request.history]
|
||||
result = rag.query_with_history(
|
||||
request.message.strip(),
|
||||
chat_history=chat_history,
|
||||
system_prompt=persona["system_prompt"],
|
||||
)
|
||||
answer = result["answer"]
|
||||
retrieved = result.get("retrieved", [])
|
||||
searches = result.get("searches", [])
|
||||
|
||||
# Server-side console trace: shorter chunk logs + raw LLM response
|
||||
if retrieved:
|
||||
@@ -105,12 +114,12 @@ def chat(request: ChatRequest):
|
||||
preview = (content[:150] + "...") if len(content) > 150 else content
|
||||
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"\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
|
||||
print(f"[RAG] LLM response ({provider_label} / {model_name}):\n{answer}")
|
||||
print(f"[RAG] LLM response ({provider_label} / {model_name}, {request.persona}):\n{answer}")
|
||||
|
||||
return ChatResponse(answer=answer, retrieved=retrieved)
|
||||
return ChatResponse(answer=answer, retrieved=retrieved, searches=searches)
|
||||
except Exception as e:
|
||||
return ChatResponse(answer="", error=str(e))
|
||||
|
||||
@@ -118,14 +127,23 @@ def chat(request: ChatRequest):
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
"""Health check and vector store status."""
|
||||
has_docs = rag_ollama.vectorstore is not None
|
||||
has_docs = all(rag.vectorstore is not None for rag in rags.values())
|
||||
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}
|
||||
return {"ollama": True, "openai": openai_available}
|
||||
|
||||
|
||||
@app.get("/api/personas")
|
||||
def personas():
|
||||
"""Return the selectable conversation partners (texts and images for the GUI)."""
|
||||
return {
|
||||
"default": DEFAULT_PERSONA,
|
||||
"personas": [{"id": key, **{f: p[f] for f in PUBLIC_FIELDS}} for key, p in PERSONAS.items()],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user