Spaces:
Sleeping
Sleeping
| # 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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def root(): | |
| return {"message": "๐ฎ KUMPAS Wizarding Oracle is alive!", "status": "ok"} | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "oracle": "ready" if qa_chain else "loading", | |
| "model": "llama-3.3-70b-versatile" | |
| } | |
| 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." | |
| ) | |