itsadityacodes commited on
Commit
fe2ee65
·
1 Parent(s): a02fa32

Deploying Python RAG chatbot stack to Hugging Face

Browse files
.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ .git/
6
+ .env
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies needed for building certain Python packages
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy and install dependencies
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy the rest of your application code
15
+ COPY . .
16
+
17
+ # Force the frontend to look at the local backend inside the same container
18
+ ENV BACKEND_URL=http://127.0.0.1:8000
19
+
20
+ # Give Windows permission to execute our startup script
21
+ RUN chmod +x start.sh
22
+
23
+ # Expose the precise port that Hugging Face expects
24
+ EXPOSE 7860
25
+
26
+ # Run our orchestration script to boot both apps together
27
+ CMD ["./start.sh"]
data/faqs.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "faq_001",
4
+ "category": "Refunds",
5
+ "question": "What is the refund window for international transactions?",
6
+ "answer": "International cross-border transactions can be refunded within 14 business days. However, a 2.5% currency conversion processing fee is non-refundable."
7
+ },
8
+ {
9
+ "id": "faq_002",
10
+ "category": "Security",
11
+ "question": "How do I reset my multi-factor authentication (MFA)?",
12
+ "answer": "To reset your MFA, navigate to Settings > Security > Two-Factor Authentication. If you are locked out, contact compliance-ops@fintech.com with your Employee ID."
13
+ },
14
+ {
15
+ "id": "faq_003",
16
+ "category": "Security",
17
+ "question": "How do I reset my account password?",
18
+ "answer": "To reset your account password, click 'Forgot Password' on the login page, enter your corporate email, and follow the secure verification link sent to your inbox."
19
+ }
20
+ ]
frontend.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import uuid
4
+ import json
5
+
6
+ # 🌐 BACKEND CONFIGURATION
7
+ BACKEND_URL = "http://backend:8000"
8
+
9
+ st.set_page_config(
10
+ page_title="Enterprise Multi-Tenant FAQ Bot",
11
+ page_icon="🤖",
12
+ layout="wide"
13
+ )
14
+
15
+ # 🔑 SESSION STATE INITIALIZATION
16
+ if "session_id" not in st.session_state:
17
+ # Generate a unique session token for this browser tab instance
18
+ st.session_state.session_id = str(uuid.uuid4())
19
+
20
+ if "messages" not in st.session_state:
21
+ st.session_state.messages = []
22
+
23
+ if "active_sources" not in st.session_state:
24
+ st.session_state.active_sources = None
25
+
26
+
27
+ # 🗂️ SIDEBAR: Workspace Management & File Uploads
28
+ with st.sidebar:
29
+ st.title("⚙️ Workspace Panel")
30
+
31
+ # Display the current isolated session ID
32
+ st.info(f"**Active Session Partition:**\n`{st.session_state.session_id}`")
33
+ st.caption("All document uploads and chat history are securely sandboxed inside this unique session token.")
34
+
35
+ st.markdown("---")
36
+
37
+ # File Uploader Widget
38
+ st.subheader("📥 Ingest Dynamic Context")
39
+ uploaded_file = st.file_uploader(
40
+ "Upload a .txt or .pdf file to train the bot for this session:",
41
+ type=["txt", "pdf"]
42
+ )
43
+
44
+ if uploaded_file is not None:
45
+ if st.button("🚀 Process & Vectorize Document", use_container_width=True):
46
+ with st.spinner("Streaming chunks to NVIDIA Embedding pipeline..."):
47
+ try:
48
+ # Prepare multipart form file payload
49
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
50
+ headers = {"X-Session-ID": st.session_state.session_id}
51
+
52
+ # Call FastAPI dynamic ingestion endpoint
53
+ response = requests.post(
54
+ f"{BACKEND_URL}/api/v1/upload",
55
+ files=files,
56
+ headers=headers
57
+ )
58
+
59
+ if response.status_code == 200:
60
+ st.success(f"✅ Context parsed! {uploaded_file.name} is now live in your workspace.")
61
+ else:
62
+ error_detail = response.json().get('detail', 'Unknown error')
63
+ st.error(f"❌ Ingestion Failed: {error_detail}")
64
+ except Exception as e:
65
+ st.error(f"❌ Connection error: {str(e)}")
66
+
67
+ st.markdown("---")
68
+
69
+ # Reset/Clear Options
70
+ st.subheader("🧹 Workspace Cleanup")
71
+ if st.button("Clear Chat Window State", use_container_width=True):
72
+ st.session_state.messages = []
73
+ st.session_state.active_sources = None
74
+ st.rerun()
75
+
76
+ if st.button("🗑️ Wipe Global Vector DB (Admin)", use_container_width=True, type="secondary"):
77
+ with st.spinner("Dropping collection..."):
78
+ try:
79
+ res = requests.post(f"{BACKEND_URL}/api/v1/clear")
80
+ if res.status_code == 200:
81
+ st.warning("💥 Global Vector Store wiped entirely.")
82
+ else:
83
+ st.error("Failed to clear DB.")
84
+ except Exception as e:
85
+ st.error(f"Error: {e}")
86
+
87
+
88
+ # 💬 MAIN INTERFACE: Streaming Chat Engine
89
+ st.title("🤖 Enterprise Multi-Tenant FAQ Bot")
90
+ st.markdown("Ask general questions, standard FAQs, or query details out of your uploaded dynamic documents.")
91
+
92
+ # 📜 Render Existing Chat Log
93
+ for message in st.session_state.messages:
94
+ with st.chat_message(message["role"]):
95
+ st.markdown(message["content"])
96
+
97
+ # ⚡ Live User Interaction Loop
98
+ if prompt := st.chat_input("Type your message here..."):
99
+ # Display user input inside the panel
100
+ with st.chat_message("user"):
101
+ st.markdown(prompt)
102
+
103
+ # Store message in state history
104
+ st.session_state.messages.append({"role": "user", "content": prompt})
105
+
106
+ # Build a clean conversion format payload for history matching
107
+ formatted_history = [
108
+ {"role": msg["role"], "role": "assistant" if msg["role"] == "assistant" else "user", "content": msg["content"]}
109
+ for msg in st.session_state.messages[:-1]
110
+ ]
111
+
112
+ # Setup the JSON request body
113
+ chat_payload = {
114
+ "question": prompt,
115
+ "history": formatted_history,
116
+ "session_id": st.session_state.session_id
117
+ }
118
+
119
+ # Stream the incoming chunks from FastAPI
120
+ with st.chat_message("assistant"):
121
+ response_placeholder = st.empty()
122
+ full_response = ""
123
+ sources_found = []
124
+
125
+ try:
126
+ # Open persistent stream connection to FastAPI router
127
+ with requests.post(f"{BACKEND_URL}/api/v1/chat", json=chat_payload, stream=True) as response:
128
+ if response.status_code == 500:
129
+ st.error("The streaming backend pipeline returned a fatal exception.")
130
+
131
+ for line in response.iter_lines():
132
+ if line:
133
+ # Decode raw byte lines from incoming NDJSON data stream
134
+ decoded_line = line.decode('utf-8')
135
+ chunk_data = json.loads(decoded_line)
136
+
137
+ # 📦 Handle Retrieved Data Sources
138
+ if chunk_data.get("type") == "sources":
139
+ sources_found = chunk_data.get("content", [])
140
+
141
+ # ⚡ Handle Live Text Tokens
142
+ elif chunk_data.get("type") == "token":
143
+ full_response += chunk_data.get("content", "")
144
+ # Live redraw token progression
145
+ response_placeholder.markdown(full_response + "▌")
146
+
147
+ # Lock final markdown block state rendering without cursor character
148
+ response_placeholder.markdown(full_response)
149
+
150
+ # Render Source Documents in an Accordion if present
151
+ if sources_found:
152
+ with st.expander("📚 View Retrieved Reference Sources"):
153
+ for idx, src in enumerate(sources_found):
154
+ src_name = src.get("metadata", {}).get("source", "Global Base FAQ")
155
+ st.markdown(f"**Source [{idx+1}]:** `{src_name}`")
156
+ st.caption(src.get("content", ""))
157
+
158
+ # Save assistant milestone string state safely
159
+ st.session_state.messages.append({"role": "assistant", "content": full_response})
160
+
161
+ except Exception as conn_err:
162
+ st.error(f"Streaming error or dropped server connection: {str(conn_err)}")
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn>=0.31.0
3
+ streamlit>=1.35.0
4
+ python-multipart>=0.0.9
5
+ langchain>=0.3.0
6
+ langchain-openai>=0.2.0
7
+ langchain-nvidia-ai-endpoints>=0.1.0
8
+ chromadb>=0.5.10
9
+ pydantic>=2.9.0
10
+ python-dotenv>=1.0.1
11
+ pypdf>=3.20.0
12
+ langchain-community>=0.3.0
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (179 Bytes). View file
 
src/__pycache__/database.cpython-314.pyc ADDED
Binary file (8.9 kB). View file
 
src/__pycache__/engine.cpython-314.pyc ADDED
Binary file (2.55 kB). View file
 
src/__pycache__/main.cpython-314.pyc ADDED
Binary file (9.08 kB). View file
 
src/database.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from openai import OpenAI
4
+ from langchain_core.documents import Document
5
+ from langchain_core.embeddings import Embeddings
6
+ from langchain_community.vectorstores import Chroma
7
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
8
+
9
+ class NvidiaCompatibleEmbeddings(Embeddings):
10
+ """
11
+ Custom embedding processor that bypasses LangChain's internal tokenization middleware
12
+ to pass raw text strings and asymmetric type configurations directly to NVIDIA NIM.
13
+ """
14
+ def __init__(self, model: str, api_key: str, base_url: str):
15
+ self.client = OpenAI(api_key=api_key, base_url=base_url)
16
+ self.model = model
17
+
18
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
19
+ """Embeds a list of documentation chunks using the 'passage' type."""
20
+ response = self.client.embeddings.create(
21
+ input=texts,
22
+ model=self.model,
23
+ extra_body={"input_type": "passage"}
24
+ )
25
+ return [item.embedding for item in response.data]
26
+
27
+ def embed_query(self, text: str) -> list[float]:
28
+ """Embeds a live user search query using the 'query' type."""
29
+ response = self.client.embeddings.create(
30
+ input=[text],
31
+ model=self.model,
32
+ extra_body={"input_type": "query"}
33
+ )
34
+ return response.data[0].embedding
35
+
36
+
37
+ class VectorDBManager:
38
+ def __init__(self, persist_directory: str = "./chroma_db"):
39
+ self.embeddings = NvidiaCompatibleEmbeddings(
40
+ model=os.getenv("EMBEDDING_MODEL", "nvidia/llama-nemotron-embed-1b-v2"),
41
+ api_key=os.getenv("NVIDIA_API_KEY"),
42
+ base_url=os.getenv("NVIDIA_BASE_URL")
43
+ )
44
+ self.persist_directory = persist_directory
45
+ self.vector_store = None
46
+
47
+ def _ensure_vector_store(self):
48
+ """🛡️ Internal safeguard to ensure the Chroma instance is actively loaded in memory."""
49
+ if self.vector_store is None:
50
+ self.vector_store = Chroma(
51
+ persist_directory=self.persist_directory,
52
+ embedding_function=self.embeddings
53
+ )
54
+
55
+ def initialize_db(self, faq_filepath: str):
56
+ """
57
+ Loads incoming JSON FAQs, formats them into searchable LangChain Documents,
58
+ and saves them locally via ChromaDB stamped with a 'global' scope.
59
+ """
60
+ if not os.path.exists(faq_filepath):
61
+ raise FileNotFoundError(f"Could not find FAQ data resource file at: {faq_filepath}")
62
+
63
+ with open(faq_filepath, 'r') as f:
64
+ faq_data = json.load(f)
65
+
66
+ documents = []
67
+ for item in faq_data:
68
+ page_content = f"Question: {item['question']}\nAnswer: {item['answer']}"
69
+
70
+ # 🔑 Added session_id: "global" so these base FAQs are accessible to all users
71
+ metadata = {
72
+ "category": item["category"],
73
+ "faq_id": item["id"],
74
+ "session_id": "global"
75
+ }
76
+ documents.append(Document(page_content=page_content, metadata=metadata))
77
+
78
+ self.vector_store = Chroma.from_documents(
79
+ documents=documents,
80
+ embedding=self.embeddings,
81
+ persist_directory=self.persist_directory
82
+ )
83
+ print(f"🚀 Vector DB successfully initialized with {len(documents)} FAQs via NVIDIA Embeddings.")
84
+
85
+ def get_retriever(self, session_id: str = "default_session"):
86
+ """
87
+ Loads the localized database and converts it into a queryable retriever layer
88
+ strictly filtered by the active session identity.
89
+ """
90
+ self._ensure_vector_store()
91
+
92
+ # 🔑 MULTI-TENANCY FILTER: Look for global baseline data OR this specific user's uploads
93
+ meta_filter = {
94
+ "$or": [
95
+ {"session_id": "global"},
96
+ {"session_id": session_id}
97
+ ]
98
+ }
99
+
100
+ return self.vector_store.as_retriever(
101
+ search_kwargs={
102
+ "k": 2,
103
+ "filter": meta_filter
104
+ }
105
+ )
106
+
107
+ def add_text_to_db(self, text: str, filename: str, session_id: str = "default_session"):
108
+ """
109
+ 📥 Chunks raw text from dynamic frontend uploads, computes NVIDIA embeddings,
110
+ and appends them directly into the live database marked with the owner's session ID.
111
+ """
112
+ self._ensure_vector_store()
113
+
114
+ # 1. Break the document down into semantic pieces
115
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=60)
116
+ chunks = text_splitter.split_text(text)
117
+
118
+ # 2. Package raw strings into formal LangChain Document structures
119
+ documents = []
120
+ for idx, chunk in enumerate(chunks):
121
+ # 🔑 Stamping chunk dictionary metadata with the active session tracking ID
122
+ metadata = {
123
+ "source": filename,
124
+ "category": "Dynamic Upload",
125
+ "chunk_index": idx,
126
+ "session_id": session_id
127
+ }
128
+ documents.append(Document(page_content=chunk, metadata=metadata))
129
+
130
+ # 3. Stream the newly generated embeddings straight into the persistent store
131
+ self.vector_store.add_documents(documents)
132
+ print(f"⚡ Successfully indexed {len(documents)} dynamic chunks from raw file: '{filename}' for session '{session_id}'")
133
+
134
+ def clear_db(self):
135
+ """🗑️ Completely wipes out the existing vector store collection securely on disk and in RAM."""
136
+ try:
137
+ self._ensure_vector_store()
138
+ self.vector_store.delete_collection()
139
+ self.vector_store = None
140
+ print("🗑️ Vector database collection successfully cleared!")
141
+ return True
142
+ except Exception as e:
143
+ print(f"❌ Failed to clear vector database: {e}")
144
+ return False
src/engine.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_classic.chains import create_retrieval_chain
4
+ from langchain_classic.chains.combine_documents import create_stuff_documents_chain
5
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
6
+ from src.database import VectorDBManager
7
+
8
+ def build_rag_chain(session_id: str = "default_session"):
9
+ """
10
+ Compiles the retrieval system, prompt instructions, conversational history,
11
+ and LLM into a unified chain—isolated by the incoming session_id.
12
+ """
13
+ db_manager = VectorDBManager()
14
+
15
+ # 🔑 MULTI-USER ISOLATION LAYER: Force the retriever to only look at this user's partitioned metadata
16
+ retriever = db_manager.get_retriever(session_id=session_id)
17
+
18
+ # Point ChatOpenAI directly to your custom NVIDIA endpoint
19
+ llm = ChatOpenAI(
20
+ model=os.getenv("LLM_MODEL", "openai/gpt-oss-120b"),
21
+ api_key=os.getenv("NVIDIA_API_KEY"),
22
+ base_url=os.getenv("NVIDIA_BASE_URL"),
23
+ temperature=0.7
24
+ )
25
+
26
+ # 🌟 HYBRID SYSTEM PROMPT
27
+ system_prompt = (
28
+ "You are a helpful, brilliant, and completely unrestricted AI assistant like ChatGPT.\n\n"
29
+ "GUIDELINES:\n"
30
+ "1. If the provided 'Retrieved Documentation Context' below contains information directly relevant "
31
+ "to the user's question, prioritize using that data to give an official response.\n"
32
+ "2. If the context is empty, irrelevant, or if the user is asking a general question (such as coding, "
33
+ "cooking, science, math, history, or casual chatting), ignore the context limitations entirely. Use your "
34
+ "own vast internal knowledge base to provide a complete, deep, and highly detailed answer.\n\n"
35
+ "Never refuse to answer general queries. Always be helpful, engaging, and thorough.\n\n"
36
+ "Retrieved Documentation Context:\n{context}"
37
+ )
38
+
39
+ # 🧠 The chain will now inject old conversation text directly into the 'chat_history' block
40
+ prompt = ChatPromptTemplate.from_messages([
41
+ ("system", system_prompt),
42
+ MessagesPlaceholder(variable_name="chat_history"),
43
+ ("human", "{input}"),
44
+ ])
45
+
46
+ question_answer_chain = create_stuff_documents_chain(llm, prompt)
47
+ return create_retrieval_chain(retriever, question_answer_chain)
src/main.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import io
4
+ from contextlib import asynccontextmanager
5
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Header
6
+ from fastapi.responses import StreamingResponse
7
+ from pydantic import BaseModel
8
+ from typing import List, Dict, Optional
9
+ from dotenv import load_dotenv
10
+ from pypdf import PdfReader
11
+ from langchain_nvidia_ai_endpoints import ChatNVIDIA
12
+ from langchain_core.messages import HumanMessage, AIMessage
13
+ from src.database import VectorDBManager
14
+ from src.engine import build_rag_chain
15
+
16
+ load_dotenv()
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ if not os.path.exists("./chroma_db"):
21
+ print("Initializing Vector Database...")
22
+ db_manager = VectorDBManager()
23
+ db_manager.initialize_db("data/faqs.json")
24
+ yield
25
+
26
+ app = FastAPI(title="Enterprise FAQ Bot Engine", version="1.0", lifespan=lifespan)
27
+
28
+ class QueryRequest(BaseModel):
29
+ question: str
30
+ history: Optional[List[Dict[str, str]]] = []
31
+ session_id: Optional[str] = "default_session"
32
+
33
+
34
+ # 📂 DYNAMIC KNOWLEDGE INGESTION ENDPOINT
35
+ @app.post("/api/v1/upload")
36
+ async def upload_file_endpoint(
37
+ file: UploadFile = File(...),
38
+ x_session_id: Optional[str] = Header(None)
39
+ ):
40
+ """
41
+ Accepts standard multipart form file uploads (.txt or .pdf), extracts raw text
42
+ contents in-memory, and passes them along with a unique session ID.
43
+ """
44
+ try:
45
+ contents = await file.read()
46
+ filename = file.filename.lower()
47
+ text_data = ""
48
+
49
+ # 📄 Process Plain Text Files
50
+ if filename.endswith(".txt"):
51
+ text_data = contents.decode("utf-8")
52
+
53
+ # 📕 Process PDF Files In-Memory
54
+ elif filename.endswith(".pdf"):
55
+ pdf_stream = io.BytesIO(contents)
56
+ pdf_reader = PdfReader(pdf_stream)
57
+
58
+ extracted_pages = []
59
+ for page in pdf_reader.pages:
60
+ page_text = page.extract_text()
61
+ if page_text:
62
+ extracted_pages.append(page_text)
63
+
64
+ text_data = "\n".join(extracted_pages)
65
+
66
+ if not text_data.strip():
67
+ raise HTTPException(
68
+ status_code=400,
69
+ detail="The uploaded PDF appears to be empty or contains only non-scanned imagery (OCR required)."
70
+ )
71
+ else:
72
+ raise HTTPException(
73
+ status_code=400,
74
+ detail="Unsupported file format. Please upload a valid plain text (.txt) or PDF (.pdf) document."
75
+ )
76
+
77
+ db_manager = VectorDBManager()
78
+ target_session = x_session_id or "default_session"
79
+ db_manager.add_text_to_db(text_data, filename=file.filename, session_id=target_session)
80
+
81
+ return {
82
+ "status": "success",
83
+ "message": f"Successfully vectorized and stored {file.filename} under session context!"
84
+ }
85
+
86
+ except UnicodeDecodeError:
87
+ raise HTTPException(
88
+ status_code=400,
89
+ detail="File encoding error: Please ensure your text file is saved with valid UTF-8 encoding."
90
+ )
91
+ except HTTPException as http_ex:
92
+ raise http_ex
93
+ except Exception as e:
94
+ raise HTTPException(status_code=500, detail=f"Ingestion pipeline failed: {str(e)}")
95
+
96
+
97
+ # 🗑️ ADMIN DATABASE RESET ENDPOINT
98
+ @app.post("/api/v1/clear")
99
+ async def clear_database_endpoint():
100
+ """
101
+ Triggers a collection wipe on the vector database.
102
+ """
103
+ db_manager = VectorDBManager()
104
+ if db_manager.clear_db():
105
+ return {"status": "success", "message": "Vector database cleared successfully."}
106
+ else:
107
+ raise HTTPException(status_code=500, detail="Failed to drop vector database collection.")
108
+
109
+
110
+ # ⚡ LIVE STREAMING CHAT ROUTER
111
+ @app.post("/api/v1/chat")
112
+ async def chat_endpoint(payload: QueryRequest):
113
+ try:
114
+ search_query = payload.question
115
+ chat_history = []
116
+
117
+ # Format the conversational history state if it exists
118
+ if payload.history and len(payload.history) > 0:
119
+ for msg in payload.history:
120
+ if msg["role"] == "user":
121
+ chat_history.append(HumanMessage(content=msg["content"]))
122
+ elif msg["role"] == "assistant":
123
+ chat_history.append(AIMessage(content=msg["content"]))
124
+
125
+ # ✨ RESTORED & ALIGNED MEMORY CONTEXT BLOCK
126
+ try:
127
+ rephrase_llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct")
128
+ history_context = ""
129
+ for msg in payload.history:
130
+ role = "User" if msg["role"] == "user" else "Assistant"
131
+ history_context += f"{role}: {msg['content']}\n"
132
+
133
+ condense_prompt = (
134
+ f"Given the following chat history and a follow-up question, "
135
+ f"rephrase the follow-up question into a standalone query.\n\n"
136
+ f"Chat History:\n{history_context}\n"
137
+ f"Follow-up Question: {payload.question}\n\n"
138
+ f"Standalone Query:"
139
+ )
140
+
141
+ # Execute standard LangChain invocation
142
+ llm_response = rephrase_llm.invoke(condense_prompt)
143
+ search_query = llm_response.content.strip()
144
+
145
+ except Exception as context_error:
146
+ print(f"⚠️ Memory rephrasing failed: {context_error}")
147
+ search_query = payload.question
148
+
149
+ # Build execution pipeline tied strictly to this user's workspace partitions
150
+ rag_chain = build_rag_chain(session_id=payload.session_id)
151
+
152
+ async def event_generator():
153
+ async for chunk in rag_chain.astream({"input": search_query, "chat_history": chat_history}):
154
+ if "context" in chunk:
155
+ sources_payload = [
156
+ {"content": doc.page_content, "metadata": doc.metadata}
157
+ for doc in chunk["context"]
158
+ ]
159
+ yield json.dumps({"type": "sources", "content": sources_payload}) + "\n"
160
+
161
+ if "answer" in chunk:
162
+ yield json.dumps({"type": "token", "content": chunk["answer"]}) + "\n"
163
+
164
+ return StreamingResponse(event_generator(), media_type="application/x-ndjson")
165
+
166
+ except Exception as e:
167
+ raise HTTPException(status_code=500, detail=f"Chat streaming pipeline failed: {str(e)}")
start.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 1. Start the FastAPI backend in the background on port 8000
4
+ echo "Starting FastAPI backend..."
5
+ uvicorn src.main:app --host 0.0.0.0 --port 8000 &
6
+
7
+ # 2. Wait a brief moment for the backend to initialize
8
+ sleep 3
9
+
10
+ # 3. Start the Streamlit frontend in the foreground on Hugging Face's required port (7860)
11
+ echo "Starting Streamlit frontend..."
12
+ streamlit run frontend.py --server.port 7860 --server.address 0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false