faq-genius / src /main.py
itsadityacodes's picture
Deploying Python RAG chatbot stack to Hugging Face
fe2ee65
Raw
History Blame Contribute Delete
6.52 kB
import os
import json
import io
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, UploadFile, File, Header
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List, Dict, Optional
from dotenv import load_dotenv
from pypdf import PdfReader
from langchain_nvidia_ai_endpoints import ChatNVIDIA
from langchain_core.messages import HumanMessage, AIMessage
from src.database import VectorDBManager
from src.engine import build_rag_chain
load_dotenv()
@asynccontextmanager
async def lifespan(app: FastAPI):
if not os.path.exists("./chroma_db"):
print("Initializing Vector Database...")
db_manager = VectorDBManager()
db_manager.initialize_db("data/faqs.json")
yield
app = FastAPI(title="Enterprise FAQ Bot Engine", version="1.0", lifespan=lifespan)
class QueryRequest(BaseModel):
question: str
history: Optional[List[Dict[str, str]]] = []
session_id: Optional[str] = "default_session"
# 📂 DYNAMIC KNOWLEDGE INGESTION ENDPOINT
@app.post("/api/v1/upload")
async def upload_file_endpoint(
file: UploadFile = File(...),
x_session_id: Optional[str] = Header(None)
):
"""
Accepts standard multipart form file uploads (.txt or .pdf), extracts raw text
contents in-memory, and passes them along with a unique session ID.
"""
try:
contents = await file.read()
filename = file.filename.lower()
text_data = ""
# 📄 Process Plain Text Files
if filename.endswith(".txt"):
text_data = contents.decode("utf-8")
# 📕 Process PDF Files In-Memory
elif filename.endswith(".pdf"):
pdf_stream = io.BytesIO(contents)
pdf_reader = PdfReader(pdf_stream)
extracted_pages = []
for page in pdf_reader.pages:
page_text = page.extract_text()
if page_text:
extracted_pages.append(page_text)
text_data = "\n".join(extracted_pages)
if not text_data.strip():
raise HTTPException(
status_code=400,
detail="The uploaded PDF appears to be empty or contains only non-scanned imagery (OCR required)."
)
else:
raise HTTPException(
status_code=400,
detail="Unsupported file format. Please upload a valid plain text (.txt) or PDF (.pdf) document."
)
db_manager = VectorDBManager()
target_session = x_session_id or "default_session"
db_manager.add_text_to_db(text_data, filename=file.filename, session_id=target_session)
return {
"status": "success",
"message": f"Successfully vectorized and stored {file.filename} under session context!"
}
except UnicodeDecodeError:
raise HTTPException(
status_code=400,
detail="File encoding error: Please ensure your text file is saved with valid UTF-8 encoding."
)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ingestion pipeline failed: {str(e)}")
# 🗑️ ADMIN DATABASE RESET ENDPOINT
@app.post("/api/v1/clear")
async def clear_database_endpoint():
"""
Triggers a collection wipe on the vector database.
"""
db_manager = VectorDBManager()
if db_manager.clear_db():
return {"status": "success", "message": "Vector database cleared successfully."}
else:
raise HTTPException(status_code=500, detail="Failed to drop vector database collection.")
# ⚡ LIVE STREAMING CHAT ROUTER
@app.post("/api/v1/chat")
async def chat_endpoint(payload: QueryRequest):
try:
search_query = payload.question
chat_history = []
# Format the conversational history state if it exists
if payload.history and len(payload.history) > 0:
for msg in payload.history:
if msg["role"] == "user":
chat_history.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
chat_history.append(AIMessage(content=msg["content"]))
# ✨ RESTORED & ALIGNED MEMORY CONTEXT BLOCK
try:
rephrase_llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct")
history_context = ""
for msg in payload.history:
role = "User" if msg["role"] == "user" else "Assistant"
history_context += f"{role}: {msg['content']}\n"
condense_prompt = (
f"Given the following chat history and a follow-up question, "
f"rephrase the follow-up question into a standalone query.\n\n"
f"Chat History:\n{history_context}\n"
f"Follow-up Question: {payload.question}\n\n"
f"Standalone Query:"
)
# Execute standard LangChain invocation
llm_response = rephrase_llm.invoke(condense_prompt)
search_query = llm_response.content.strip()
except Exception as context_error:
print(f"⚠️ Memory rephrasing failed: {context_error}")
search_query = payload.question
# Build execution pipeline tied strictly to this user's workspace partitions
rag_chain = build_rag_chain(session_id=payload.session_id)
async def event_generator():
async for chunk in rag_chain.astream({"input": search_query, "chat_history": chat_history}):
if "context" in chunk:
sources_payload = [
{"content": doc.page_content, "metadata": doc.metadata}
for doc in chunk["context"]
]
yield json.dumps({"type": "sources", "content": sources_payload}) + "\n"
if "answer" in chunk:
yield json.dumps({"type": "token", "content": chunk["answer"]}) + "\n"
return StreamingResponse(event_generator(), media_type="application/x-ndjson")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Chat streaming pipeline failed: {str(e)}")