Spaces:
Sleeping
Sleeping
| 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() | |
| 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 | |
| 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 | |
| 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 | |
| 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)}") |