testtest123 commited on
Commit
bf7338b
·
1 Parent(s): 1b08040

Fix file upload UI refresh and add Metadata Filters UI

Browse files
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build frontend
2
+ FROM node:20-alpine AS frontend-builder
3
+ WORKDIR /frontend
4
+ COPY RAG_FULL_APPLICATION_FRONTEND/package*.json ./
5
+ RUN npm install
6
+ COPY RAG_FULL_APPLICATION_FRONTEND/ ./
7
+ RUN npm run build
8
+
9
+ # Stage 2: Build backend & final monolithic image
10
+ FROM python:3.12-slim
11
+ WORKDIR /app
12
+
13
+ # System dependencies for python-docx, tiktoken, Tesseract OCR, etc.
14
+ RUN apt-get update && apt-get install -y \
15
+ build-essential libpq-dev tesseract-ocr libmagic1 libgl1 && \
16
+ rm -rf /var/lib/apt/lists/*
17
+
18
+ COPY RAG_FULL_APPLICATION_BACKEND/requirements.txt ./
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Download cross-encoder model at build time to avoid slow cold start
22
+ RUN python -c "from sentence_transformers import CrossEncoder; \
23
+ CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')"
24
+
25
+ # Copy backend files
26
+ COPY RAG_FULL_APPLICATION_BACKEND/ ./
27
+
28
+ # Copy built frontend assets to the static directory
29
+ COPY --from=frontend-builder /frontend/dist ./static
30
+
31
+ # Create data directories and set permissions for Hugging Face non-root user (UID 1000)
32
+ RUN mkdir -p data/uploads data/bm25_indexes data/cache && \
33
+ chown -R 1000:1000 /app
34
+
35
+ EXPOSE 7860
36
+
37
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
RAG_FULL_APPLICATION_BACKEND/Dockerfile CHANGED
@@ -19,6 +19,6 @@ COPY . .
19
  # Create data dirs
20
  RUN mkdir -p data/uploads data/bm25_indexes data/cache
21
 
22
- EXPOSE 8000
23
 
24
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
 
19
  # Create data dirs
20
  RUN mkdir -p data/uploads data/bm25_indexes data/cache
21
 
22
+ EXPOSE 7860
23
 
24
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
RAG_FULL_APPLICATION_BACKEND/README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: RAG Backend
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # RAG Multimodal Pipeline Backend
12
+ This is the backend API for the JEE/NEET RAG pipeline, hosted on Hugging Face Spaces.
13
+
14
+ ## Configuration
15
+ All environment variables are managed via Hugging Face Secrets.
16
+
17
+ ## Tech Stack
18
+ - FastAPI
19
+ - Uvicorn
20
+ - Docker
21
+ - Sentence Transformers
22
+ - Redis (External via Upstash)
23
+ - Supabase (External)
RAG_FULL_APPLICATION_BACKEND/app/config.py CHANGED
@@ -31,7 +31,7 @@ class Settings(BaseSettings):
31
  VISION_SPACE_URL: str = "Qwen/Qwen3-VL-30B-A3B-Demo"
32
 
33
  # Redis
34
- REDIS_URL: str
35
  CACHE_TTL_SECONDS: int = 3600
36
 
37
  # Auth
@@ -53,7 +53,7 @@ class Settings(BaseSettings):
53
  MAX_FILE_SIZE_MB: int = 50
54
 
55
  # CORS
56
- CORS_ORIGINS: str
57
 
58
  class Config:
59
  env_file = ".env"
 
31
  VISION_SPACE_URL: str = "Qwen/Qwen3-VL-30B-A3B-Demo"
32
 
33
  # Redis
34
+ REDIS_URL: str = ""
35
  CACHE_TTL_SECONDS: int = 3600
36
 
37
  # Auth
 
53
  MAX_FILE_SIZE_MB: int = 50
54
 
55
  # CORS
56
+ CORS_ORIGINS: str = ""
57
 
58
  class Config:
59
  env_file = ".env"
RAG_FULL_APPLICATION_BACKEND/app/main.py CHANGED
@@ -1,8 +1,11 @@
1
  from fastapi import FastAPI, WebSocket, Depends
2
  from fastapi.middleware.cors import CORSMiddleware
 
 
3
  from .config import settings
4
  from .utils.ws_manager import ws_manager
5
  import logging
 
6
 
7
  # Setup Logger
8
  logging.basicConfig(level=logging.INFO)
@@ -11,7 +14,8 @@ logger = logging.getLogger(__name__)
11
  app = FastAPI(title="RAG Pipeline API", version="3.0.0")
12
 
13
  # CORS
14
- origins = settings.CORS_ORIGINS.split(",")
 
15
  app.add_middleware(
16
  CORSMiddleware,
17
  allow_origins=origins,
@@ -44,3 +48,17 @@ async def pipeline_ws(websocket: WebSocket, job_id: str, token: str):
44
  logger.error(f"WebSocket error for job {job_id}: {e}")
45
  finally:
46
  await ws_manager.disconnect(job_id, "anonymous")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, WebSocket, Depends
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
  from .config import settings
6
  from .utils.ws_manager import ws_manager
7
  import logging
8
+ import os
9
 
10
  # Setup Logger
11
  logging.basicConfig(level=logging.INFO)
 
14
  app = FastAPI(title="RAG Pipeline API", version="3.0.0")
15
 
16
  # CORS
17
+ # origins = settings.CORS_ORIGINS.split(",")
18
+ origins = settings.CORS_ORIGINS.split(",") if settings.CORS_ORIGINS else ["*"]
19
  app.add_middleware(
20
  CORSMiddleware,
21
  allow_origins=origins,
 
48
  logger.error(f"WebSocket error for job {job_id}: {e}")
49
  finally:
50
  await ws_manager.disconnect(job_id, "anonymous")
51
+
52
+ # Serve frontend static files in production monolith
53
+ static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "static")
54
+ if os.path.exists(static_dir):
55
+ app.mount("/assets", StaticFiles(directory=os.path.join(static_dir, "assets")), name="assets")
56
+
57
+ @app.get("/{catchall:path}")
58
+ async def serve_frontend(catchall: str):
59
+ # Prevent catching API calls
60
+ if catchall.startswith(("auth", "ingest", "query", "health", "ws")):
61
+ return None
62
+ index_file = os.path.join(static_dir, "index.html")
63
+ if os.path.exists(index_file):
64
+ return FileResponse(index_file)
RAG_FULL_APPLICATION_BACKEND/app/routers/auth.py CHANGED
@@ -46,12 +46,10 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
46
 
47
  @router.post("/seed_admin")
48
  async def seed_admin():
49
- """Utility to pre-create admin user for local testing. Forced clean sync."""
50
- hashed = get_password_hash("admin123")
51
  try:
52
- # Delete existing to ensure fresh hash if environment changed
53
  supabase_service.client.table("users").delete().eq("username", "admin").execute()
54
-
55
  supabase_service.client.table("users").insert({
56
  "username": "admin",
57
  "password_hash": hashed
@@ -60,4 +58,4 @@ async def seed_admin():
60
  return {"msg": "Admin user created/reset (admin / admin123)"}
61
  except Exception as e:
62
  logger.error(f"Seeding failed: {e}")
63
- return {"msg": f"Seeding failed: {str(e)}"}
 
46
 
47
  @router.post("/seed_admin")
48
  async def seed_admin():
49
+ import traceback
 
50
  try:
51
+ hashed = get_password_hash("admin123")
52
  supabase_service.client.table("users").delete().eq("username", "admin").execute()
 
53
  supabase_service.client.table("users").insert({
54
  "username": "admin",
55
  "password_hash": hashed
 
58
  return {"msg": "Admin user created/reset (admin / admin123)"}
59
  except Exception as e:
60
  logger.error(f"Seeding failed: {e}")
61
+ return {"error": str(e), "traceback": traceback.format_exc()}
RAG_FULL_APPLICATION_BACKEND/app/techniques/metadata_filter.py CHANGED
@@ -6,6 +6,10 @@ class MetadataFilter(BaseRAGTechnique):
6
  async def retrieve(self, query: str, document_id: str, top_k: int, **kwargs) -> List[Dict[str, Any]]:
7
  filters = kwargs.get("filters", {})
8
 
 
 
 
 
9
  # 1. SQL Pre-filtering
10
  await self.emit("FILTER", "#D97706", f"SQL filter: {filters}...")
11
  matching_ids = await self.supabase.filter_chunk_ids(document_id, self.user_id, filters)
 
6
  async def retrieve(self, query: str, document_id: str, top_k: int, **kwargs) -> List[Dict[str, Any]]:
7
  filters = kwargs.get("filters", {})
8
 
9
+ if not filters:
10
+ await self.emit("DONE", "#EF4444", "Metadata Filter selected but no filters provided.")
11
+ return []
12
+
13
  # 1. SQL Pre-filtering
14
  await self.emit("FILTER", "#D97706", f"SQL filter: {filters}...")
15
  matching_ids = await self.supabase.filter_chunk_ids(document_id, self.user_id, filters)
RAG_FULL_APPLICATION_BACKEND/requirements.txt CHANGED
@@ -7,6 +7,7 @@ asyncpg==0.29.0
7
  redis==5.0.6
8
  python-jose[cryptography]==3.3.0
9
  passlib[bcrypt]==1.7.4
 
10
  python-multipart==0.0.9
11
  httpx==0.27.0
12
  gradio_client>=1.0.0
 
7
  redis==5.0.6
8
  python-jose[cryptography]==3.3.0
9
  passlib[bcrypt]==1.7.4
10
+ bcrypt==4.0.1
11
  python-multipart==0.0.9
12
  httpx==0.27.0
13
  gradio_client>=1.0.0
RAG_FULL_APPLICATION_FRONTEND/src/App.jsx CHANGED
@@ -15,7 +15,7 @@ function App() {
15
  const [password, setPassword] = useState('admin123');
16
  const [query, setQuery] = useState('');
17
  const [technique, setTechnique] = useState('hybrid');
18
- const [activeJob, setActiveJob] = useState(null);
19
 
20
  const {
21
  documents, setDocuments,
@@ -23,7 +23,8 @@ function App() {
23
  currentAnswer, setAnswer,
24
  sources, setSources,
25
  steps, addStep, clearSteps,
26
- setQuerying, isQuerying
 
27
  } = usePipelineStore();
28
 
29
  const ws = useRef(null);
@@ -39,7 +40,7 @@ function App() {
39
  useEffect(() => {
40
  if (activeJob && isAuthenticated) {
41
  const token = sessionStorage.getItem('token');
42
- const apiBase = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001';
43
  const wsProtocol = apiBase.startsWith('https') ? 'wss' : 'ws';
44
  const wsHost = apiBase.replace(/^https?:\/\//, '');
45
  const wsUrl = `${wsProtocol}://${wsHost}/ws/pipeline/${activeJob}?token=${token}`;
@@ -82,17 +83,29 @@ function App() {
82
  setShowSettings(false);
83
 
84
  try {
85
- const { data } = await api.post('/query/search', {
86
  query,
87
  document_id: selectedDoc.id,
88
  technique
89
- });
 
 
 
 
 
 
 
 
 
 
 
 
90
  setAnswer(data.answer);
91
  setSources(data.sources);
92
  setActiveJob(data.job_id);
93
  } catch (error) {
94
  console.error('Search failed', error);
95
- addStep({ step: 'ERROR', detail: 'Search failed. Please try again.', color: '#EF4444' });
96
  } finally {
97
  setQuerying(false);
98
  }
@@ -271,6 +284,19 @@ function App() {
271
  <label className="text-[10px] text-gray-600 font-bold uppercase">LLM Temp</label>
272
  <input type="number" defaultValue={0.1} step={0.1} className="w-full bg-surface-800 border border-surface-700 rounded px-2 py-1 text-xs outline-none focus:border-accent-500" />
273
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  </div>
275
  </motion.div>
276
  )}
 
15
  const [password, setPassword] = useState('admin123');
16
  const [query, setQuery] = useState('');
17
  const [technique, setTechnique] = useState('hybrid');
18
+ const [metadataFilters, setMetadataFilters] = useState('{}');
19
 
20
  const {
21
  documents, setDocuments,
 
23
  currentAnswer, setAnswer,
24
  sources, setSources,
25
  steps, addStep, clearSteps,
26
+ setQuerying, isQuerying,
27
+ activeJob, setActiveJob
28
  } = usePipelineStore();
29
 
30
  const ws = useRef(null);
 
40
  useEffect(() => {
41
  if (activeJob && isAuthenticated) {
42
  const token = sessionStorage.getItem('token');
43
+ const apiBase = import.meta.env.VITE_API_BASE_URL || window.location.origin;
44
  const wsProtocol = apiBase.startsWith('https') ? 'wss' : 'ws';
45
  const wsHost = apiBase.replace(/^https?:\/\//, '');
46
  const wsUrl = `${wsProtocol}://${wsHost}/ws/pipeline/${activeJob}?token=${token}`;
 
83
  setShowSettings(false);
84
 
85
  try {
86
+ const payload = {
87
  query,
88
  document_id: selectedDoc.id,
89
  technique
90
+ };
91
+
92
+ if (technique === 'meta') {
93
+ try {
94
+ payload.filters = JSON.parse(metadataFilters);
95
+ } catch (e) {
96
+ alert("Invalid JSON format for Metadata Filters. Please check your input.");
97
+ setQuerying(false);
98
+ return;
99
+ }
100
+ }
101
+
102
+ const { data } = await api.post('/query/search', payload);
103
  setAnswer(data.answer);
104
  setSources(data.sources);
105
  setActiveJob(data.job_id);
106
  } catch (error) {
107
  console.error('Search failed', error);
108
+ addStep({ step: 'ERROR', detail: error?.response?.data?.detail || 'Search failed. Please try again.', color: '#EF4444' });
109
  } finally {
110
  setQuerying(false);
111
  }
 
284
  <label className="text-[10px] text-gray-600 font-bold uppercase">LLM Temp</label>
285
  <input type="number" defaultValue={0.1} step={0.1} className="w-full bg-surface-800 border border-surface-700 rounded px-2 py-1 text-xs outline-none focus:border-accent-500" />
286
  </div>
287
+ <div className="col-span-2 space-y-1 pt-2 border-t border-surface-800">
288
+ <label className="text-[10px] text-gray-600 font-bold uppercase flex justify-between">
289
+ <span>Metadata Filters (JSON)</span>
290
+ {technique === 'meta' && <span className="text-orange-500">Required for Meta search</span>}
291
+ </label>
292
+ <input
293
+ type="text"
294
+ value={metadataFilters}
295
+ onChange={e => setMetadataFilters(e.target.value)}
296
+ placeholder='e.g., {"page": 1, "section": "Introduction"}'
297
+ className={`w-full bg-surface-800 border ${technique === 'meta' ? 'border-orange-500/50 focus:border-orange-500' : 'border-surface-700 focus:border-accent-500'} rounded px-2 py-1 text-xs outline-none font-mono text-gray-400`}
298
+ />
299
+ </div>
300
  </div>
301
  </motion.div>
302
  )}
RAG_FULL_APPLICATION_FRONTEND/src/api/client.js CHANGED
@@ -1,7 +1,7 @@
1
  import axios from 'axios';
2
 
3
  const api = axios.create({
4
- baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001',
5
  });
6
 
7
  // Interceptor for JWT
 
1
  import axios from 'axios';
2
 
3
  const api = axios.create({
4
+ baseURL: import.meta.env.VITE_API_BASE_URL || window.location.origin,
5
  });
6
 
7
  // Interceptor for JWT
RAG_FULL_APPLICATION_FRONTEND/src/components/FileUpload.jsx CHANGED
@@ -14,7 +14,7 @@ const STRATEGIES = [
14
 
15
  export default function FileUpload() {
16
  const [file, setFile] = useState(null);
17
- const { setIngesting, isIngesting } = usePipelineStore();
18
  const [status, setStatus] = useState('idle'); // idle | loading | success | error
19
  const [errorMsg, setErrorMsg] = useState('');
20
 
@@ -36,7 +36,9 @@ export default function FileUpload() {
36
  formData.append('strategy', strategy);
37
 
38
  try {
 
39
  const { data } = await api.post('/ingest/upload', formData);
 
40
  setStatus('success');
41
  setTimeout(() => {
42
  setStatus('idle');
 
14
 
15
  export default function FileUpload() {
16
  const [file, setFile] = useState(null);
17
+ const { setIngesting, isIngesting, setActiveJob, clearSteps } = usePipelineStore();
18
  const [status, setStatus] = useState('idle'); // idle | loading | success | error
19
  const [errorMsg, setErrorMsg] = useState('');
20
 
 
36
  formData.append('strategy', strategy);
37
 
38
  try {
39
+ clearSteps();
40
  const { data } = await api.post('/ingest/upload', formData);
41
+ setActiveJob(data.job_id);
42
  setStatus('success');
43
  setTimeout(() => {
44
  setStatus('idle');
RAG_FULL_APPLICATION_FRONTEND/src/store/pipelineStore.js CHANGED
@@ -8,6 +8,7 @@ export const usePipelineStore = create((set) => ({
8
  steps: [],
9
  isQuerying: false,
10
  isIngesting: false,
 
11
 
12
  setDocuments: (documents) => set({ documents }),
13
  setSelectedDoc: (selectedDoc) => set({ selectedDoc }),
@@ -17,4 +18,5 @@ export const usePipelineStore = create((set) => ({
17
  clearSteps: () => set({ steps: [] }),
18
  setQuerying: (isQuerying) => set({ isQuerying }),
19
  setIngesting: (isIngesting) => set({ isIngesting }),
 
20
  }));
 
8
  steps: [],
9
  isQuerying: false,
10
  isIngesting: false,
11
+ activeJob: null,
12
 
13
  setDocuments: (documents) => set({ documents }),
14
  setSelectedDoc: (selectedDoc) => set({ selectedDoc }),
 
18
  clearSteps: () => set({ steps: [] }),
19
  setQuerying: (isQuerying) => set({ isQuerying }),
20
  setIngesting: (isIngesting) => set({ isIngesting }),
21
+ setActiveJob: (activeJob) => set({ activeJob }),
22
  }));
README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: RAG Monolith
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # RAG Multimodal Pipeline
12
+
13
+ This repository hosts a monolithic full-stack deployment of the RAG Multimodal Pipeline on Hugging Face Spaces.
14
+
15
+ ## Setup & Secrets
16
+
17
+ Make sure to configure the following Secrets in your Space settings:
18
+ 1. `SUPABASE_URL`
19
+ 2. `SUPABASE_KEY`
20
+ 3. `SUPABASE_DB_URL`
21
+ 4. `REDIS_URL`
22
+ 5. `JWT_SECRET_KEY`
23
+ 6. `CORS_ORIGINS` (value can be `*` or the final Space URL)
patch_auth.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ with open("/Content/AI-PROJECTS/RAG_FULL_APPLICATION/RAG_FULL_APPLICATION_BACKEND/app/routers/auth.py", "r") as f:
4
+ content = f.read()
5
+
6
+ new_seed_admin = """@router.post("/seed_admin")
7
+ async def seed_admin():
8
+ import traceback
9
+ try:
10
+ hashed = get_password_hash("admin123")
11
+ supabase_service.client.table("users").delete().eq("username", "admin").execute()
12
+ supabase_service.client.table("users").insert({
13
+ "username": "admin",
14
+ "password_hash": hashed
15
+ }).execute()
16
+ logger.info("Admin user seeded successfully.")
17
+ return {"msg": "Admin user created/reset (admin / admin123)"}
18
+ except Exception as e:
19
+ logger.error(f"Seeding failed: {e}")
20
+ return {"error": str(e), "traceback": traceback.format_exc()}
21
+ """
22
+
23
+ content = re.sub(r'@router\.post\("/seed_admin"\).*?(?=\n@|\Z)', new_seed_admin, content, flags=re.DOTALL)
24
+
25
+ with open("/Content/AI-PROJECTS/RAG_FULL_APPLICATION/RAG_FULL_APPLICATION_BACKEND/app/routers/auth.py", "w") as f:
26
+ f.write(content)