KumpasRAG / app.py
adrian4444's picture
Initial deployment
3a21631
Raw
History Blame Contribute Delete
7.17 kB
# app.py โ€” KUMPAS RAG Server for Hugging Face Spaces
# Stack: FAISS + HuggingFace all-MiniLM-L6-v2 + Groq llama-3.3-70b
import os
import logging
from contextlib import asynccontextmanager
from typing import List, Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_groq import ChatGroq
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# โ”€โ”€ Global chain (loaded once at startup) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
qa_chain = None
def build_chain():
"""
Loads the FAISS index from disk and builds the QA chain.
Called once when the server starts.
"""
global qa_chain
groq_api_key = os.environ.get("GROQ_API_KEY", "")
if not groq_api_key:
raise RuntimeError("GROQ_API_KEY environment variable is not set!")
faiss_path = os.environ.get("FAISS_PATH", "student_manual_faiss")
text_path = os.environ.get("TEXT_PATH", "harrypotter.txt")
logger.info("Loading embeddings model...")
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# โ”€โ”€ Try loading saved FAISS index first (fast) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if os.path.exists(faiss_path):
logger.info(f"Loading FAISS index from {faiss_path}...")
vectorstore = FAISS.load_local(
faiss_path,
embeddings,
allow_dangerous_deserialization=True
)
logger.info("FAISS index loaded from disk โœ…")
# โ”€โ”€ Otherwise build from text file (slow, first run only) โ”€โ”€โ”€
elif os.path.exists(text_path):
logger.info(f"Building FAISS index from {text_path} (this takes a few minutes)...")
from langchain_text_splitters import RecursiveCharacterTextSplitter
try:
with open(text_path, "r", encoding="latin-1") as f:
text = f.read()
except UnicodeDecodeError:
with open(text_path, "r", encoding="cp1252") as f:
text = f.read()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(text)
logger.info(f"Created {len(chunks)} chunks")
vectorstore = FAISS.from_texts(chunks, embedding=embeddings)
vectorstore.save_local(faiss_path)
logger.info("FAISS index built and saved โœ…")
else:
raise RuntimeError(
f"Neither FAISS index ({faiss_path}) nor text file ({text_path}) found! "
"Make sure harrypotter.txt is in your Space files."
)
# โ”€โ”€ Build retriever โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
# โ”€โ”€ Groq LLM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
llm = ChatGroq(
model="llama-3.3-70b-versatile",
temperature=0.5,
groq_api_key=groq_api_key
)
# โ”€โ”€ Prompt (same as your Colab notebook) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
prompt = ChatPromptTemplate.from_template("""
Answer the question based only on the following context from the Harry Potter series:
{context}
Question: {question}
Answer clearly and concisely. If the answer is not in the context, say so honestly.
""")
# โ”€โ”€ LCEL chain (identical to your Colab) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
qa_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
logger.info("QA chain ready โœ…")
return qa_chain
# โ”€โ”€ Lifespan: build chain on startup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("๐Ÿ”ฎ Starting KUMPAS RAG server...")
build_chain()
logger.info("๐Ÿ”ฎ Server ready!")
yield
logger.info("Server shutting down.")
# โ”€โ”€ FastAPI app โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app = FastAPI(
title="KUMPAS Harry Potter RAG API",
description="Wizarding Oracle โ€” FAISS + Groq LLaMA",
version="1.0.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
# โ”€โ”€ Request / Response models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class HistoryItem(BaseModel):
role: str
content: str
class AskRequest(BaseModel):
question: str
history: Optional[List[HistoryItem]] = []
class AskResponse(BaseModel):
answer: str
# โ”€โ”€ Endpoints โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@app.get("/")
def root():
return {"message": "๐Ÿ”ฎ KUMPAS Wizarding Oracle is alive!", "status": "ok"}
@app.get("/health")
def health():
return {
"status": "ok",
"oracle": "ready" if qa_chain else "loading",
"model": "llama-3.3-70b-versatile"
}
@app.post("/ask", response_model=AskResponse)
def ask(req: AskRequest):
if qa_chain is None:
raise HTTPException(status_code=503, detail="Oracle is still initializing. Try again in a moment.")
question = req.question.strip()
if not question:
raise HTTPException(status_code=400, detail="Question cannot be empty.")
# โ”€โ”€ Prepend recent history to give the LLM context โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if req.history:
recent = req.history[-4:] # last 2 turns
history_text = "\n".join(
f"{'User' if h.role == 'user' else 'Oracle'}: {h.content}"
for h in recent
)
full_question = f"Previous conversation:\n{history_text}\n\nCurrent question: {question}"
else:
full_question = question
try:
logger.info(f"Question: {question[:80]}...")
answer = qa_chain.invoke(full_question)
answer = str(answer).strip()
if not answer:
answer = "The wizarding archives hold no record of this. Try rephrasing your question."
logger.info(f"Answer: {answer[:80]}...")
return AskResponse(answer=answer)
except Exception as e:
logger.error(f"Chain error: {e}")
raise HTTPException(
status_code=500,
detail="The magic is disrupted. The Oracle cannot answer right now."
)