CHAMATH commited on
Commit
464b72a
Β·
0 Parent(s):

Deploy Space with optional ASR mode

Browse files
.dockerignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VCS
2
+ .git
3
+ .gitignore
4
+
5
+ # Python cache and local environments
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.pyo
9
+ *.pyd
10
+ *.so
11
+ venv/
12
+ .venv/
13
+
14
+ # Local env files
15
+ .env
16
+ .env.*
17
+
18
+ # Editor and notebook
19
+ .vscode/
20
+ *.ipynb
21
+
22
+ # Build and test caches
23
+ .pytest_cache/
24
+ .mypy_cache/
25
+ ruff_cache/
26
+
27
+ # Large local assets not required in container build context
28
+ models/
29
+ final_model/
30
+ rag_data/faiss_index/
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Gemini API Key
2
+ # Get your API key from: https://makersuite.google.com/app/apikey
3
+ GEMINI_API_KEY=AIzaSyC7tkb3uFgmh8YSuOVHYgIDywyL2lzICBA
.gitignore ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # PyInstaller
28
+ *.manifest
29
+ *.spec
30
+
31
+ # Installer logs
32
+ pip-log.txt
33
+ pip-delete-this-directory.txt
34
+
35
+ # Unit test / coverage reports
36
+ htmlcov/
37
+ .tox/
38
+ .coverage
39
+ .coverage.*
40
+ .cache
41
+ nosetests.xml
42
+ coverage.xml
43
+ *.cover
44
+ .hypothesis/
45
+ .pytest_cache/
46
+
47
+ # Translations
48
+ *.mo
49
+ *.pot
50
+
51
+ # Environments
52
+ .env
53
+ .venv
54
+ env/
55
+ venv/
56
+ ENV/
57
+ env.bak/
58
+ venv.bak/
59
+
60
+ # IDE
61
+ .idea/
62
+ .vscode/
63
+ *.swp
64
+ *.swo
65
+ *~
66
+
67
+ # OS
68
+ .DS_Store
69
+ Thumbs.db
70
+
71
+ # Project specific
72
+ *.wav
73
+ *.mp3
74
+ *.webm
75
+ temp/
76
+ logs/
77
+
78
+ # Model cache (can be large)
79
+ models/
80
+ final_model/
81
+ .cache/
82
+
83
+ # RAG binary artifacts (not required at deploy time)
84
+ rag_data/*.pdf
85
+ rag_data/faiss_index/
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PORT=7860
6
+
7
+ WORKDIR /app
8
+
9
+ # System libs for audio and ML dependencies.
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ build-essential \
12
+ ffmpeg \
13
+ libsndfile1 \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ COPY requirements.txt ./
17
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
18
+ pip install --no-cache-dir -r requirements.txt
19
+
20
+ COPY . .
21
+
22
+ EXPOSE 7860
23
+
24
+ CMD ["sh", "-c", "uvicorn app.hf_space:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Sinhala Chatbot
3
+ emoji: "πŸŽ™οΈ"
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Sinhala Chatbot
12
+
13
+ A voice-enabled Sinhala/English chatbot that uses speech recognition, translation, RAG, and text-to-speech.
14
+
15
+ ## Features
16
+
17
+ - Voice recording in Sinhala or English
18
+ - Speech-to-text using Whisper ASR (`seniruk/whisper-small-si`)
19
+ - Translation to English before querying RAG
20
+ - RAG from uploaded PDFs (FAISS + embeddings)
21
+ - AI fallback (Gemini or Hugging Face)
22
+ - Text-to-speech with Google TTS
23
+
24
+ ## Project Structure
25
+
26
+ ```
27
+ chatbot-project-python/
28
+ app/
29
+ __init__.py
30
+ main.py
31
+ admin.py
32
+ rag.py
33
+ static/
34
+ css/style.css
35
+ js/script.js
36
+ templates/
37
+ index.html
38
+ admin.html
39
+ rag_data/
40
+ .env
41
+ .env.example
42
+ requirements.txt
43
+ README.md
44
+ ```
45
+
46
+ ## Prerequisites
47
+
48
+ - Python 3.9+
49
+ - A modern browser (Chrome, Edge, Firefox)
50
+ - Microphone access
51
+ - Gemini API key (optional but recommended)
52
+ - Hugging Face API token (optional fallback)
53
+
54
+ ## Installation
55
+
56
+ ### 1. Create a virtual environment
57
+
58
+ ```bash
59
+ # Windows
60
+ python -m venv venv
61
+ venv\Scripts\activate
62
+
63
+ # macOS/Linux
64
+ python3 -m venv venv
65
+ source venv/bin/activate
66
+ ```
67
+
68
+ ### 2. Install dependencies
69
+
70
+ ```bash
71
+ pip install -r requirements.txt
72
+ ```
73
+
74
+ ### 3. Configure environment variables
75
+
76
+ Copy the example environment file and add your API keys:
77
+
78
+ ```bash
79
+ # Windows
80
+ copy .env.example .env
81
+
82
+ # macOS/Linux
83
+ cp .env.example .env
84
+ ```
85
+
86
+ Edit `.env` and add:
87
+
88
+ ```
89
+ GEMINI_API_KEY=your_gemini_key_here
90
+ HF_API_TOKEN=your_huggingface_token_here
91
+ ```
92
+
93
+ If you do not provide a Gemini key, the app will fall back to the free Hugging Face API.
94
+
95
+ ## Running the Application
96
+
97
+ ### Start the main chatbot (port 8000)
98
+
99
+ ```bash
100
+ # From the project root
101
+ python -m app.main
102
+ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
103
+ ```
104
+
105
+ Or using uvicorn directly:
106
+
107
+ ```bash
108
+ swas
109
+ ```
110
+
111
+ ### Start the admin panel (port 9000)
112
+
113
+ In a separate terminal:
114
+
115
+ ```bash
116
+ python -m app.admin
117
+ ```
118
+
119
+ Or using uvicorn directly:
120
+
121
+ ```bash
122
+ uvicorn app.admin:admin_app --reload --host 0.0.0.0 --port 9000
123
+ ```
124
+
125
+ ### Access the applications
126
+
127
+ - Chatbot UI: http://localhost:8000
128
+ - Admin panel (PDF upload): http://localhost:9000
129
+
130
+ ## Usage
131
+
132
+ 1. Upload PDFs using the admin panel (port 9000)
133
+ 2. Open the chatbot UI (port 8000)
134
+ 3. Click the microphone and speak in Sinhala or English
135
+ 4. The app transcribes, translates to English, then queries RAG
136
+ 5. The response is shown in text and can be played via TTS
137
+
138
+ ## Troubleshooting
139
+
140
+ - If RAG answers are always from AI, upload at least one PDF and verify RAG status.
141
+ - If you see a missing API key error, check `.env` and restart the server.
142
+ - If model loading is slow, the first run downloads Whisper and embeddings.
143
+ - If PDFs already exist under `rag_data/`, the app now rebuilds/loads RAG at startup automatically.
144
+ - You can manually rebuild from all PDFs with `POST /api/rag/rebuild`.
145
+
146
+ ## Deploy on Hugging Face Spaces (Docker)
147
+
148
+ This project can be deployed with UI on Hugging Face Spaces using Docker.
149
+
150
+ ### 1. Create a new Space
151
+
152
+ - Go to Hugging Face Spaces and create a new Space.
153
+ - Set `SDK` to `Docker`.
154
+ - Upload/push this project files to that Space repository.
155
+
156
+ ### 2. Add Space secrets
157
+
158
+ In Space settings, add these secrets:
159
+
160
+ - `GEMINI_API_KEY` (optional)
161
+ - `HF_API_TOKEN` (optional fallback)
162
+
163
+ ### 3. Build and run
164
+
165
+ The provided `Dockerfile` starts:
166
+
167
+ - Main UI at `/`
168
+ - Admin UI at `/admin`
169
+
170
+ When Space build completes, open:
171
+
172
+ - `https://<your-space>.hf.space/`
173
+ - `https://<your-space>.hf.space/admin`
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # App package marker.
app/admin.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RAG Admin Panel - PDF Upload Management (Port 9000)
2
+ import os
3
+ from pathlib import Path
4
+ from contextlib import asynccontextmanager
5
+
6
+ from fastapi import FastAPI, UploadFile, File, HTTPException
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.templating import Jinja2Templates
9
+ from fastapi.requests import Request
10
+ from fastapi.responses import JSONResponse, HTMLResponse
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+
13
+ # Import RAG module
14
+ from app import rag
15
+
16
+ IS_HF_SPACE = bool(os.getenv("SPACE_ID"))
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ """Ensure RAG index is ready when admin panel starts."""
21
+ if not IS_HF_SPACE:
22
+ loaded = rag.load_vector_store()
23
+ if not loaded:
24
+ rag.rebuild_vector_store_from_pdfs()
25
+ yield
26
+
27
+
28
+ # Initialize FastAPI app for admin
29
+ admin_app = FastAPI(title="RAG Admin Panel", version="1.0.0", lifespan=lifespan)
30
+
31
+ # Add CORS middleware
32
+ admin_app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=["*"],
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ # Mount static files and templates
41
+ BASE_DIR = Path(__file__).resolve().parent
42
+ admin_app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
43
+ templates = Jinja2Templates(directory=BASE_DIR / "templates")
44
+
45
+
46
+ @admin_app.get("/", response_class=HTMLResponse)
47
+ async def admin_home(request: Request):
48
+ """Render the admin panel for PDF upload"""
49
+ return templates.TemplateResponse("admin.html", {"request": request})
50
+
51
+
52
+ @admin_app.post("/api/upload")
53
+ async def upload_pdf(file: UploadFile = File(...)):
54
+ """Upload a PDF file for RAG processing"""
55
+ if not file.filename.lower().endswith('.pdf'):
56
+ raise HTTPException(status_code=400, detail="Only PDF files are allowed")
57
+
58
+ try:
59
+ # Initialize embeddings if not already done
60
+ rag.initialize_embeddings()
61
+
62
+ # Save uploaded file
63
+ RAG_DATA_DIR = Path(__file__).resolve().parent.parent / "rag_data"
64
+ RAG_DATA_DIR.mkdir(parents=True, exist_ok=True)
65
+
66
+ pdf_path = RAG_DATA_DIR / file.filename
67
+
68
+ content = await file.read()
69
+ with open(pdf_path, "wb") as f:
70
+ f.write(content)
71
+
72
+ # Process the PDF
73
+ chunks = rag.load_and_process_pdf(str(pdf_path))
74
+
75
+ if not chunks:
76
+ raise HTTPException(status_code=400, detail="Could not extract text from PDF")
77
+
78
+ # Create/update vector store
79
+ success = rag.create_vector_store(chunks)
80
+
81
+ if success:
82
+ rag.get_rag_status()
83
+ return JSONResponse({
84
+ "success": True,
85
+ "message": f"PDF '{file.filename}' uploaded and processed successfully",
86
+ "chunks_created": len(chunks),
87
+ "total_documents": len(rag.uploaded_documents)
88
+ })
89
+ else:
90
+ raise HTTPException(status_code=500, detail="Failed to create vector store")
91
+
92
+ except Exception as e:
93
+ print(f"RAG Upload Error: {str(e)}")
94
+ raise HTTPException(status_code=500, detail=f"Failed to process PDF: {str(e)}")
95
+
96
+
97
+ @admin_app.get("/api/status")
98
+ async def get_status():
99
+ """Get RAG system status"""
100
+ return JSONResponse(rag.get_rag_status())
101
+
102
+
103
+ @admin_app.post("/api/clear")
104
+ async def clear_data():
105
+ """Clear all RAG data"""
106
+ rag.clear_rag_data()
107
+ return JSONResponse({"success": True, "message": "RAG data cleared"})
108
+
109
+
110
+ @admin_app.delete("/api/document/{filename}")
111
+ async def delete_document(filename: str):
112
+ """Delete a specific document"""
113
+ try:
114
+ RAG_DATA_DIR = Path(__file__).resolve().parent.parent / "rag_data"
115
+ pdf_path = RAG_DATA_DIR / filename
116
+
117
+ if pdf_path.exists():
118
+ os.remove(pdf_path)
119
+
120
+ if list(RAG_DATA_DIR.glob("*.pdf")):
121
+ rag.rebuild_vector_store_from_pdfs()
122
+ else:
123
+ rag.clear_rag_data()
124
+
125
+ return JSONResponse({"success": True, "message": f"Document '{filename}' deleted"})
126
+ except Exception as e:
127
+ raise HTTPException(status_code=500, detail=f"Failed to delete: {str(e)}")
128
+
129
+
130
+ @admin_app.post("/api/rebuild")
131
+ async def rebuild_data():
132
+ """Rebuild vector store from all PDFs in rag_data."""
133
+ success = rag.rebuild_vector_store_from_pdfs()
134
+ if success:
135
+ return JSONResponse({"success": True, "message": "RAG rebuilt successfully from all PDFs"})
136
+ return JSONResponse({"success": False, "message": "No valid PDFs found to rebuild RAG"})
137
+
138
+
139
+ if __name__ == "__main__":
140
+ import uvicorn
141
+ uvicorn.run(admin_app, host="0.0.0.0", port=9000)
app/hf_space.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ from app.main import app as chatbot_app
4
+ from app.admin import admin_app
5
+
6
+ app = FastAPI(title="Sinhala Chatbot Space", version="1.0.0")
7
+
8
+ # Hugging Face Spaces exposes a single port, so mount both apps under one server.
9
+ app.mount("/admin", admin_app)
10
+ app.mount("/", chatbot_app)
app/main.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import tempfile
4
+ from pathlib import Path
5
+ from contextlib import asynccontextmanager
6
+
7
+ from dotenv import load_dotenv
8
+ from fastapi import FastAPI, UploadFile, File, HTTPException
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.templating import Jinja2Templates
11
+ from fastapi.requests import Request
12
+ from fastapi.responses import JSONResponse, StreamingResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ import google.generativeai as genai
15
+ from gtts import gTTS
16
+ from deep_translator import GoogleTranslator
17
+
18
+ from app import rag
19
+
20
+ load_dotenv()
21
+
22
+ asr_model = None
23
+ model_loaded = False
24
+ model_loading = False
25
+
26
+ conversation_history = []
27
+
28
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
29
+ if GEMINI_API_KEY:
30
+ genai.configure(api_key=GEMINI_API_KEY)
31
+
32
+ LOCAL_MODEL_PATH = Path(__file__).resolve().parent.parent / "final_model"
33
+ HUGGINGFACE_MODEL_ID = "seniruk/whisper-small-si"
34
+ IS_HF_SPACE = bool(os.getenv("SPACE_ID"))
35
+
36
+
37
+ def load_asr_model():
38
+ """Load the ASR model - tries local model first, falls back to Hugging Face."""
39
+ global asr_model, model_loaded, model_loading
40
+
41
+ if model_loaded:
42
+ return asr_model
43
+
44
+ model_loading = True
45
+
46
+ try:
47
+ from transformers import WhisperProcessor, WhisperForConditionalGeneration
48
+ import torch
49
+ except Exception as import_error:
50
+ model_loading = False
51
+ raise RuntimeError(
52
+ "ASR dependencies are not installed. Install transformers and torch to enable speech input."
53
+ ) from import_error
54
+
55
+ processor = None
56
+ model = None
57
+ model_source = None
58
+
59
+ if LOCAL_MODEL_PATH.exists():
60
+ print("=" * 50)
61
+ print(f"Loading ASR model from local path: {LOCAL_MODEL_PATH}")
62
+ print("=" * 50)
63
+ try:
64
+ processor = WhisperProcessor.from_pretrained(str(LOCAL_MODEL_PATH))
65
+ model = WhisperForConditionalGeneration.from_pretrained(
66
+ str(LOCAL_MODEL_PATH), torch_dtype=torch.float32
67
+ )
68
+ model_source = "local"
69
+ print("Local model loaded successfully.")
70
+ except Exception as e:
71
+ print(f"Failed to load local model: {str(e)}")
72
+ print("Falling back to Hugging Face model...")
73
+ processor = None
74
+ model = None
75
+ else:
76
+ print(f"Local model not found at: {LOCAL_MODEL_PATH}")
77
+ print("Falling back to Hugging Face model...")
78
+
79
+ if model is None:
80
+ print("=" * 50)
81
+ print(f"Loading ASR model from Hugging Face: {HUGGINGFACE_MODEL_ID}")
82
+ print("This may take a minute on first run...")
83
+ print("=" * 50)
84
+ processor = WhisperProcessor.from_pretrained(HUGGINGFACE_MODEL_ID)
85
+ model = WhisperForConditionalGeneration.from_pretrained(
86
+ HUGGINGFACE_MODEL_ID, torch_dtype=torch.float32
87
+ )
88
+ model_source = "huggingface"
89
+ print("Hugging Face model loaded successfully.")
90
+
91
+ model.eval()
92
+
93
+ device = "cpu"
94
+ if torch.cuda.is_available():
95
+ device = "cuda"
96
+ model = model.half()
97
+ model = model.to("cuda")
98
+ print("Using GPU with float16 for faster inference.")
99
+ else:
100
+ print("Running on CPU.")
101
+
102
+ asr_model = {
103
+ "processor": processor,
104
+ "model": model,
105
+ "device": device,
106
+ "source": model_source,
107
+ }
108
+
109
+ model_loaded = True
110
+ model_loading = False
111
+ print(f"Model ready. (Source: {model_source})")
112
+ return asr_model
113
+
114
+
115
+ def transcribe_audio(audio_path: str) -> str:
116
+ """Transcribe audio file to text - optimized."""
117
+ global asr_model
118
+
119
+ try:
120
+ import soundfile as sf
121
+ import numpy as np
122
+ from scipy import signal
123
+ import torch
124
+ except Exception as import_error:
125
+ raise RuntimeError(
126
+ "Audio dependencies are not installed. Install soundfile, numpy, and scipy."
127
+ ) from import_error
128
+
129
+ processor = asr_model["processor"]
130
+ model = asr_model["model"]
131
+ device = asr_model["device"]
132
+
133
+ audio_array, sample_rate = sf.read(audio_path)
134
+
135
+ if len(audio_array.shape) > 1:
136
+ audio_array = audio_array.mean(axis=1)
137
+
138
+ if sample_rate != 16000:
139
+ num_samples = int(len(audio_array) * 16000 / sample_rate)
140
+ audio_array = signal.resample(audio_array, num_samples)
141
+
142
+ audio_array = audio_array.astype(np.float32)
143
+
144
+ inputs = processor(audio_array, sampling_rate=16000, return_tensors="pt").input_features
145
+
146
+ if device == "cuda":
147
+ inputs = inputs.half().to("cuda")
148
+
149
+ with torch.no_grad():
150
+ predicted_ids = model.generate(
151
+ inputs,
152
+ max_length=225,
153
+ num_beams=1,
154
+ do_sample=False,
155
+ use_cache=True,
156
+ )
157
+
158
+ return processor.batch_decode(predicted_ids, skip_special_tokens=True)[0].strip()
159
+
160
+
161
+ @asynccontextmanager
162
+ async def lifespan(app: FastAPI):
163
+ """Load model at startup."""
164
+ print("\nStarting Sinhala Chatbot Server...")
165
+ if IS_HF_SPACE:
166
+ print("Hugging Face Space detected. Skipping heavy startup preloads.")
167
+ else:
168
+ load_asr_model()
169
+ loaded = rag.load_vector_store()
170
+ if not loaded:
171
+ rag.rebuild_vector_store_from_pdfs()
172
+ print("Server ready.\n")
173
+ yield
174
+ print("\nShutting down...")
175
+
176
+
177
+ app = FastAPI(title="Sinhala Chatbot", version="1.0.0", lifespan=lifespan)
178
+
179
+ app.add_middleware(
180
+ CORSMiddleware,
181
+ allow_origins=["*"],
182
+ allow_credentials=True,
183
+ allow_methods=["*"],
184
+ allow_headers=["*"],
185
+ )
186
+
187
+ BASE_DIR = Path(__file__).resolve().parent
188
+ app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
189
+ templates = Jinja2Templates(directory=BASE_DIR / "templates")
190
+
191
+
192
+ @app.get("/")
193
+ async def home(request: Request):
194
+ """Render the main chatbot interface."""
195
+ return templates.TemplateResponse("index.html", {"request": request})
196
+
197
+
198
+ @app.get("/api/model-status")
199
+ async def get_model_status():
200
+ """Check if ASR model is loaded."""
201
+ source = asr_model.get("source", None) if asr_model else None
202
+ return JSONResponse({"loaded": model_loaded, "loading": model_loading, "source": source})
203
+
204
+
205
+ @app.post("/api/speech-to-text")
206
+ async def speech_to_text(audio: UploadFile = File(...)):
207
+ """Convert speech to text using Whisper ASR model."""
208
+ if not model_loaded:
209
+ try:
210
+ load_asr_model()
211
+ except Exception as load_error:
212
+ raise HTTPException(status_code=503, detail=str(load_error)) from load_error
213
+
214
+ try:
215
+ audio_bytes = await audio.read()
216
+
217
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
218
+ tmp_file.write(audio_bytes)
219
+ tmp_path = tmp_file.name
220
+
221
+ try:
222
+ transcription = transcribe_audio(tmp_path)
223
+ return JSONResponse({"success": True, "text": transcription})
224
+ finally:
225
+ os.unlink(tmp_path)
226
+
227
+ except Exception as e:
228
+ print(f"ASR Error: {str(e)}")
229
+ raise HTTPException(status_code=500, detail=f"Speech recognition failed: {str(e)}")
230
+
231
+
232
+ @app.post("/api/chat")
233
+ async def chat(request: Request):
234
+ """
235
+ Send text to RAG system (retrieves from documents first, then falls back to Gemini/HF).
236
+ Automatically translates non-English questions to English before RAG processing.
237
+ """
238
+ global conversation_history
239
+
240
+ try:
241
+ data = await request.json()
242
+ user_message = data.get("message", "")
243
+
244
+ if not user_message:
245
+ raise HTTPException(status_code=400, detail="Message is required")
246
+
247
+ english_question = user_message
248
+ try:
249
+ translator = GoogleTranslator(source="auto", target="en")
250
+ english_question = translator.translate(user_message)
251
+ print(f"Original Question: {user_message}")
252
+ print(f"English Question: {english_question}")
253
+ except Exception as trans_error:
254
+ print(f"Translation failed, using original: {trans_error}")
255
+ english_question = user_message
256
+
257
+ rag_result = rag.rag_answer(english_question)
258
+ assistant_message = rag_result.get("answer", "")
259
+
260
+ conversation_history.append({
261
+ "role": "user",
262
+ "parts": [user_message],
263
+ })
264
+
265
+ conversation_history.append({
266
+ "role": "model",
267
+ "parts": [assistant_message],
268
+ })
269
+
270
+ if len(conversation_history) > 20:
271
+ conversation_history = conversation_history[-20:]
272
+
273
+ return JSONResponse({
274
+ "success": True,
275
+ "response": assistant_message,
276
+ "source": rag_result.get("source", "none"),
277
+ "context_found": rag_result.get("context_found", False),
278
+ })
279
+
280
+ except Exception as e:
281
+ print(f"Chat Error: {str(e)}")
282
+ raise HTTPException(status_code=500, detail=f"Chat failed: {str(e)}")
283
+
284
+
285
+ @app.post("/api/text-to-speech")
286
+ async def text_to_speech(request: Request):
287
+ """Convert text to speech using Google TTS."""
288
+ try:
289
+ data = await request.json()
290
+ text = data.get("text", "")
291
+ lang = data.get("lang", "si")
292
+
293
+ if not text:
294
+ raise HTTPException(status_code=400, detail="Text is required")
295
+
296
+ tts = gTTS(text=text, lang=lang, slow=False)
297
+
298
+ audio_buffer = io.BytesIO()
299
+ tts.write_to_fp(audio_buffer)
300
+ audio_buffer.seek(0)
301
+
302
+ return StreamingResponse(
303
+ audio_buffer,
304
+ media_type="audio/mpeg",
305
+ headers={"Content-Disposition": "inline; filename=speech.mp3"},
306
+ )
307
+
308
+ except Exception as e:
309
+ print(f"TTS Error: {str(e)}")
310
+ raise HTTPException(status_code=500, detail=f"Text-to-speech failed: {str(e)}")
311
+
312
+
313
+ @app.post("/api/clear-history")
314
+ async def clear_history():
315
+ """Clear conversation history."""
316
+ global conversation_history
317
+ conversation_history = []
318
+ return JSONResponse({"success": True, "message": "Conversation history cleared"})
319
+
320
+
321
+ @app.get("/api/health")
322
+ async def health_check():
323
+ """Health check endpoint."""
324
+ return JSONResponse({
325
+ "status": "healthy",
326
+ "gemini_configured": GEMINI_API_KEY is not None,
327
+ })
328
+
329
+
330
+ @app.post("/api/translate-to-english")
331
+ async def translate_to_english(request: Request):
332
+ """Translate Sinhala/mixed language question to full English using Google Translate."""
333
+ try:
334
+ data = await request.json()
335
+ question = data.get("question", "")
336
+ if not question:
337
+ raise HTTPException(status_code=400, detail="Question is required")
338
+
339
+ translator = GoogleTranslator(source="auto", target="en")
340
+ english_question = translator.translate(question)
341
+
342
+ print(f"Original: {question}")
343
+ print(f"Translated: {english_question}")
344
+
345
+ return JSONResponse({"success": True, "english_question": english_question, "translated": True})
346
+ except Exception as e:
347
+ print(f"Translation Error: {str(e)}")
348
+ error_msg = str(e)
349
+ return JSONResponse({
350
+ "success": False,
351
+ "english_question": question,
352
+ "translated": False,
353
+ "error": error_msg,
354
+ })
355
+
356
+
357
+ @app.post("/api/rag/upload")
358
+ async def upload_pdf(file: UploadFile = File(...)):
359
+ """Upload a PDF file for RAG processing."""
360
+ if not file.filename.lower().endswith(".pdf"):
361
+ raise HTTPException(status_code=400, detail="Only PDF files are allowed")
362
+
363
+ try:
364
+ rag_data_dir = Path(__file__).resolve().parent.parent / "rag_data"
365
+ rag_data_dir.mkdir(parents=True, exist_ok=True)
366
+
367
+ pdf_path = rag_data_dir / file.filename
368
+
369
+ content = await file.read()
370
+ with open(pdf_path, "wb") as f:
371
+ f.write(content)
372
+
373
+ chunks = rag.load_and_process_pdf(str(pdf_path))
374
+
375
+ if not chunks:
376
+ raise HTTPException(status_code=400, detail="Could not extract text from PDF")
377
+
378
+ success = rag.create_vector_store(chunks)
379
+
380
+ if success:
381
+ status = rag.get_rag_status()
382
+ return JSONResponse({
383
+ "success": True,
384
+ "message": f"PDF '{file.filename}' uploaded and processed successfully",
385
+ "chunks_created": len(chunks),
386
+ "total_documents": status.get("documents_count", 0),
387
+ })
388
+ raise HTTPException(status_code=500, detail="Failed to create vector store")
389
+
390
+ except Exception as e:
391
+ print(f"RAG Upload Error: {str(e)}")
392
+ raise HTTPException(status_code=500, detail=f"Failed to process PDF: {str(e)}")
393
+
394
+
395
+ @app.post("/api/rag/ask")
396
+ async def rag_ask(request: Request):
397
+ """Ask a question using RAG - first checks database, then falls back to Gemini/HF."""
398
+ try:
399
+ data = await request.json()
400
+ question = data.get("question", "")
401
+ response_lang = data.get("response_lang", "en")
402
+
403
+ print(f"Question: {question}")
404
+ print(f"Response language: {response_lang}")
405
+
406
+ if not question:
407
+ raise HTTPException(status_code=400, detail="Question is required")
408
+
409
+ result = rag.rag_answer(question)
410
+ answer = result["answer"]
411
+
412
+ print(f"Original answer length: {len(answer) if answer else 0}")
413
+
414
+ if response_lang == "si-en" and answer:
415
+ print("Translating to Sinhala+English...")
416
+ try:
417
+ translator = GoogleTranslator(source="en", target="si")
418
+ sinhala_answer = translator.translate(answer)
419
+ answer = f"**Sinhala:**\n{sinhala_answer}\n\n---\n\n**English:**\n{answer}"
420
+ print("Translated successfully.")
421
+ except Exception as trans_err:
422
+ print(f"Translation to Sinhala failed: {trans_err}")
423
+ answer = f"Translation failed: {trans_err}\n\n**English:** {answer}"
424
+
425
+ return JSONResponse({
426
+ "success": True,
427
+ "question": question,
428
+ "answer": answer,
429
+ "source": result["source"],
430
+ "context_found": result["context_found"],
431
+ "relevance_score": result["relevance_score"],
432
+ "response_lang": response_lang,
433
+ })
434
+
435
+ except Exception as e:
436
+ print(f"RAG Ask Error: {str(e)}")
437
+ raise HTTPException(status_code=500, detail=f"RAG query failed: {str(e)}")
438
+
439
+
440
+ @app.get("/api/rag/status")
441
+ async def rag_status():
442
+ """Get RAG system status."""
443
+ return JSONResponse(rag.get_rag_status())
444
+
445
+
446
+ @app.post("/api/rag/clear")
447
+ async def clear_rag():
448
+ """Clear all RAG data."""
449
+ rag.clear_rag_data()
450
+ return JSONResponse({"success": True, "message": "RAG data cleared"})
451
+
452
+
453
+ @app.post("/api/rag/rebuild")
454
+ async def rebuild_rag():
455
+ """Rebuild vector store from all PDFs in rag_data directory."""
456
+ success = rag.rebuild_vector_store_from_pdfs()
457
+ if not success:
458
+ return JSONResponse(
459
+ {
460
+ "success": False,
461
+ "message": "No valid PDFs found to rebuild vector store.",
462
+ }
463
+ )
464
+ return JSONResponse({"success": True, "message": "RAG vector store rebuilt successfully."})
465
+
466
+
467
+ if __name__ == "__main__":
468
+ import uvicorn
469
+
470
+ uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
app/rag.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import unicodedata
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from dotenv import load_dotenv
8
+ import google.generativeai as genai
9
+ from huggingface_hub import InferenceClient
10
+
11
+ load_dotenv()
12
+
13
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
14
+ if GEMINI_API_KEY:
15
+ genai.configure(api_key=GEMINI_API_KEY)
16
+
17
+ vectordb = None
18
+ retriever = None
19
+ embeddings = None
20
+ rag_initialized = False
21
+ uploaded_documents = []
22
+ last_index_mtime = None
23
+
24
+ RAG_DATA_DIR = Path(__file__).resolve().parent.parent / "rag_data"
25
+ FAISS_INDEX_PATH = RAG_DATA_DIR / "faiss_index"
26
+ INSUFFICIENT_CONTEXT_MARKER = "i don't have enough information in the documents"
27
+
28
+
29
+ def initialize_embeddings():
30
+ """Initialize the multilingual embedding model."""
31
+ global embeddings
32
+
33
+ if embeddings is not None:
34
+ return embeddings
35
+
36
+ print("Loading multilingual embedding model...")
37
+ from langchain_huggingface import HuggingFaceEmbeddings
38
+
39
+ embeddings = HuggingFaceEmbeddings(
40
+ model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
41
+ encode_kwargs={"normalize_embeddings": True},
42
+ )
43
+ print("Embedding model loaded.")
44
+ return embeddings
45
+
46
+
47
+ def clean_text(text: str) -> str:
48
+ """Clean and normalize text for embedding."""
49
+ if not isinstance(text, str) or not text.strip():
50
+ return ""
51
+
52
+ normalized_text = unicodedata.normalize("NFKC", text)
53
+
54
+ cleaned_chars = [
55
+ char for char in normalized_text
56
+ if unicodedata.category(char) not in ["So", "Cn", "Cc", "Cf", "Cs"]
57
+ ]
58
+ cleaned_text = "".join(cleaned_chars)
59
+
60
+ cleaned_text = re.sub(r"\s+", " ", cleaned_text).strip()
61
+
62
+ return cleaned_text
63
+
64
+
65
+ def load_and_process_pdf(pdf_path: str) -> List[dict]:
66
+ """Load a PDF and split it into chunks."""
67
+ from langchain_community.document_loaders import PyPDFLoader
68
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
69
+
70
+ print(f"Loading PDF: {pdf_path}")
71
+
72
+ loader = PyPDFLoader(pdf_path)
73
+ docs = loader.load()
74
+
75
+ splitter = RecursiveCharacterTextSplitter(
76
+ chunk_size=300,
77
+ chunk_overlap=80,
78
+ )
79
+ chunks = splitter.split_documents(docs)
80
+
81
+ print(f"Loaded {len(docs)} pages, created {len(chunks)} chunks.")
82
+ return chunks
83
+
84
+
85
+ def create_vector_store(chunks: List) -> bool:
86
+ """Create or update the FAISS vector store with document chunks."""
87
+ global vectordb, retriever, rag_initialized
88
+
89
+ from langchain_community.vectorstores import FAISS
90
+
91
+ initialize_embeddings()
92
+
93
+ texts = [doc.page_content for doc in chunks]
94
+ metadatas = [doc.metadata for doc in chunks]
95
+
96
+ processed_texts = []
97
+ processed_metadatas = []
98
+
99
+ for i, text in enumerate(texts):
100
+ cleaned_text = clean_text(text)
101
+ if cleaned_text:
102
+ processed_texts.append(cleaned_text)
103
+ processed_metadatas.append(metadatas[i])
104
+
105
+ if not processed_texts:
106
+ print("No valid texts after cleaning.")
107
+ return False
108
+
109
+ print(f"Processing {len(processed_texts)} text chunks for embedding...")
110
+
111
+ if vectordb is None:
112
+ vectordb = FAISS.from_texts(processed_texts, embeddings, metadatas=processed_metadatas)
113
+ else:
114
+ new_vectordb = FAISS.from_texts(processed_texts, embeddings, metadatas=processed_metadatas)
115
+ vectordb.merge_from(new_vectordb)
116
+
117
+ retriever = vectordb.as_retriever(search_kwargs={"k": 4})
118
+ rag_initialized = True
119
+
120
+ save_vector_store()
121
+ _sync_uploaded_documents()
122
+
123
+ print("Vector store created/updated successfully.")
124
+ return True
125
+
126
+
127
+ def save_vector_store():
128
+ """Save the FAISS index to disk."""
129
+ global vectordb, last_index_mtime
130
+
131
+ if vectordb is None:
132
+ return
133
+
134
+ RAG_DATA_DIR.mkdir(parents=True, exist_ok=True)
135
+ vectordb.save_local(str(FAISS_INDEX_PATH))
136
+ last_index_mtime = _get_index_mtime()
137
+ print(f"Vector store saved to {FAISS_INDEX_PATH}.")
138
+
139
+
140
+ def load_vector_store() -> bool:
141
+ """Load the FAISS index from disk if it exists."""
142
+ global vectordb, retriever, rag_initialized, last_index_mtime
143
+
144
+ if not FAISS_INDEX_PATH.exists():
145
+ return False
146
+
147
+ try:
148
+ from langchain_community.vectorstores import FAISS
149
+
150
+ initialize_embeddings()
151
+ vectordb = FAISS.load_local(
152
+ str(FAISS_INDEX_PATH),
153
+ embeddings,
154
+ allow_dangerous_deserialization=True,
155
+ )
156
+ retriever = vectordb.as_retriever(search_kwargs={"k": 4})
157
+ rag_initialized = True
158
+ last_index_mtime = _get_index_mtime()
159
+ _sync_uploaded_documents()
160
+ print("Loaded existing vector store from disk.")
161
+ return True
162
+ except Exception as e:
163
+ print(f"Failed to load vector store: {e}")
164
+ return False
165
+
166
+
167
+ def rag_answer(question: str) -> dict:
168
+ """Answer a question using RAG - first check database, then fallback to Gemini/HF."""
169
+ global retriever, vectordb, last_index_mtime
170
+
171
+ result = {
172
+ "answer": "",
173
+ "source": "none",
174
+ "context_found": False,
175
+ "relevance_score": 0.0,
176
+ }
177
+
178
+ if FAISS_INDEX_PATH.exists():
179
+ current_mtime = _get_index_mtime()
180
+ if (not rag_initialized or retriever is None) or (
181
+ current_mtime and last_index_mtime and current_mtime > last_index_mtime
182
+ ):
183
+ load_vector_store()
184
+
185
+ if not rag_initialized or retriever is None:
186
+ result["source"] = "gemini"
187
+ result["answer"] = _ask_gemini_directly(question)
188
+ return result
189
+
190
+ docs_with_scores = vectordb.similarity_search_with_score(question, k=4)
191
+
192
+ if not docs_with_scores:
193
+ print(f"No documents found for question: {question}")
194
+ result["source"] = "gemini"
195
+ result["answer"] = _ask_gemini_directly(question)
196
+ return result
197
+
198
+ best_score = docs_with_scores[0][1] if docs_with_scores else float("inf")
199
+ result["relevance_score"] = float(best_score)
200
+
201
+ print(f"\nQuestion: {question}")
202
+ print(f"Retrieved {len(docs_with_scores)} documents:")
203
+ for i, (doc, score) in enumerate(docs_with_scores):
204
+ preview = doc.page_content[:100].replace("\n", " ")
205
+ print(f" [{i + 1}] Score: {score:.3f} - {preview}...")
206
+
207
+ print(f"Using RAG with relevance score: {best_score}")
208
+
209
+ docs = [doc for doc, score in docs_with_scores]
210
+ context = "\n\n".join([d.page_content for d in docs])
211
+ result["context_found"] = True
212
+
213
+ prompt = (
214
+ "You are a helpful assistant. Answer the question based ONLY on the following "
215
+ "context from the PDF document. If the context doesn't contain enough information "
216
+ "to answer the question, say \"I don't have enough information in the documents to "
217
+ "answer this question.\"\n\n"
218
+ "Context from PDF:\n"
219
+ f"{context}\n\n"
220
+ f"Question: {question}\n\n"
221
+ "Answer (in English):"
222
+ )
223
+
224
+ try:
225
+ gemini_key = os.getenv("GEMINI_API_KEY")
226
+ if gemini_key:
227
+ try:
228
+ model = genai.GenerativeModel("models/gemini-2.5-flash")
229
+ response = model.generate_content(prompt)
230
+ rag_answer_text = (response.text or "").strip()
231
+ if _is_insufficient_context_answer(rag_answer_text):
232
+ print("RAG context not sufficient. Falling back to direct AI answer.")
233
+ result["answer"] = _ask_gemini_directly(question)
234
+ result["source"] = "gemini"
235
+ return result
236
+ result["answer"] = rag_answer_text
237
+ result["source"] = "rag"
238
+ return result
239
+ except Exception as gemini_error:
240
+ error_msg = str(gemini_error)
241
+ print(f"Gemini error in RAG: {error_msg[:200]}...")
242
+ if "429" in error_msg or "quota" in error_msg.lower():
243
+ print("Gemini quota exceeded. Using Hugging Face for RAG.")
244
+
245
+ print("Using Hugging Face for RAG answer...")
246
+ rag_answer_text = _ask_huggingface_free(prompt).strip()
247
+ if _is_insufficient_context_answer(rag_answer_text):
248
+ print("RAG context not sufficient. Falling back to direct AI answer.")
249
+ result["answer"] = _ask_gemini_directly(question)
250
+ result["source"] = "gemini"
251
+ return result
252
+ result["answer"] = rag_answer_text
253
+ result["source"] = "rag"
254
+
255
+ except Exception as e:
256
+ print(f"All RAG generation failed: {e}")
257
+ result["answer"] = "Sorry, unable to generate answer. Please try again later."
258
+ result["source"] = "error"
259
+
260
+ return result
261
+
262
+
263
+ def _ask_huggingface_free(prompt: str) -> str:
264
+ """Use free Hugging Face Inference API with token if available."""
265
+ hf_token = os.getenv("HF_API_TOKEN")
266
+
267
+ try:
268
+ client = InferenceClient(token=hf_token)
269
+ except Exception as e:
270
+ raise Exception(f"Failed to create Hugging Face client: {e}")
271
+
272
+ messages = [{"role": "user", "content": prompt}]
273
+
274
+ try:
275
+ print("Calling Hugging Face API (Qwen2.5-72B-Instruct)...")
276
+ response = client.chat_completion(
277
+ messages=messages,
278
+ model="Qwen/Qwen2.5-72B-Instruct",
279
+ max_tokens=500,
280
+ temperature=0.7,
281
+ )
282
+ return response.choices[0].message.content
283
+ except Exception as e:
284
+ error_str = str(e)
285
+ print(f"Hugging Face primary model error: {e}")
286
+
287
+ try:
288
+ print("Trying backup model (Microsoft Phi-3)...")
289
+ response = client.chat_completion(
290
+ messages=messages,
291
+ model="microsoft/Phi-3-mini-4k-instruct",
292
+ max_tokens=500,
293
+ temperature=0.7,
294
+ )
295
+ return response.choices[0].message.content
296
+ except Exception as e2:
297
+ print(f"Backup model also failed: {e2}")
298
+ raise Exception(f"All HF models failed: {error_str}")
299
+
300
+
301
+ def _ask_gemini_directly(question: str) -> str:
302
+ """Fallback: Ask Gemini directly without RAG context, with Hugging Face fallback."""
303
+ prompt = (
304
+ "Answer the following question helpfully and accurately:\n\n"
305
+ f"Question: {question}\n\n"
306
+ "Answer:"
307
+ )
308
+
309
+ gemini_key = os.getenv("GEMINI_API_KEY")
310
+
311
+ if gemini_key:
312
+ try:
313
+ model = genai.GenerativeModel("models/gemini-2.5-flash")
314
+ response = model.generate_content(prompt)
315
+ return response.text
316
+ except Exception as gemini_error:
317
+ error_msg = str(gemini_error)
318
+ print(f"Gemini API error: {error_msg[:200]}...")
319
+
320
+ if "429" in error_msg or "quota" in error_msg.lower():
321
+ print("Gemini quota exceeded. Switching to Hugging Face.")
322
+ else:
323
+ print("Gemini error. Switching to Hugging Face.")
324
+ else:
325
+ print("No Gemini API key, using Hugging Face.")
326
+
327
+ try:
328
+ print("Using Hugging Face for direct answer...")
329
+ return _ask_huggingface_free(prompt)
330
+ except Exception as hf_error:
331
+ print(f"Hugging Face error: {hf_error}")
332
+ return (
333
+ "Sorry, both AI services are unavailable. "
334
+ f"Gemini quota exceeded, and Hugging Face error: {str(hf_error)}"
335
+ )
336
+
337
+
338
+ def get_rag_status() -> dict:
339
+ """Get the current status of the RAG system."""
340
+ if not rag_initialized and FAISS_INDEX_PATH.exists():
341
+ load_vector_store()
342
+
343
+ _sync_uploaded_documents()
344
+ return {
345
+ "initialized": rag_initialized,
346
+ "documents_count": len(uploaded_documents),
347
+ "documents": uploaded_documents,
348
+ "has_embeddings": embeddings is not None,
349
+ "has_vector_store": vectordb is not None,
350
+ }
351
+
352
+
353
+ def clear_rag_data():
354
+ """Clear all RAG data."""
355
+ global vectordb, retriever, rag_initialized, uploaded_documents, last_index_mtime
356
+
357
+ vectordb = None
358
+ retriever = None
359
+ rag_initialized = False
360
+ uploaded_documents = []
361
+ last_index_mtime = None
362
+
363
+ if FAISS_INDEX_PATH.exists():
364
+ import shutil
365
+
366
+ shutil.rmtree(FAISS_INDEX_PATH)
367
+
368
+ print("RAG data cleared.")
369
+ return True
370
+
371
+
372
+ def _get_index_mtime():
373
+ index_file = FAISS_INDEX_PATH / "index.faiss"
374
+ if index_file.exists():
375
+ return index_file.stat().st_mtime
376
+ return None
377
+
378
+
379
+ def _is_insufficient_context_answer(answer_text: str) -> bool:
380
+ if not answer_text:
381
+ return True
382
+ normalized = answer_text.strip().lower()
383
+ return INSUFFICIENT_CONTEXT_MARKER in normalized
384
+
385
+
386
+ def _sync_uploaded_documents():
387
+ global uploaded_documents
388
+
389
+ if not RAG_DATA_DIR.exists():
390
+ uploaded_documents = []
391
+ return
392
+
393
+ uploaded_documents = sorted(
394
+ [pdf.name for pdf in RAG_DATA_DIR.glob("*.pdf") if pdf.is_file()]
395
+ )
396
+
397
+
398
+ def rebuild_vector_store_from_pdfs() -> bool:
399
+ """Rebuild vector store from all PDFs in rag_data directory."""
400
+ global vectordb, retriever, rag_initialized
401
+
402
+ _sync_uploaded_documents()
403
+ if not uploaded_documents:
404
+ print("No PDFs found in rag_data to rebuild vector store.")
405
+ return False
406
+
407
+ initialize_embeddings()
408
+
409
+ vectordb = None
410
+ retriever = None
411
+ rag_initialized = False
412
+
413
+ all_chunks = []
414
+ for filename in uploaded_documents:
415
+ pdf_path = RAG_DATA_DIR / filename
416
+ try:
417
+ chunks = load_and_process_pdf(str(pdf_path))
418
+ all_chunks.extend(chunks)
419
+ except Exception as e:
420
+ print(f"Skipping PDF '{filename}' due to processing error: {e}")
421
+
422
+ if not all_chunks:
423
+ print("No chunks generated from PDFs. Rebuild aborted.")
424
+ return False
425
+
426
+ success = create_vector_store(all_chunks)
427
+ if success:
428
+ print(f"Rebuilt vector store from {len(uploaded_documents)} PDF(s).")
429
+ return success
app/static/css/style.css ADDED
@@ -0,0 +1,1054 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CSS Variables */
2
+ :root {
3
+ --primary: #6d5ce7;
4
+ --primary-light: #a29bfe;
5
+ --primary-dark: #4a3db0;
6
+ --primary-glow: rgba(109, 92, 231, 0.35);
7
+ --accent: #5f72f3;
8
+ --accent-light: #7c8cf8;
9
+ --success: #00cec9;
10
+ --danger: #ff6b6b;
11
+ --warning: #feca57;
12
+ --bg-dark: #080816;
13
+ --bg-card: rgba(15, 15, 35, 0.65);
14
+ --bg-card-solid: #0f0f23;
15
+ --bg-surface: rgba(20, 20, 50, 0.5);
16
+ --border-color: rgba(109, 92, 231, 0.12);
17
+ --border-hover: rgba(109, 92, 231, 0.35);
18
+ --text-white: #eef0ff;
19
+ --text-secondary: #a0a8c8;
20
+ --text-muted: #5c6280;
21
+ --font-main: "Inter", "Noto Sans Sinhala", system-ui, sans-serif;
22
+ --radius-sm: 10px;
23
+ --radius-md: 16px;
24
+ --radius-lg: 24px;
25
+ --radius-full: 9999px;
26
+ }
27
+
28
+ /* Reset & Base */
29
+ *,
30
+ *::before,
31
+ *::after {
32
+ margin: 0;
33
+ padding: 0;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ html {
38
+ scroll-behavior: smooth;
39
+ }
40
+
41
+ body {
42
+ font-family: var(--font-main);
43
+ background: var(--bg-dark);
44
+ min-height: 100vh;
45
+ color: var(--text-white);
46
+ overflow-x: hidden;
47
+ }
48
+
49
+ /* Three.js Background Canvas */
50
+ #bgCanvas {
51
+ position: fixed;
52
+ top: 0;
53
+ left: 0;
54
+ width: 100%;
55
+ height: 100%;
56
+ z-index: 0;
57
+ pointer-events: none;
58
+ }
59
+
60
+ /* App Wrapper */
61
+ .app-wrapper {
62
+ position: relative;
63
+ z-index: 1;
64
+ max-width: 1000px;
65
+ margin: 0 auto;
66
+ padding: 8px 24px 20px;
67
+ min-height: 100vh;
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 6px;
71
+ }
72
+
73
+ /* ========== TOP BAR ========== */
74
+ .top-bar {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ padding: 14px 24px;
79
+ background: var(--bg-card);
80
+ border: 1px solid var(--border-color);
81
+ border-radius: var(--radius-lg);
82
+ backdrop-filter: blur(20px);
83
+ }
84
+
85
+ .top-bar-left {
86
+ display: flex;
87
+ align-items: center;
88
+ }
89
+
90
+ .status-indicator {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 10px;
94
+ padding: 8px 18px;
95
+ background: rgba(34, 197, 94, 0.08);
96
+ border: 1px solid rgba(34, 197, 94, 0.25);
97
+ border-radius: var(--radius-full);
98
+ }
99
+
100
+ .status-dot {
101
+ width: 10px;
102
+ height: 10px;
103
+ border-radius: 50%;
104
+ background: var(--success);
105
+ box-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
106
+ animation: pulse-glow 2s ease-in-out infinite;
107
+ }
108
+
109
+ .status-dot.recording {
110
+ background: var(--danger);
111
+ box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
112
+ animation: pulse-glow 0.5s ease-in-out infinite;
113
+ }
114
+
115
+ .status-dot.processing {
116
+ background: var(--warning);
117
+ box-shadow: 0 0 8px rgba(245, 158, 11, 0.6);
118
+ animation: pulse-glow 0.8s ease-in-out infinite;
119
+ }
120
+
121
+ .status-text {
122
+ font-size: 0.9rem;
123
+ font-weight: 600;
124
+ color: var(--success);
125
+ }
126
+
127
+ .top-bar-right {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 10px;
131
+ }
132
+
133
+ .top-btn {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 8px;
137
+ padding: 10px 20px;
138
+ border-radius: var(--radius-full);
139
+ font-size: 0.88rem;
140
+ font-weight: 600;
141
+ cursor: pointer;
142
+ transition: all 0.25s ease;
143
+ font-family: var(--font-main);
144
+ border: 1px solid var(--border-color);
145
+ }
146
+
147
+ .lang-toggle-btn {
148
+ background: var(--primary);
149
+ color: white;
150
+ border-color: var(--primary);
151
+ }
152
+
153
+ .lang-toggle-btn:hover {
154
+ background: var(--primary-dark);
155
+ box-shadow: 0 0 20px var(--primary-glow);
156
+ }
157
+
158
+ .lang-toggle-btn.si-mode {
159
+ background: var(--accent);
160
+ border-color: var(--accent);
161
+ }
162
+
163
+ .clear-btn {
164
+ background: transparent;
165
+ color: var(--text-secondary);
166
+ border: 1px solid var(--border-color);
167
+ }
168
+
169
+ .clear-btn:hover {
170
+ color: var(--danger);
171
+ border-color: rgba(239, 68, 68, 0.4);
172
+ background: rgba(239, 68, 68, 0.08);
173
+ }
174
+
175
+ /* ========== HERO ========== */
176
+ .hero {
177
+ text-align: center;
178
+ padding: 10px 20px 0;
179
+ }
180
+
181
+ .hero.compact {
182
+ padding: 30px 20px 20px;
183
+ }
184
+
185
+ .hero.compact .hero-top-row {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: center;
189
+ gap: 20px;
190
+ margin-bottom: 4px;
191
+ }
192
+
193
+ .hero.compact .hero-badge {
194
+ margin-bottom: 0;
195
+ }
196
+
197
+ .hero.compact .hero-title {
198
+ font-size: 2.6rem;
199
+ margin-bottom: 0;
200
+ }
201
+
202
+ .hero.compact .hero-desc {
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ gap: 10px;
207
+ }
208
+
209
+ .hero.compact .hero-desc i {
210
+ color: var(--primary-light);
211
+ font-size: 1rem;
212
+ }
213
+
214
+ .hero-badge {
215
+ display: inline-block;
216
+ padding: 6px 20px;
217
+ background: var(--primary);
218
+ color: white;
219
+ font-size: 0.82rem;
220
+ font-weight: 700;
221
+ border-radius: var(--radius-full);
222
+ letter-spacing: 0.5px;
223
+ margin-bottom: 18px;
224
+ box-shadow: 0 4px 20px var(--primary-glow);
225
+ }
226
+
227
+ .hero-title {
228
+ font-size: 3.5rem;
229
+ font-weight: 800;
230
+ background: linear-gradient(
231
+ 135deg,
232
+ var(--primary-light),
233
+ var(--primary),
234
+ var(--accent-light)
235
+ );
236
+ -webkit-background-clip: text;
237
+ -webkit-text-fill-color: transparent;
238
+ background-clip: text;
239
+ letter-spacing: -1.5px;
240
+ line-height: 1.1;
241
+ margin-bottom: 8px;
242
+ }
243
+
244
+ .hero-desc {
245
+ font-size: 0.95rem;
246
+ color: var(--text-secondary);
247
+ font-weight: 400;
248
+ }
249
+
250
+ /* ========== INFO BANNER ========== */
251
+ .info-banner {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 14px;
255
+ padding: 18px 28px;
256
+ background: var(--bg-card);
257
+ border: 1px solid var(--border-color);
258
+ border-radius: var(--radius-md);
259
+ backdrop-filter: blur(20px);
260
+ }
261
+
262
+ .info-banner i {
263
+ font-size: 1.3rem;
264
+ color: var(--primary-light);
265
+ }
266
+
267
+ .info-banner span {
268
+ font-size: 0.95rem;
269
+ color: var(--text-secondary);
270
+ font-weight: 500;
271
+ }
272
+
273
+ /* ========== MAIN CONTENT ========== */
274
+ .main-content {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 6px;
278
+ }
279
+
280
+ /* ========== MIC AREA ========== */
281
+ .mic-area {
282
+ position: relative;
283
+ display: flex;
284
+ flex-direction: column;
285
+ align-items: center;
286
+ justify-content: center;
287
+ padding: 6px 20px 4px;
288
+ }
289
+
290
+ .mic-area.no-box {
291
+ background: none;
292
+ border: none;
293
+ border-radius: 0;
294
+ backdrop-filter: none;
295
+ overflow: visible;
296
+ }
297
+
298
+ .mic-area.no-box::before {
299
+ display: none;
300
+ }
301
+
302
+ /* Recording Timer */
303
+ .recording-timer {
304
+ display: none;
305
+ align-items: center;
306
+ gap: 8px;
307
+ padding: 8px 18px;
308
+ background: rgba(239, 68, 68, 0.1);
309
+ border-radius: var(--radius-full);
310
+ border: 1px solid rgba(239, 68, 68, 0.3);
311
+ margin-bottom: 20px;
312
+ z-index: 2;
313
+ }
314
+
315
+ .recording-timer.active {
316
+ display: flex;
317
+ }
318
+
319
+ .timer-dot {
320
+ width: 8px;
321
+ height: 8px;
322
+ background: var(--danger);
323
+ border-radius: 50%;
324
+ animation: blink 1s infinite;
325
+ }
326
+
327
+ .timer-text {
328
+ font-family: "JetBrains Mono", monospace;
329
+ font-size: 1rem;
330
+ font-weight: 600;
331
+ color: var(--danger);
332
+ }
333
+
334
+ /* Mic Row - inline mic + reset */
335
+ .mic-row {
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: center;
339
+ gap: 24px;
340
+ z-index: 2;
341
+ }
342
+
343
+ /* Reset Row - right aligned above chat */
344
+ .reset-row {
345
+ display: flex;
346
+ justify-content: flex-end;
347
+ }
348
+
349
+ /* Mic Wrapper & Glow Rings */
350
+ .mic-wrapper {
351
+ position: relative;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ width: 140px;
356
+ height: 140px;
357
+ z-index: 2;
358
+ }
359
+
360
+ .mic-glow-ring {
361
+ position: absolute;
362
+ border-radius: 50%;
363
+ border: 1.5px solid rgba(109, 92, 231, 0.2);
364
+ pointer-events: none;
365
+ }
366
+
367
+ .mic-glow-ring.ring-1 {
368
+ width: 90px;
369
+ height: 90px;
370
+ border-color: rgba(109, 92, 231, 0.25);
371
+ box-shadow: 0 0 15px rgba(109, 92, 231, 0.08);
372
+ }
373
+
374
+ .mic-glow-ring.ring-2 {
375
+ width: 115px;
376
+ height: 115px;
377
+ border-color: rgba(109, 92, 231, 0.15);
378
+ box-shadow: 0 0 25px rgba(109, 92, 231, 0.05);
379
+ }
380
+
381
+ .mic-glow-ring.ring-3 {
382
+ width: 140px;
383
+ height: 140px;
384
+ border-color: rgba(109, 92, 231, 0.08);
385
+ }
386
+
387
+ .mic-btn {
388
+ width: 64px;
389
+ height: 64px;
390
+ border-radius: 50%;
391
+ border: none;
392
+ background: linear-gradient(135deg, var(--primary), var(--accent));
393
+ color: white;
394
+ font-size: 1.6rem;
395
+ cursor: pointer;
396
+ position: relative;
397
+ z-index: 3;
398
+ transition: all 0.3s ease;
399
+ box-shadow:
400
+ 0 8px 32px var(--primary-glow),
401
+ 0 0 60px rgba(109, 92, 231, 0.15);
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ }
406
+
407
+ .mic-btn:hover {
408
+ transform: scale(1.08);
409
+ box-shadow:
410
+ 0 12px 40px var(--primary-glow),
411
+ 0 0 80px rgba(109, 92, 231, 0.2);
412
+ }
413
+
414
+ .mic-btn:active {
415
+ transform: scale(0.98);
416
+ }
417
+
418
+ .mic-btn.recording {
419
+ background: linear-gradient(135deg, var(--danger), #e55050);
420
+ box-shadow: 0 8px 32px rgba(255, 107, 107, 0.4);
421
+ animation: pulse-btn 1s infinite;
422
+ }
423
+
424
+ .mic-btn.recording ~ .mic-glow-ring {
425
+ border-color: rgba(255, 107, 107, 0.2);
426
+ animation: ring-pulse 1.5s ease-in-out infinite;
427
+ }
428
+
429
+ .mic-btn.recording i::before {
430
+ content: "\f04d";
431
+ }
432
+
433
+ /* Audio Visualizer */
434
+ .visualizer {
435
+ display: none;
436
+ align-items: flex-end;
437
+ gap: 5px;
438
+ height: 40px;
439
+ margin-top: 20px;
440
+ z-index: 2;
441
+ }
442
+
443
+ .visualizer.active {
444
+ display: flex;
445
+ }
446
+
447
+ .visualizer .bar {
448
+ width: 6px;
449
+ background: linear-gradient(to top, var(--primary), var(--accent-light));
450
+ border-radius: 3px;
451
+ animation: visualize 0.5s ease infinite;
452
+ }
453
+
454
+ .visualizer .bar:nth-child(1) {
455
+ animation-delay: 0s;
456
+ height: 15px;
457
+ }
458
+ .visualizer .bar:nth-child(2) {
459
+ animation-delay: 0.1s;
460
+ height: 25px;
461
+ }
462
+ .visualizer .bar:nth-child(3) {
463
+ animation-delay: 0.2s;
464
+ height: 35px;
465
+ }
466
+ .visualizer .bar:nth-child(4) {
467
+ animation-delay: 0.3s;
468
+ height: 20px;
469
+ }
470
+ .visualizer .bar:nth-child(5) {
471
+ animation-delay: 0.4s;
472
+ height: 30px;
473
+ }
474
+
475
+ /* Reset Button */
476
+ .reset-btn {
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 8px;
480
+ padding: 10px 18px;
481
+ background: rgba(255, 255, 255, 0.05);
482
+ border: 1px solid var(--border-color);
483
+ border-radius: var(--radius-full);
484
+ color: var(--text-secondary);
485
+ font-size: 0.88rem;
486
+ font-weight: 500;
487
+ cursor: pointer;
488
+ transition: all 0.25s ease;
489
+ font-family: var(--font-main);
490
+ z-index: 2;
491
+ }
492
+
493
+ .reset-btn:hover {
494
+ background: rgba(255, 255, 255, 0.1);
495
+ color: var(--text-white);
496
+ border-color: var(--border-hover);
497
+ }
498
+
499
+ /* ========== CHAT MESSAGES ========== */
500
+ .chat-messages {
501
+ display: flex;
502
+ flex-direction: column;
503
+ gap: 8px;
504
+ }
505
+
506
+ .message-card {
507
+ display: flex;
508
+ align-items: flex-start;
509
+ gap: 16px;
510
+ padding: 22px 24px;
511
+ background: var(--bg-card);
512
+ border: 1px solid var(--border-color);
513
+ border-radius: var(--radius-lg);
514
+ backdrop-filter: blur(20px);
515
+ transition: border-color 0.3s ease;
516
+ }
517
+
518
+ .message-card:hover {
519
+ border-color: var(--border-hover);
520
+ }
521
+
522
+ .message-avatar {
523
+ width: 48px;
524
+ height: 48px;
525
+ border-radius: 50%;
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ font-size: 1.5rem;
530
+ flex-shrink: 0;
531
+ }
532
+
533
+ .user-avatar {
534
+ background: linear-gradient(
535
+ 135deg,
536
+ rgba(109, 92, 231, 0.2),
537
+ rgba(95, 114, 243, 0.2)
538
+ );
539
+ color: var(--primary-light);
540
+ border: 1px solid rgba(109, 92, 231, 0.3);
541
+ }
542
+
543
+ .bot-avatar {
544
+ background: linear-gradient(
545
+ 135deg,
546
+ rgba(0, 206, 201, 0.15),
547
+ rgba(95, 114, 243, 0.15)
548
+ );
549
+ color: var(--accent-light);
550
+ border: 1px solid rgba(95, 114, 243, 0.3);
551
+ }
552
+
553
+ .message-body {
554
+ flex: 1;
555
+ min-width: 0;
556
+ }
557
+
558
+ .message-label {
559
+ font-size: 0.88rem;
560
+ font-weight: 700;
561
+ color: var(--text-white);
562
+ margin-bottom: 8px;
563
+ letter-spacing: 0.3px;
564
+ }
565
+
566
+ .message-text {
567
+ line-height: 1.7;
568
+ font-size: 0.95rem;
569
+ color: var(--text-secondary);
570
+ }
571
+
572
+ .message-text p {
573
+ color: var(--text-secondary);
574
+ }
575
+
576
+ .message-text .placeholder {
577
+ color: var(--text-muted);
578
+ font-style: italic;
579
+ }
580
+
581
+ /* Message Actions (Speak / Pause) - Top Right inside bot card */
582
+ .bot-card {
583
+ position: relative;
584
+ }
585
+
586
+ .message-actions-top {
587
+ position: absolute;
588
+ top: 12px;
589
+ right: 14px;
590
+ display: flex;
591
+ gap: 6px;
592
+ z-index: 2;
593
+ }
594
+
595
+ .action-btn-sm {
596
+ display: flex;
597
+ align-items: center;
598
+ justify-content: center;
599
+ width: 34px;
600
+ height: 34px;
601
+ border-radius: 50%;
602
+ border: 1px solid var(--border-color);
603
+ background: rgba(255, 255, 255, 0.04);
604
+ color: var(--text-secondary);
605
+ font-size: 0.85rem;
606
+ cursor: pointer;
607
+ transition: all 0.25s ease;
608
+ font-family: var(--font-main);
609
+ }
610
+
611
+ .action-btn-sm:hover:not(:disabled) {
612
+ background: rgba(109, 92, 231, 0.15);
613
+ color: var(--primary-light);
614
+ border-color: var(--border-hover);
615
+ }
616
+
617
+ .action-btn-sm:disabled {
618
+ opacity: 0.3;
619
+ cursor: not-allowed;
620
+ }
621
+
622
+ .action-btn-sm.playing {
623
+ background: var(--primary);
624
+ color: white;
625
+ border-color: var(--primary);
626
+ }
627
+
628
+ /* Legacy action-btn (keep for compatibility) */
629
+ .message-actions {
630
+ display: flex;
631
+ flex-direction: column;
632
+ gap: 8px;
633
+ flex-shrink: 0;
634
+ align-self: center;
635
+ }
636
+
637
+ .action-btn {
638
+ display: flex;
639
+ flex-direction: column;
640
+ align-items: center;
641
+ gap: 4px;
642
+ width: 60px;
643
+ padding: 12px 8px;
644
+ border-radius: var(--radius-sm);
645
+ border: 1px solid var(--border-color);
646
+ background: rgba(255, 255, 255, 0.04);
647
+ color: var(--text-secondary);
648
+ font-size: 0.72rem;
649
+ font-weight: 600;
650
+ cursor: pointer;
651
+ transition: all 0.25s ease;
652
+ font-family: var(--font-main);
653
+ }
654
+
655
+ .action-btn i {
656
+ font-size: 1.1rem;
657
+ }
658
+
659
+ .action-btn:hover:not(:disabled) {
660
+ background: rgba(109, 92, 231, 0.15);
661
+ color: var(--primary-light);
662
+ border-color: var(--border-hover);
663
+ }
664
+
665
+ .action-btn:disabled {
666
+ opacity: 0.3;
667
+ cursor: not-allowed;
668
+ }
669
+
670
+ .action-btn.playing {
671
+ background: var(--primary);
672
+ color: white;
673
+ border-color: var(--primary);
674
+ }
675
+
676
+ /* ========== SOURCE BADGES ========== */
677
+ .source-badge {
678
+ display: inline-flex;
679
+ align-items: center;
680
+ gap: 6px;
681
+ padding: 5px 12px;
682
+ border-radius: var(--radius-full);
683
+ font-size: 0.78rem;
684
+ font-weight: 600;
685
+ margin-bottom: 10px;
686
+ }
687
+
688
+ .source-rag {
689
+ background: rgba(0, 206, 201, 0.15);
690
+ color: #00cec9;
691
+ border: 1px solid rgba(0, 206, 201, 0.3);
692
+ }
693
+
694
+ .source-gemini {
695
+ background: rgba(109, 92, 231, 0.15);
696
+ color: var(--primary-light);
697
+ border: 1px solid rgba(109, 92, 231, 0.3);
698
+ }
699
+
700
+ /* ========== TRANSLATED TEXT ========== */
701
+ .translated-text {
702
+ font-size: 1rem;
703
+ margin-bottom: 8px;
704
+ color: var(--text-white) !important;
705
+ }
706
+
707
+ .original-text {
708
+ color: var(--text-muted) !important;
709
+ font-size: 0.85rem;
710
+ padding-top: 8px;
711
+ border-top: 1px solid var(--border-color);
712
+ }
713
+
714
+ .original-text i {
715
+ margin-right: 4px;
716
+ color: var(--primary-light);
717
+ }
718
+
719
+ /* ========== LOADING OVERLAY ========== */
720
+ .loading-overlay {
721
+ position: fixed;
722
+ top: 0;
723
+ left: 0;
724
+ width: 100%;
725
+ height: 100%;
726
+ background: rgba(8, 8, 22, 0.88);
727
+ display: none;
728
+ align-items: center;
729
+ justify-content: center;
730
+ z-index: 1000;
731
+ backdrop-filter: blur(12px);
732
+ }
733
+
734
+ .loading-overlay.active {
735
+ display: flex;
736
+ }
737
+
738
+ .loader {
739
+ display: flex;
740
+ flex-direction: column;
741
+ align-items: center;
742
+ gap: 28px;
743
+ }
744
+
745
+ /* Loader Visual - Triple ring spinner */
746
+ .loader-visual {
747
+ position: relative;
748
+ width: 100px;
749
+ height: 100px;
750
+ }
751
+
752
+ .loader-ring {
753
+ position: absolute;
754
+ border-radius: 50%;
755
+ border: 2px solid transparent;
756
+ }
757
+
758
+ .loader-ring:nth-child(1) {
759
+ width: 100px;
760
+ height: 100px;
761
+ top: 0;
762
+ left: 0;
763
+ border-top-color: var(--primary);
764
+ border-right-color: var(--primary);
765
+ animation: loader-spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
766
+ filter: drop-shadow(0 0 6px var(--primary-glow));
767
+ }
768
+
769
+ .loader-ring:nth-child(2) {
770
+ width: 76px;
771
+ height: 76px;
772
+ top: 12px;
773
+ left: 12px;
774
+ border-bottom-color: var(--accent);
775
+ border-left-color: var(--accent);
776
+ animation: loader-spin-reverse 1s cubic-bezier(0.5, 0, 0.5, 1) infinite;
777
+ filter: drop-shadow(0 0 6px rgba(95, 114, 243, 0.35));
778
+ }
779
+
780
+ .loader-ring:nth-child(3) {
781
+ width: 52px;
782
+ height: 52px;
783
+ top: 24px;
784
+ left: 24px;
785
+ border-top-color: var(--primary-light);
786
+ border-right-color: var(--accent-light);
787
+ animation: loader-spin 0.8s cubic-bezier(0.5, 0, 0.5, 1) infinite;
788
+ filter: drop-shadow(0 0 4px rgba(162, 155, 254, 0.3));
789
+ }
790
+
791
+ .loader-core {
792
+ position: absolute;
793
+ width: 36px;
794
+ height: 36px;
795
+ top: 32px;
796
+ left: 32px;
797
+ display: flex;
798
+ align-items: center;
799
+ justify-content: center;
800
+ font-size: 1.1rem;
801
+ color: var(--primary-light);
802
+ animation: loader-pulse 1.5s ease-in-out infinite;
803
+ }
804
+
805
+ /* Loader Text */
806
+ .loader-text-area {
807
+ display: flex;
808
+ flex-direction: column;
809
+ align-items: center;
810
+ gap: 10px;
811
+ }
812
+
813
+ .loader-text-area p {
814
+ color: var(--text-white);
815
+ font-size: 1.05rem;
816
+ font-weight: 600;
817
+ letter-spacing: 0.3px;
818
+ }
819
+
820
+ .loader-dots {
821
+ display: flex;
822
+ gap: 6px;
823
+ }
824
+
825
+ .loader-dots span {
826
+ width: 6px;
827
+ height: 6px;
828
+ border-radius: 50%;
829
+ background: var(--primary-light);
830
+ animation: loader-dot-bounce 1.2s ease-in-out infinite;
831
+ }
832
+
833
+ .loader-dots span:nth-child(2) {
834
+ animation-delay: 0.15s;
835
+ }
836
+
837
+ .loader-dots span:nth-child(3) {
838
+ animation-delay: 0.3s;
839
+ }
840
+
841
+ @keyframes loader-spin {
842
+ 0% {
843
+ transform: rotate(0deg);
844
+ }
845
+ 100% {
846
+ transform: rotate(360deg);
847
+ }
848
+ }
849
+
850
+ @keyframes loader-spin-reverse {
851
+ 0% {
852
+ transform: rotate(0deg);
853
+ }
854
+ 100% {
855
+ transform: rotate(-360deg);
856
+ }
857
+ }
858
+
859
+ @keyframes loader-pulse {
860
+ 0%,
861
+ 100% {
862
+ opacity: 0.6;
863
+ transform: scale(1);
864
+ }
865
+ 50% {
866
+ opacity: 1;
867
+ transform: scale(1.15);
868
+ }
869
+ }
870
+
871
+ @keyframes loader-dot-bounce {
872
+ 0%,
873
+ 80%,
874
+ 100% {
875
+ opacity: 0.3;
876
+ transform: scale(0.8);
877
+ }
878
+ 40% {
879
+ opacity: 1;
880
+ transform: scale(1.2);
881
+ }
882
+ }
883
+
884
+ /* ========== ANIMATIONS ========== */
885
+ @keyframes pulse-glow {
886
+ 0%,
887
+ 100% {
888
+ opacity: 1;
889
+ }
890
+ 50% {
891
+ opacity: 0.5;
892
+ }
893
+ }
894
+
895
+ @keyframes blink {
896
+ 0%,
897
+ 100% {
898
+ opacity: 1;
899
+ }
900
+ 50% {
901
+ opacity: 0;
902
+ }
903
+ }
904
+
905
+ @keyframes pulse-btn {
906
+ 0%,
907
+ 100% {
908
+ transform: scale(1);
909
+ }
910
+ 50% {
911
+ transform: scale(1.06);
912
+ }
913
+ }
914
+
915
+ @keyframes ring-pulse {
916
+ 0%,
917
+ 100% {
918
+ transform: scale(1);
919
+ opacity: 0.6;
920
+ }
921
+ 50% {
922
+ transform: scale(1.05);
923
+ opacity: 1;
924
+ }
925
+ }
926
+
927
+ @keyframes visualize {
928
+ 0%,
929
+ 100% {
930
+ transform: scaleY(0.5);
931
+ }
932
+ 50% {
933
+ transform: scaleY(1.3);
934
+ }
935
+ }
936
+
937
+ @keyframes spin {
938
+ to {
939
+ transform: rotate(360deg);
940
+ }
941
+ }
942
+
943
+ /* ========== RESPONSIVE ========== */
944
+ @media (max-width: 768px) {
945
+ .app-wrapper {
946
+ padding: 14px 16px 30px;
947
+ gap: 20px;
948
+ }
949
+
950
+ .top-bar {
951
+ padding: 12px 16px;
952
+ border-radius: var(--radius-md);
953
+ }
954
+
955
+ .top-btn span {
956
+ display: none;
957
+ }
958
+
959
+ .top-btn {
960
+ padding: 10px 14px;
961
+ }
962
+
963
+ .hero.compact .hero-title {
964
+ font-size: 1.6rem;
965
+ }
966
+
967
+ .hero-desc {
968
+ font-size: 0.88rem;
969
+ }
970
+
971
+ .mic-btn {
972
+ width: 58px;
973
+ height: 58px;
974
+ font-size: 1.4rem;
975
+ }
976
+
977
+ .mic-glow-ring.ring-1 {
978
+ width: 80px;
979
+ height: 80px;
980
+ }
981
+ .mic-glow-ring.ring-2 {
982
+ width: 100px;
983
+ height: 100px;
984
+ }
985
+ .mic-glow-ring.ring-3 {
986
+ width: 120px;
987
+ height: 120px;
988
+ }
989
+
990
+ .mic-area {
991
+ padding: 24px 16px 20px;
992
+ }
993
+
994
+ .message-card {
995
+ padding: 16px 18px;
996
+ }
997
+
998
+ .message-actions {
999
+ flex-direction: row;
1000
+ }
1001
+
1002
+ .action-btn {
1003
+ width: 50px;
1004
+ padding: 10px 6px;
1005
+ font-size: 0.68rem;
1006
+ }
1007
+ }
1008
+
1009
+ @media (max-width: 480px) {
1010
+ .hero.compact .hero-title {
1011
+ font-size: 1.4rem;
1012
+ }
1013
+
1014
+ .mic-btn {
1015
+ width: 68px;
1016
+ height: 68px;
1017
+ font-size: 1.7rem;
1018
+ }
1019
+
1020
+ .status-indicator {
1021
+ padding: 6px 14px;
1022
+ }
1023
+
1024
+ .status-text {
1025
+ font-size: 0.82rem;
1026
+ }
1027
+
1028
+ .info-banner {
1029
+ padding: 14px 18px;
1030
+ }
1031
+ }
1032
+
1033
+ /* ========== SCROLLBAR ========== */
1034
+ ::-webkit-scrollbar {
1035
+ width: 6px;
1036
+ }
1037
+
1038
+ ::-webkit-scrollbar-track {
1039
+ background: transparent;
1040
+ }
1041
+
1042
+ ::-webkit-scrollbar-thumb {
1043
+ background: rgba(109, 92, 231, 0.3);
1044
+ border-radius: 3px;
1045
+ }
1046
+
1047
+ ::-webkit-scrollbar-thumb:hover {
1048
+ background: rgba(109, 92, 231, 0.5);
1049
+ }
1050
+
1051
+ ::selection {
1052
+ background: var(--primary);
1053
+ color: white;
1054
+ }
app/static/js/bg-animation.js ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ const canvas = document.getElementById('bgCanvas');
3
+ if (!canvas || typeof THREE === 'undefined') return;
4
+
5
+ const renderer = new THREE.WebGLRenderer({
6
+ canvas,
7
+ antialias: true,
8
+ alpha: true,
9
+ });
10
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
11
+ renderer.setSize(window.innerWidth, window.innerHeight);
12
+
13
+ const scene = new THREE.Scene();
14
+ const camera = new THREE.PerspectiveCamera(
15
+ 60,
16
+ window.innerWidth / window.innerHeight,
17
+ 0.1,
18
+ 1000
19
+ );
20
+ camera.position.z = 30;
21
+
22
+ // ── Floating Particles ──
23
+ const particleCount = 180;
24
+ const particleGeometry = new THREE.BufferGeometry();
25
+ const positions = new Float32Array(particleCount * 3);
26
+ const velocities = new Float32Array(particleCount * 3);
27
+ const sizes = new Float32Array(particleCount);
28
+
29
+ for (let i = 0; i < particleCount; i++) {
30
+ const i3 = i * 3;
31
+ positions[i3] = (Math.random() - 0.5) * 80;
32
+ positions[i3 + 1] = (Math.random() - 0.5) * 80;
33
+ positions[i3 + 2] = (Math.random() - 0.5) * 40;
34
+
35
+ velocities[i3] = (Math.random() - 0.5) * 0.008;
36
+ velocities[i3 + 1] = (Math.random() - 0.5) * 0.008;
37
+ velocities[i3 + 2] = (Math.random() - 0.5) * 0.004;
38
+
39
+ sizes[i] = Math.random() * 2.5 + 0.5;
40
+ }
41
+
42
+ particleGeometry.setAttribute(
43
+ 'position',
44
+ new THREE.BufferAttribute(positions, 3)
45
+ );
46
+ particleGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
47
+
48
+ const particleMaterial = new THREE.ShaderMaterial({
49
+ uniforms: {
50
+ uTime: { value: 0 },
51
+ uColor1: { value: new THREE.Color(0x6d5ce7) },
52
+ uColor2: { value: new THREE.Color(0x5f72f3) },
53
+ },
54
+ vertexShader: `
55
+ attribute float size;
56
+ uniform float uTime;
57
+ varying float vAlpha;
58
+ void main() {
59
+ vec3 pos = position;
60
+ pos.x += sin(uTime * 0.3 + position.y * 0.1) * 0.5;
61
+ pos.y += cos(uTime * 0.2 + position.x * 0.1) * 0.5;
62
+ vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
63
+ gl_PointSize = size * (20.0 / -mvPosition.z);
64
+ gl_Position = projectionMatrix * mvPosition;
65
+ vAlpha = smoothstep(0.0, 1.0, size / 3.0) * 0.6;
66
+ }
67
+ `,
68
+ fragmentShader: `
69
+ uniform vec3 uColor1;
70
+ uniform vec3 uColor2;
71
+ uniform float uTime;
72
+ varying float vAlpha;
73
+ void main() {
74
+ float d = length(gl_PointCoord - vec2(0.5));
75
+ if (d > 0.5) discard;
76
+ float alpha = smoothstep(0.5, 0.1, d) * vAlpha;
77
+ vec3 color = mix(uColor1, uColor2, sin(uTime * 0.5) * 0.5 + 0.5);
78
+ gl_FragColor = vec4(color, alpha);
79
+ }
80
+ `,
81
+ transparent: true,
82
+ depthWrite: false,
83
+ blending: THREE.AdditiveBlending,
84
+ });
85
+
86
+ const particles = new THREE.Points(particleGeometry, particleMaterial);
87
+ scene.add(particles);
88
+
89
+ // ── Subtle Connection Lines ──
90
+ const lineCount = 60;
91
+ const linePositions = new Float32Array(lineCount * 6);
92
+ const lineGeometry = new THREE.BufferGeometry();
93
+ lineGeometry.setAttribute(
94
+ 'position',
95
+ new THREE.BufferAttribute(linePositions, 3)
96
+ );
97
+
98
+ const lineMaterial = new THREE.LineBasicMaterial({
99
+ color: 0x6d5ce7,
100
+ transparent: true,
101
+ opacity: 0.06,
102
+ blending: THREE.AdditiveBlending,
103
+ });
104
+
105
+ const lines = new THREE.LineSegments(lineGeometry, lineMaterial);
106
+ scene.add(lines);
107
+
108
+ // ── Floating Mesh Ring ──
109
+ const ringGeometry = new THREE.TorusGeometry(12, 0.04, 16, 100);
110
+ const ringMaterial = new THREE.MeshBasicMaterial({
111
+ color: 0x5f72f3,
112
+ transparent: true,
113
+ opacity: 0.08,
114
+ });
115
+ const ring = new THREE.Mesh(ringGeometry, ringMaterial);
116
+ ring.position.z = -10;
117
+ scene.add(ring);
118
+
119
+ // Second ring
120
+ const ring2Geometry = new THREE.TorusGeometry(18, 0.03, 16, 120);
121
+ const ring2Material = new THREE.MeshBasicMaterial({
122
+ color: 0x6d5ce7,
123
+ transparent: true,
124
+ opacity: 0.05,
125
+ });
126
+ const ring2 = new THREE.Mesh(ring2Geometry, ring2Material);
127
+ ring2.position.z = -15;
128
+ scene.add(ring2);
129
+
130
+ // ── Mouse interaction (subtle parallax) ──
131
+ let mouseX = 0;
132
+ let mouseY = 0;
133
+ document.addEventListener('mousemove', (e) => {
134
+ mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
135
+ mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
136
+ });
137
+
138
+ // ── Animation Loop ──
139
+ const clock = new THREE.Clock();
140
+
141
+ function updateLines() {
142
+ const pos = particleGeometry.attributes.position.array;
143
+ let idx = 0;
144
+ const maxDist = 12;
145
+
146
+ for (let i = 0; i < particleCount && idx < lineCount * 6; i++) {
147
+ for (let j = i + 1; j < particleCount && idx < lineCount * 6; j++) {
148
+ const dx = pos[i * 3] - pos[j * 3];
149
+ const dy = pos[i * 3 + 1] - pos[j * 3 + 1];
150
+ const dz = pos[i * 3 + 2] - pos[j * 3 + 2];
151
+ const dist = dx * dx + dy * dy + dz * dz;
152
+
153
+ if (dist < maxDist * maxDist) {
154
+ linePositions[idx++] = pos[i * 3];
155
+ linePositions[idx++] = pos[i * 3 + 1];
156
+ linePositions[idx++] = pos[i * 3 + 2];
157
+ linePositions[idx++] = pos[j * 3];
158
+ linePositions[idx++] = pos[j * 3 + 1];
159
+ linePositions[idx++] = pos[j * 3 + 2];
160
+ }
161
+ }
162
+ }
163
+
164
+ // Zero out unused
165
+ for (let i = idx; i < lineCount * 6; i++) {
166
+ linePositions[i] = 0;
167
+ }
168
+
169
+ lineGeometry.attributes.position.needsUpdate = true;
170
+ }
171
+
172
+ function animate() {
173
+ requestAnimationFrame(animate);
174
+
175
+ const elapsed = clock.getElapsedTime();
176
+ particleMaterial.uniforms.uTime.value = elapsed;
177
+
178
+ // Move particles
179
+ const pos = particleGeometry.attributes.position.array;
180
+ for (let i = 0; i < particleCount; i++) {
181
+ const i3 = i * 3;
182
+ pos[i3] += velocities[i3];
183
+ pos[i3 + 1] += velocities[i3 + 1];
184
+ pos[i3 + 2] += velocities[i3 + 2];
185
+
186
+ // Wrap around
187
+ if (pos[i3] > 40) pos[i3] = -40;
188
+ if (pos[i3] < -40) pos[i3] = 40;
189
+ if (pos[i3 + 1] > 40) pos[i3 + 1] = -40;
190
+ if (pos[i3 + 1] < -40) pos[i3 + 1] = 40;
191
+ if (pos[i3 + 2] > 20) pos[i3 + 2] = -20;
192
+ if (pos[i3 + 2] < -20) pos[i3 + 2] = 20;
193
+ }
194
+ particleGeometry.attributes.position.needsUpdate = true;
195
+
196
+ // Update connection lines every few frames
197
+ if (Math.floor(elapsed * 10) % 3 === 0) {
198
+ updateLines();
199
+ }
200
+
201
+ // Rotate rings
202
+ ring.rotation.x = elapsed * 0.08;
203
+ ring.rotation.y = elapsed * 0.12;
204
+ ring2.rotation.x = -elapsed * 0.05;
205
+ ring2.rotation.y = elapsed * 0.08;
206
+
207
+ // Subtle mouse parallax
208
+ camera.position.x += (mouseX * 1.5 - camera.position.x) * 0.02;
209
+ camera.position.y += (-mouseY * 1.5 - camera.position.y) * 0.02;
210
+ camera.lookAt(scene.position);
211
+
212
+ renderer.render(scene, camera);
213
+ }
214
+
215
+ animate();
216
+
217
+ // ── Resize Handler ──
218
+ window.addEventListener('resize', () => {
219
+ camera.aspect = window.innerWidth / window.innerHeight;
220
+ camera.updateProjectionMatrix();
221
+ renderer.setSize(window.innerWidth, window.innerHeight);
222
+ });
223
+ })();
app/static/js/script.js ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global Variables
2
+ let mediaRecorder = null;
3
+ let audioChunks = [];
4
+ let isRecording = false;
5
+ let recordingStartTime = null;
6
+ let timerInterval = null;
7
+ let currentAudio = null;
8
+ let responseLanguage = 'en'; // 'en' for English only, 'si-en' for Sinhala+English
9
+
10
+ // DOM Elements - Voice Chat
11
+ const micBtn = document.getElementById('micBtn');
12
+ const statusIndicator = document.getElementById('statusIndicator');
13
+ const statusDot = statusIndicator.querySelector('.status-dot');
14
+ const statusText = statusIndicator.querySelector('.status-text');
15
+ const recordingTimer = document.getElementById('recordingTimer');
16
+ const timerText = recordingTimer.querySelector('.timer-text');
17
+ const visualizer = document.getElementById('visualizer');
18
+ const userText = document.getElementById('userText');
19
+ const botText = document.getElementById('botText');
20
+ const speakerBtn = document.getElementById('speakerBtn');
21
+ const pauseBtn = document.getElementById('pauseBtn');
22
+ const loadingOverlay = document.getElementById('loadingOverlay');
23
+ const loadingText = document.getElementById('loadingText');
24
+ const chatContainer = document.getElementById('chatContainer');
25
+ const resetBtn = document.getElementById('resetBtn');
26
+
27
+ // DOM Elements - Sections
28
+ const voiceChatSection = document.getElementById('voiceChatSection');
29
+
30
+ // Initialize
31
+ document.addEventListener('DOMContentLoaded', () => {
32
+ checkBrowserSupport();
33
+ setupEventListeners();
34
+ });
35
+
36
+ // Check browser support for audio recording
37
+ function checkBrowserSupport() {
38
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
39
+ showError('Your browser does not support audio recording. Please use a modern browser like Chrome or Firefox.');
40
+ micBtn.disabled = true;
41
+ }
42
+ }
43
+
44
+ // Setup Event Listeners
45
+ function setupEventListeners() {
46
+ micBtn.addEventListener('click', toggleRecording);
47
+ speakerBtn.addEventListener('click', playResponse);
48
+
49
+ // Pause button
50
+ if (pauseBtn) {
51
+ pauseBtn.addEventListener('click', pauseAudio);
52
+ }
53
+
54
+ // Reset button - also clears history
55
+ if (resetBtn) {
56
+ resetBtn.addEventListener('click', resetRecording);
57
+ }
58
+ }
59
+
60
+ // Toggle Recording
61
+ async function toggleRecording() {
62
+ if (isRecording) {
63
+ stopRecording();
64
+ } else {
65
+ await startRecording();
66
+ }
67
+ }
68
+
69
+ // Start Recording
70
+ async function startRecording() {
71
+ try {
72
+ const stream = await navigator.mediaDevices.getUserMedia({
73
+ audio: {
74
+ sampleRate: 16000,
75
+ channelCount: 1,
76
+ echoCancellation: true,
77
+ noiseSuppression: true
78
+ }
79
+ });
80
+
81
+ // Determine the best supported MIME type
82
+ let mimeType = 'audio/webm';
83
+ if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
84
+ mimeType = 'audio/webm;codecs=opus';
85
+ } else if (MediaRecorder.isTypeSupported('audio/webm')) {
86
+ mimeType = 'audio/webm';
87
+ } else if (MediaRecorder.isTypeSupported('audio/mp4')) {
88
+ mimeType = 'audio/mp4';
89
+ } else if (MediaRecorder.isTypeSupported('audio/ogg')) {
90
+ mimeType = 'audio/ogg';
91
+ }
92
+
93
+ mediaRecorder = new MediaRecorder(stream, { mimeType });
94
+ audioChunks = [];
95
+
96
+ mediaRecorder.ondataavailable = (event) => {
97
+ if (event.data.size > 0) {
98
+ audioChunks.push(event.data);
99
+ }
100
+ };
101
+
102
+ mediaRecorder.onstop = async () => {
103
+ const audioBlob = new Blob(audioChunks, { type: mimeType });
104
+ stream.getTracks().forEach(track => track.stop());
105
+ await processAudio(audioBlob);
106
+ };
107
+
108
+ mediaRecorder.start(100); // Collect data every 100ms
109
+ isRecording = true;
110
+ recordingStartTime = Date.now();
111
+
112
+ // Update UI
113
+ updateUIForRecording(true);
114
+ startTimer();
115
+
116
+ } catch (error) {
117
+ console.error('Error starting recording:', error);
118
+ showError('Could not access microphone. Please allow microphone permission.');
119
+ }
120
+ }
121
+
122
+ // Stop Recording
123
+ function stopRecording() {
124
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
125
+ mediaRecorder.stop();
126
+ isRecording = false;
127
+ stopTimer();
128
+ updateUIForRecording(false);
129
+ }
130
+ }
131
+
132
+ // Update UI for Recording State
133
+ function updateUIForRecording(recording) {
134
+ if (recording) {
135
+ micBtn.classList.add('recording');
136
+ statusDot.classList.add('recording');
137
+ statusText.textContent = 'Recording...';
138
+ recordingTimer.classList.add('active');
139
+ visualizer.classList.add('active');
140
+ } else {
141
+ micBtn.classList.remove('recording');
142
+ statusDot.classList.remove('recording');
143
+ statusText.textContent = 'Processing...';
144
+ recordingTimer.classList.remove('active');
145
+ visualizer.classList.remove('active');
146
+ }
147
+ }
148
+
149
+ // Timer Functions
150
+ function startTimer() {
151
+ timerInterval = setInterval(() => {
152
+ const elapsed = Date.now() - recordingStartTime;
153
+ const minutes = Math.floor(elapsed / 60000);
154
+ const seconds = Math.floor((elapsed % 60000) / 1000);
155
+ timerText.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
156
+ }, 100);
157
+ }
158
+
159
+ function stopTimer() {
160
+ if (timerInterval) {
161
+ clearInterval(timerInterval);
162
+ timerInterval = null;
163
+ }
164
+ timerText.textContent = '00:00';
165
+ }
166
+
167
+ // Process Audio - Send to Backend
168
+ async function processAudio(audioBlob) {
169
+ showLoading('Converting speech to text...');
170
+
171
+ try {
172
+ // Convert to WAV format for better compatibility
173
+ const wavBlob = await convertToWav(audioBlob);
174
+
175
+ // Create form data
176
+ const formData = new FormData();
177
+ formData.append('audio', wavBlob, 'recording.wav');
178
+
179
+ // Send to speech-to-text endpoint
180
+ const sttResponse = await fetch('/api/speech-to-text', {
181
+ method: 'POST',
182
+ body: formData
183
+ });
184
+
185
+ if (!sttResponse.ok) {
186
+ const error = await sttResponse.json();
187
+ throw new Error(error.detail || 'Speech recognition failed');
188
+ }
189
+
190
+ const sttResult = await sttResponse.json();
191
+ const transcribedText = sttResult.text;
192
+
193
+ // Show original transcription temporarily
194
+ displayUserText(transcribedText + ' (translating...)');
195
+
196
+ // Step 2: Translate to English
197
+ showLoading('Translating to English...');
198
+ let englishText = transcribedText;
199
+ let translationSuccess = false;
200
+
201
+ try {
202
+ const translateRes = await fetch('/api/translate-to-english', {
203
+ method: 'POST',
204
+ headers: { 'Content-Type': 'application/json' },
205
+ body: JSON.stringify({ question: transcribedText })
206
+ });
207
+
208
+ if (translateRes.ok) {
209
+ const translateData = await translateRes.json();
210
+ if (translateData.translated && translateData.english_question) {
211
+ englishText = translateData.english_question;
212
+ translationSuccess = true;
213
+ } else if (translateData.english_question && translateData.english_question !== transcribedText) {
214
+ // Even if translated flag is false, check if we got different text
215
+ englishText = translateData.english_question;
216
+ translationSuccess = true;
217
+ }
218
+ }
219
+ } catch (translateError) {
220
+ console.error('Translation error:', translateError);
221
+ }
222
+
223
+ // Display both original and English if translation succeeded, otherwise just show original
224
+ if (translationSuccess && englishText !== transcribedText) {
225
+ displayUserTextWithOriginal(transcribedText, englishText);
226
+ } else {
227
+ displayUserText(transcribedText + ' (translation failed - using original)');
228
+ }
229
+
230
+ // Step 3: Use RAG first, fallback to Gemini API
231
+ showLoading('Searching knowledge base...');
232
+ const ragResponse = await fetch('/api/rag/ask', {
233
+ method: 'POST',
234
+ headers: {
235
+ 'Content-Type': 'application/json'
236
+ },
237
+ body: JSON.stringify({
238
+ question: englishText,
239
+ response_lang: responseLanguage // 'en' or 'si-en'
240
+ })
241
+ });
242
+
243
+ if (!ragResponse.ok) {
244
+ const error = await ragResponse.json();
245
+ throw new Error(error.detail || 'Query failed');
246
+ }
247
+
248
+ const ragResult = await ragResponse.json();
249
+ const botResponse = ragResult.answer;
250
+ const source = ragResult.source; // 'rag', 'gemini', or 'none'
251
+
252
+ // Display bot response with source indicator
253
+ displayBotTextWithSource(botResponse, source);
254
+
255
+ // Enable speaker button
256
+ speakerBtn.disabled = false;
257
+
258
+ // Update status
259
+ updateStatus('ready', 'Ready');
260
+
261
+ } catch (error) {
262
+ console.error('Processing error:', error);
263
+ showError(error.message);
264
+ updateStatus('ready', 'Ready');
265
+ } finally {
266
+ hideLoading();
267
+ }
268
+ }
269
+
270
+ // Convert audio blob to WAV format
271
+ async function convertToWav(audioBlob) {
272
+ return new Promise((resolve, reject) => {
273
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
274
+ const reader = new FileReader();
275
+
276
+ reader.onload = async () => {
277
+ try {
278
+ const arrayBuffer = reader.result;
279
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
280
+
281
+ // Resample to 16kHz for Whisper model
282
+ const targetSampleRate = 16000;
283
+ const offlineContext = new OfflineAudioContext(
284
+ 1, // mono
285
+ audioBuffer.duration * targetSampleRate,
286
+ targetSampleRate
287
+ );
288
+
289
+ const source = offlineContext.createBufferSource();
290
+ source.buffer = audioBuffer;
291
+ source.connect(offlineContext.destination);
292
+ source.start(0);
293
+
294
+ const renderedBuffer = await offlineContext.startRendering();
295
+ const wavBlob = audioBufferToWav(renderedBuffer);
296
+ resolve(wavBlob);
297
+ } catch (error) {
298
+ // If conversion fails, return original blob
299
+ console.warn('WAV conversion failed, using original format:', error);
300
+ resolve(audioBlob);
301
+ }
302
+ };
303
+
304
+ reader.onerror = () => reject(reader.error);
305
+ reader.readAsArrayBuffer(audioBlob);
306
+ });
307
+ }
308
+
309
+ // Convert AudioBuffer to WAV Blob
310
+ function audioBufferToWav(buffer) {
311
+ const numChannels = buffer.numberOfChannels;
312
+ const sampleRate = buffer.sampleRate;
313
+ const format = 1; // PCM
314
+ const bitDepth = 16;
315
+
316
+ const bytesPerSample = bitDepth / 8;
317
+ const blockAlign = numChannels * bytesPerSample;
318
+
319
+ const dataLength = buffer.length * blockAlign;
320
+ const bufferLength = 44 + dataLength;
321
+
322
+ const arrayBuffer = new ArrayBuffer(bufferLength);
323
+ const view = new DataView(arrayBuffer);
324
+
325
+ // WAV header
326
+ writeString(view, 0, 'RIFF');
327
+ view.setUint32(4, 36 + dataLength, true);
328
+ writeString(view, 8, 'WAVE');
329
+ writeString(view, 12, 'fmt ');
330
+ view.setUint32(16, 16, true);
331
+ view.setUint16(20, format, true);
332
+ view.setUint16(22, numChannels, true);
333
+ view.setUint32(24, sampleRate, true);
334
+ view.setUint32(28, sampleRate * blockAlign, true);
335
+ view.setUint16(32, blockAlign, true);
336
+ view.setUint16(34, bitDepth, true);
337
+ writeString(view, 36, 'data');
338
+ view.setUint32(40, dataLength, true);
339
+
340
+ // Write audio data
341
+ const channelData = buffer.getChannelData(0);
342
+ let offset = 44;
343
+ for (let i = 0; i < channelData.length; i++) {
344
+ const sample = Math.max(-1, Math.min(1, channelData[i]));
345
+ view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
346
+ offset += 2;
347
+ }
348
+
349
+ return new Blob([arrayBuffer], { type: 'audio/wav' });
350
+ }
351
+
352
+ function writeString(view, offset, string) {
353
+ for (let i = 0; i < string.length; i++) {
354
+ view.setUint8(offset + i, string.charCodeAt(i));
355
+ }
356
+ }
357
+
358
+ // Display Functions
359
+ function displayUserText(text) {
360
+ userText.innerHTML = `<p>${escapeHtml(text)}</p>`;
361
+ }
362
+
363
+ function displayUserTextWithOriginal(originalText, englishText) {
364
+ userText.innerHTML = `
365
+ <p>${escapeHtml(originalText)}</p>
366
+ `;
367
+ }
368
+
369
+ function displayBotText(text) {
370
+ // Convert markdown-like formatting to HTML
371
+ const formattedText = formatText(text);
372
+ botText.innerHTML = formattedText;
373
+ }
374
+
375
+ function displayBotTextWithSource(text, source) {
376
+ // Convert markdown-like formatting to HTML with source badge
377
+ const formattedText = formatText(text);
378
+ let sourceLabel = '';
379
+ if (source === 'rag') {
380
+ sourceLabel = '<span class="source-badge source-rag"><i class="fas fa-database"></i> From Documents</span>';
381
+ } else if (source === 'gemini') {
382
+ sourceLabel = '<span class="source-badge source-gemini"><i class="fas fa-brain"></i> From AI</span>';
383
+ }
384
+ botText.innerHTML = sourceLabel + formattedText;
385
+ }
386
+
387
+ function formatText(text) {
388
+ // Basic formatting
389
+ let formatted = escapeHtml(text);
390
+
391
+ // Convert line breaks
392
+ formatted = formatted.replace(/\n/g, '<br>');
393
+
394
+ // Convert **bold** to <strong>
395
+ formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
396
+
397
+ // Convert *italic* to <em>
398
+ formatted = formatted.replace(/\*(.*?)\*/g, '<em>$1</em>');
399
+
400
+ return `<p>${formatted}</p>`;
401
+ }
402
+
403
+ function escapeHtml(text) {
404
+ const div = document.createElement('div');
405
+ div.textContent = text;
406
+ return div.innerHTML;
407
+ }
408
+
409
+ // Play Response using TTS
410
+ async function playResponse() {
411
+ const text = botText.textContent || botText.innerText;
412
+
413
+ if (!text || text.includes('will appear here')) {
414
+ return;
415
+ }
416
+
417
+ // If paused, resume
418
+ if (currentAudio && currentAudio.paused) {
419
+ currentAudio.play();
420
+ speakerBtn.classList.add('playing');
421
+ pauseBtn.classList.remove('paused');
422
+ pauseBtn.querySelector('i').className = 'fas fa-pause';
423
+ return;
424
+ }
425
+
426
+ // Stop current audio if playing
427
+ if (currentAudio) {
428
+ currentAudio.pause();
429
+ currentAudio = null;
430
+ speakerBtn.classList.remove('playing');
431
+ }
432
+
433
+ speakerBtn.classList.add('playing');
434
+ speakerBtn.querySelector('i').className = 'fas fa-spinner fa-spin';
435
+
436
+ try {
437
+ const ttsLang = responseLanguage === 'en' ? 'en' : 'si';
438
+
439
+ const response = await fetch('/api/text-to-speech', {
440
+ method: 'POST',
441
+ headers: {
442
+ 'Content-Type': 'application/json'
443
+ },
444
+ body: JSON.stringify({
445
+ text: text,
446
+ lang: ttsLang
447
+ })
448
+ });
449
+
450
+ if (!response.ok) {
451
+ throw new Error('Text-to-speech failed');
452
+ }
453
+
454
+ const audioBlob = await response.blob();
455
+ const audioUrl = URL.createObjectURL(audioBlob);
456
+
457
+ currentAudio = new Audio(audioUrl);
458
+
459
+ currentAudio.onended = () => {
460
+ speakerBtn.classList.remove('playing');
461
+ speakerBtn.querySelector('i').className = 'fas fa-volume-up';
462
+ pauseBtn.classList.remove('paused');
463
+ pauseBtn.querySelector('i').className = 'fas fa-pause';
464
+ URL.revokeObjectURL(audioUrl);
465
+ currentAudio = null;
466
+ };
467
+
468
+ currentAudio.onerror = () => {
469
+ speakerBtn.classList.remove('playing');
470
+ speakerBtn.querySelector('i').className = 'fas fa-volume-up';
471
+ showError('Failed to play audio');
472
+ };
473
+
474
+ await currentAudio.play();
475
+ speakerBtn.querySelector('i').className = 'fas fa-volume-up';
476
+
477
+ } catch (error) {
478
+ console.error('TTS error:', error);
479
+ speakerBtn.classList.remove('playing');
480
+ speakerBtn.querySelector('i').className = 'fas fa-volume-up';
481
+ showError('Text-to-speech failed');
482
+ }
483
+ }
484
+
485
+ // Pause Audio Playback
486
+ function pauseAudio() {
487
+ if (currentAudio && !currentAudio.paused) {
488
+ currentAudio.pause();
489
+ speakerBtn.classList.remove('playing');
490
+ pauseBtn.classList.add('paused');
491
+ pauseBtn.querySelector('i').className = 'fas fa-play';
492
+ } else if (currentAudio && currentAudio.paused) {
493
+ currentAudio.play();
494
+ speakerBtn.classList.add('playing');
495
+ pauseBtn.classList.remove('paused');
496
+ pauseBtn.querySelector('i').className = 'fas fa-pause';
497
+ }
498
+ }
499
+
500
+ // Reset Recording / Stop current action
501
+ function resetRecording() {
502
+ if (isRecording) {
503
+ stopRecording();
504
+ }
505
+ if (currentAudio) {
506
+ currentAudio.pause();
507
+ currentAudio = null;
508
+ speakerBtn.classList.remove('playing');
509
+ }
510
+ updateStatus('ready', 'Ready');
511
+ clearHistory();
512
+ }
513
+
514
+ // Clear Conversation History
515
+ async function clearHistory() {
516
+ try {
517
+ const response = await fetch('/api/clear-history', {
518
+ method: 'POST'
519
+ });
520
+
521
+ if (response.ok) {
522
+ // Reset UI
523
+ userText.innerHTML = '<p class="placeholder">Your transcribed message will appear here...</p>';
524
+ botText.innerHTML = '<p class="placeholder">Bot response will appear here...</p>';
525
+ speakerBtn.disabled = true;
526
+
527
+ // Show confirmation
528
+ showSuccess('Conversation history cleared');
529
+ }
530
+ } catch (error) {
531
+ console.error('Error clearing history:', error);
532
+ showError('Failed to clear history');
533
+ }
534
+ }
535
+
536
+ // Loading Functions
537
+ function showLoading(message = 'Processing...') {
538
+ loadingText.textContent = message;
539
+ loadingOverlay.classList.add('active');
540
+ }
541
+
542
+ function hideLoading() {
543
+ loadingOverlay.classList.remove('active');
544
+ }
545
+
546
+ // Status Update
547
+ function updateStatus(state, text) {
548
+ statusDot.className = 'status-dot';
549
+ if (state !== 'ready') {
550
+ statusDot.classList.add(state);
551
+ }
552
+ statusText.textContent = text;
553
+ }
554
+
555
+ // Notification Functions
556
+ function showError(message) {
557
+ // Create toast notification
558
+ showToast(message, 'error');
559
+ // Clear user and bot input fields after 2 seconds
560
+ setTimeout(() => {
561
+ if (userText) {
562
+ userText.innerHTML = '<p class="placeholder">Your transcribed message will appear here...</p>';
563
+ }
564
+ if (botText) {
565
+ botText.innerHTML = '<p class="placeholder">Bot response will appear here...</p>';
566
+ }
567
+ }, 2000);
568
+ }
569
+
570
+ function showSuccess(message) {
571
+ showToast(message, 'success');
572
+ }
573
+
574
+ function showToast(message, type = 'info') {
575
+ // Remove existing toasts
576
+ const existingToasts = document.querySelectorAll('.toast');
577
+ existingToasts.forEach(t => t.remove());
578
+
579
+ // Create toast element
580
+ const toast = document.createElement('div');
581
+ toast.className = `toast toast-${type}`;
582
+ toast.innerHTML = `
583
+ <i class="fas ${type === 'error' ? 'fa-exclamation-circle' : 'fa-check-circle'}"></i>
584
+ <span>${message}</span>
585
+ `;
586
+
587
+ // Add styles
588
+ toast.style.cssText = `
589
+ position: fixed;
590
+ bottom: 20px;
591
+ left: 50%;
592
+ transform: translateX(-50%);
593
+ padding: 12px 24px;
594
+ background: ${type === 'error' ? '#ef4444' : '#22c55e'};
595
+ color: white;
596
+ border-radius: 8px;
597
+ display: flex;
598
+ align-items: center;
599
+ gap: 10px;
600
+ z-index: 2000;
601
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
602
+ animation: slideUp 0.3s ease;
603
+ `;
604
+
605
+ // Add animation keyframes if not exists
606
+ if (!document.getElementById('toast-styles')) {
607
+ const style = document.createElement('style');
608
+ style.id = 'toast-styles';
609
+ style.textContent = `
610
+ @keyframes slideUp {
611
+ from { transform: translateX(-50%) translateY(100%); opacity: 0; }
612
+ to { transform: translateX(-50%) translateY(0); opacity: 1; }
613
+ }
614
+ `;
615
+ document.head.appendChild(style);
616
+ }
617
+
618
+ document.body.appendChild(toast);
619
+
620
+ // Remove after 4 seconds
621
+ setTimeout(() => {
622
+ toast.style.opacity = '0';
623
+ toast.style.transition = 'opacity 0.3s ease';
624
+ setTimeout(() => toast.remove(), 300);
625
+ }, 4000);
626
+ }
627
+
628
+
app/templates/admin.html ADDED
@@ -0,0 +1,769 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>RAG Admin Panel - Document Management</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
9
+ <style>
10
+ :root {
11
+ --primary-color: #6d5ce7;
12
+ --primary-hover: #4a3db0;
13
+ --primary-light: #a29bfe;
14
+ --primary-glow: rgba(109, 92, 231, 0.35);
15
+ --accent: #5f72f3;
16
+ --accent-light: #7c8cf8;
17
+ --success-color: #00cec9;
18
+ --danger-color: #ff6b6b;
19
+ --warning-color: #feca57;
20
+ --bg-dark: #080816;
21
+ --bg-card: rgba(15, 15, 35, 0.65);
22
+ --text-primary: #eef0ff;
23
+ --text-secondary: #a0a8c8;
24
+ --border-color: rgba(109, 92, 231, 0.12);
25
+ --border-hover: rgba(109, 92, 231, 0.35);
26
+ }
27
+
28
+ * {
29
+ margin: 0;
30
+ padding: 0;
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ body {
35
+ font-family: 'Inter', sans-serif;
36
+ background: var(--bg-dark);
37
+ min-height: 100vh;
38
+ color: var(--text-primary);
39
+ padding: 20px;
40
+ overflow-x: hidden;
41
+ }
42
+
43
+ #bgCanvas {
44
+ position: fixed;
45
+ top: 0;
46
+ left: 0;
47
+ width: 100%;
48
+ height: 100%;
49
+ z-index: 0;
50
+ pointer-events: none;
51
+ }
52
+
53
+ .container {
54
+ max-width: 900px;
55
+ margin: 0 auto;
56
+ position: relative;
57
+ z-index: 1;
58
+ }
59
+
60
+ .header {
61
+ text-align: center;
62
+ margin-bottom: 40px;
63
+ padding: 30px;
64
+ background: var(--bg-card);
65
+ border-radius: 16px;
66
+ border: 1px solid var(--border-color);
67
+ backdrop-filter: blur(20px);
68
+ }
69
+
70
+ .header h1 {
71
+ font-size: 2rem;
72
+ margin-bottom: 10px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ gap: 12px;
77
+ }
78
+
79
+ .header h1 i {
80
+ color: var(--primary-color);
81
+ }
82
+
83
+ .header p {
84
+ color: var(--text-secondary);
85
+ }
86
+
87
+ .status-card {
88
+ background: var(--bg-card);
89
+ border-radius: 12px;
90
+ padding: 20px;
91
+ margin-bottom: 20px;
92
+ border: 1px solid var(--border-color);
93
+ backdrop-filter: blur(20px);
94
+ }
95
+
96
+ .status-header {
97
+ display: flex;
98
+ justify-content: space-between;
99
+ align-items: center;
100
+ margin-bottom: 15px;
101
+ }
102
+
103
+ .status-header h3 {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 10px;
107
+ }
108
+
109
+ .status-indicator {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 8px;
113
+ padding: 6px 12px;
114
+ border-radius: 20px;
115
+ font-size: 0.85rem;
116
+ }
117
+
118
+ .status-indicator.ready {
119
+ background: rgba(0, 206, 201, 0.2);
120
+ color: var(--success-color);
121
+ }
122
+
123
+ .status-indicator.empty {
124
+ background: rgba(254, 202, 87, 0.2);
125
+ color: var(--warning-color);
126
+ }
127
+
128
+ .status-dot {
129
+ width: 8px;
130
+ height: 8px;
131
+ border-radius: 50%;
132
+ background: currentColor;
133
+ }
134
+
135
+ .upload-section {
136
+ background: var(--bg-card);
137
+ border-radius: 12px;
138
+ padding: 30px;
139
+ margin-bottom: 20px;
140
+ border: 1px solid var(--border-color);
141
+ backdrop-filter: blur(20px);
142
+ }
143
+
144
+ .upload-box {
145
+ border: 2px dashed var(--border-color);
146
+ border-radius: 12px;
147
+ padding: 50px 20px;
148
+ text-align: center;
149
+ cursor: pointer;
150
+ transition: all 0.3s ease;
151
+ }
152
+
153
+ .upload-box:hover {
154
+ border-color: var(--primary-color);
155
+ background: rgba(109, 92, 231, 0.1);
156
+ }
157
+
158
+ .upload-box.dragover {
159
+ border-color: var(--primary-color);
160
+ background: rgba(109, 92, 231, 0.2);
161
+ }
162
+
163
+ .upload-box i {
164
+ font-size: 3rem;
165
+ color: var(--primary-color);
166
+ margin-bottom: 15px;
167
+ }
168
+
169
+ .upload-box h3 {
170
+ margin-bottom: 8px;
171
+ }
172
+
173
+ .upload-box p {
174
+ color: var(--text-secondary);
175
+ font-size: 0.9rem;
176
+ }
177
+
178
+ .documents-section {
179
+ background: var(--bg-card);
180
+ border-radius: 12px;
181
+ padding: 20px;
182
+ border: 1px solid var(--border-color);
183
+ backdrop-filter: blur(20px);
184
+ }
185
+
186
+ .documents-section h3 {
187
+ margin-bottom: 15px;
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 10px;
191
+ }
192
+
193
+ .document-list {
194
+ display: flex;
195
+ flex-direction: column;
196
+ gap: 10px;
197
+ }
198
+
199
+ .document-item {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: space-between;
203
+ padding: 15px;
204
+ background: rgba(8, 8, 22, 0.5);
205
+ border-radius: 8px;
206
+ border: 1px solid var(--border-color);
207
+ }
208
+
209
+ .document-info {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 12px;
213
+ }
214
+
215
+ .document-info i {
216
+ font-size: 1.5rem;
217
+ color: var(--danger-color);
218
+ }
219
+
220
+ .document-name {
221
+ font-weight: 500;
222
+ }
223
+
224
+ .delete-btn {
225
+ background: rgba(255, 107, 107, 0.2);
226
+ border: 1px solid var(--danger-color);
227
+ color: var(--danger-color);
228
+ padding: 8px 16px;
229
+ border-radius: 6px;
230
+ cursor: pointer;
231
+ transition: all 0.3s ease;
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 6px;
235
+ }
236
+
237
+ .delete-btn:hover {
238
+ background: var(--danger-color);
239
+ color: white;
240
+ }
241
+
242
+ .clear-all-btn {
243
+ background: var(--danger-color);
244
+ border: none;
245
+ color: white;
246
+ padding: 10px 20px;
247
+ border-radius: 8px;
248
+ cursor: pointer;
249
+ font-weight: 500;
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 8px;
253
+ transition: all 0.3s ease;
254
+ }
255
+
256
+ .clear-all-btn:hover {
257
+ background: #e55050;
258
+ }
259
+
260
+ .rebuild-btn {
261
+ background: var(--primary-color);
262
+ border: none;
263
+ color: white;
264
+ padding: 10px 20px;
265
+ border-radius: 8px;
266
+ cursor: pointer;
267
+ font-weight: 500;
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ transition: all 0.3s ease;
272
+ }
273
+
274
+ .rebuild-btn:hover {
275
+ background: var(--primary-hover);
276
+ }
277
+
278
+ .empty-state {
279
+ text-align: center;
280
+ padding: 40px;
281
+ color: var(--text-secondary);
282
+ }
283
+
284
+ .empty-state i {
285
+ font-size: 3rem;
286
+ margin-bottom: 15px;
287
+ opacity: 0.5;
288
+ }
289
+
290
+ .toast {
291
+ position: fixed;
292
+ bottom: 20px;
293
+ right: 20px;
294
+ padding: 15px 25px;
295
+ border-radius: 8px;
296
+ color: white;
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 10px;
300
+ z-index: 1000;
301
+ animation: slideIn 0.3s ease;
302
+ }
303
+
304
+ .toast.success {
305
+ background: var(--success-color);
306
+ }
307
+
308
+ .toast.error {
309
+ background: var(--danger-color);
310
+ }
311
+
312
+ @keyframes slideIn {
313
+ from {
314
+ transform: translateX(100%);
315
+ opacity: 0;
316
+ }
317
+ to {
318
+ transform: translateX(0);
319
+ opacity: 1;
320
+ }
321
+ }
322
+
323
+ .loading-overlay {
324
+ position: fixed;
325
+ top: 0;
326
+ left: 0;
327
+ width: 100%;
328
+ height: 100%;
329
+ background: rgba(8, 8, 22, 0.88);
330
+ display: none;
331
+ justify-content: center;
332
+ align-items: center;
333
+ z-index: 999;
334
+ backdrop-filter: blur(12px);
335
+ }
336
+
337
+ .loading-content {
338
+ display: flex;
339
+ flex-direction: column;
340
+ align-items: center;
341
+ gap: 28px;
342
+ }
343
+
344
+ .loader-visual {
345
+ position: relative;
346
+ width: 100px;
347
+ height: 100px;
348
+ }
349
+
350
+ .loader-ring {
351
+ position: absolute;
352
+ border-radius: 50%;
353
+ border: 2px solid transparent;
354
+ }
355
+
356
+ .loader-ring:nth-child(1) {
357
+ width: 100px;
358
+ height: 100px;
359
+ top: 0; left: 0;
360
+ border-top-color: var(--primary-color);
361
+ border-right-color: var(--primary-color);
362
+ animation: lspin 1.2s cubic-bezier(0.5,0,0.5,1) infinite;
363
+ filter: drop-shadow(0 0 6px var(--primary-glow));
364
+ }
365
+
366
+ .loader-ring:nth-child(2) {
367
+ width: 76px;
368
+ height: 76px;
369
+ top: 12px; left: 12px;
370
+ border-bottom-color: var(--accent);
371
+ border-left-color: var(--accent);
372
+ animation: lspin-r 1s cubic-bezier(0.5,0,0.5,1) infinite;
373
+ filter: drop-shadow(0 0 6px rgba(95,114,243,0.35));
374
+ }
375
+
376
+ .loader-ring:nth-child(3) {
377
+ width: 52px;
378
+ height: 52px;
379
+ top: 24px; left: 24px;
380
+ border-top-color: var(--primary-light);
381
+ border-right-color: var(--accent-light);
382
+ animation: lspin 0.8s cubic-bezier(0.5,0,0.5,1) infinite;
383
+ filter: drop-shadow(0 0 4px rgba(162,155,254,0.3));
384
+ }
385
+
386
+ .loader-core {
387
+ position: absolute;
388
+ width: 36px; height: 36px;
389
+ top: 32px; left: 32px;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ font-size: 1.1rem;
394
+ color: var(--primary-light);
395
+ animation: lpulse 1.5s ease-in-out infinite;
396
+ }
397
+
398
+ .loader-text-area {
399
+ display: flex;
400
+ flex-direction: column;
401
+ align-items: center;
402
+ gap: 10px;
403
+ }
404
+
405
+ .loader-text-area p {
406
+ color: var(--text-primary);
407
+ font-size: 1.05rem;
408
+ font-weight: 600;
409
+ }
410
+
411
+ .loader-dots {
412
+ display: flex;
413
+ gap: 6px;
414
+ }
415
+
416
+ .loader-dots span {
417
+ width: 6px; height: 6px;
418
+ border-radius: 50%;
419
+ background: var(--primary-light);
420
+ animation: ldot 1.2s ease-in-out infinite;
421
+ }
422
+
423
+ .loader-dots span:nth-child(2) { animation-delay: 0.15s; }
424
+ .loader-dots span:nth-child(3) { animation-delay: 0.3s; }
425
+
426
+ @keyframes lspin {
427
+ 0% { transform: rotate(0deg); }
428
+ 100% { transform: rotate(360deg); }
429
+ }
430
+
431
+ @keyframes lspin-r {
432
+ 0% { transform: rotate(0deg); }
433
+ 100% { transform: rotate(-360deg); }
434
+ }
435
+
436
+ @keyframes lpulse {
437
+ 0%,100% { opacity: 0.6; transform: scale(1); }
438
+ 50% { opacity: 1; transform: scale(1.15); }
439
+ }
440
+
441
+ @keyframes ldot {
442
+ 0%,80%,100% { opacity: 0.3; transform: scale(0.8); }
443
+ 40% { opacity: 1; transform: scale(1.2); }
444
+ }
445
+
446
+ .spinner {
447
+ width: 50px;
448
+ height: 50px;
449
+ border: 4px solid var(--border-color);
450
+ border-top-color: var(--primary-color);
451
+ border-radius: 50%;
452
+ animation: spin 1s linear infinite;
453
+ margin: 0 auto 15px;
454
+ }
455
+
456
+ @keyframes spin {
457
+ to { transform: rotate(360deg); }
458
+ }
459
+
460
+ .back-link {
461
+ display: inline-flex;
462
+ align-items: center;
463
+ gap: 8px;
464
+ color: var(--text-secondary);
465
+ text-decoration: none;
466
+ margin-bottom: 20px;
467
+ transition: color 0.3s;
468
+ }
469
+
470
+ .back-link:hover {
471
+ color: var(--primary-color);
472
+ }
473
+ </style>
474
+ </head>
475
+ <body>
476
+ <!-- Three.js Background Canvas -->
477
+ <canvas id="bgCanvas"></canvas>
478
+
479
+ <div class="container">
480
+ <a href="http://localhost:8000" class="back-link" target="_blank">
481
+ <i class="fas fa-arrow-left"></i> Open Chatbot (Port 8000)
482
+ </a>
483
+
484
+ <div class="header">
485
+ <h1><i class="fas fa-database"></i> RAG Admin Panel</h1>
486
+ <p>Upload and manage PDF documents for the RAG knowledge base</p>
487
+ </div>
488
+
489
+ <div class="status-card">
490
+ <div class="status-header">
491
+ <h3><i class="fas fa-chart-bar"></i> System Status</h3>
492
+ <div class="status-indicator empty" id="statusIndicator">
493
+ <span class="status-dot"></span>
494
+ <span id="statusText">No documents</span>
495
+ </div>
496
+ </div>
497
+ <div id="statsInfo">
498
+ <p style="color: var(--text-secondary);">Documents: <span id="docCount">0</span></p>
499
+ </div>
500
+ </div>
501
+
502
+ <div class="upload-section">
503
+ <div class="upload-box" id="uploadBox">
504
+ <i class="fas fa-cloud-upload-alt"></i>
505
+ <h3>Upload PDF Document</h3>
506
+ <p>Drag & drop your PDF here or click to browse</p>
507
+ <input type="file" id="fileInput" accept=".pdf" hidden>
508
+ </div>
509
+ </div>
510
+
511
+ <div class="documents-section">
512
+ <div class="status-header">
513
+ <h3><i class="fas fa-folder-open"></i> Uploaded Documents</h3>
514
+ <div style="display: flex; gap: 10px;">
515
+ <button class="rebuild-btn" id="rebuildBtn" style="display: none;">
516
+ <i class="fas fa-gears"></i> Rebuild RAG
517
+ </button>
518
+ <button class="clear-all-btn" id="clearAllBtn" style="display: none;">
519
+ <i class="fas fa-trash-alt"></i> Clear All
520
+ </button>
521
+ </div>
522
+ </div>
523
+ <div class="document-list" id="documentList">
524
+ <div class="empty-state">
525
+ <i class="fas fa-file-pdf"></i>
526
+ <p>No documents uploaded yet</p>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ </div>
531
+
532
+ <div class="loading-overlay" id="loadingOverlay">
533
+ <div class="loading-content">
534
+ <div class="loader-visual">
535
+ <div class="loader-ring"></div>
536
+ <div class="loader-ring"></div>
537
+ <div class="loader-ring"></div>
538
+ <div class="loader-core">
539
+ <i class="fas fa-brain"></i>
540
+ </div>
541
+ </div>
542
+ <div class="loader-text-area">
543
+ <p id="loadingText">Processing...</p>
544
+ <div class="loader-dots">
545
+ <span></span><span></span><span></span>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ </div>
550
+
551
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
552
+ <script src="/static/js/bg-animation.js"></script>
553
+
554
+ <script>
555
+ const uploadBox = document.getElementById('uploadBox');
556
+ const fileInput = document.getElementById('fileInput');
557
+ const documentList = document.getElementById('documentList');
558
+ const statusIndicator = document.getElementById('statusIndicator');
559
+ const statusText = document.getElementById('statusText');
560
+ const docCount = document.getElementById('docCount');
561
+ const clearAllBtn = document.getElementById('clearAllBtn');
562
+ const rebuildBtn = document.getElementById('rebuildBtn');
563
+ const loadingOverlay = document.getElementById('loadingOverlay');
564
+ const loadingText = document.getElementById('loadingText');
565
+
566
+ // Initialize
567
+ document.addEventListener('DOMContentLoaded', loadStatus);
568
+
569
+ // Upload box events
570
+ uploadBox.addEventListener('click', () => fileInput.click());
571
+ fileInput.addEventListener('change', handleFileSelect);
572
+
573
+ // Drag and drop
574
+ uploadBox.addEventListener('dragover', (e) => {
575
+ e.preventDefault();
576
+ uploadBox.classList.add('dragover');
577
+ });
578
+ uploadBox.addEventListener('dragleave', () => {
579
+ uploadBox.classList.remove('dragover');
580
+ });
581
+ uploadBox.addEventListener('drop', (e) => {
582
+ e.preventDefault();
583
+ uploadBox.classList.remove('dragover');
584
+ if (e.dataTransfer.files.length > 0) {
585
+ handleFileUpload(e.dataTransfer.files[0]);
586
+ }
587
+ });
588
+
589
+ // Clear all
590
+ clearAllBtn.addEventListener('click', clearAllDocuments);
591
+ rebuildBtn.addEventListener('click', rebuildRag);
592
+
593
+ function handleFileSelect(e) {
594
+ if (e.target.files.length > 0) {
595
+ handleFileUpload(e.target.files[0]);
596
+ }
597
+ }
598
+
599
+ async function handleFileUpload(file) {
600
+ if (!file.name.toLowerCase().endsWith('.pdf')) {
601
+ showToast('Please upload a PDF file', 'error');
602
+ return;
603
+ }
604
+
605
+ showLoading('Uploading and processing PDF...');
606
+
607
+ const formData = new FormData();
608
+ formData.append('file', file);
609
+
610
+ try {
611
+ const response = await fetch('/api/upload', {
612
+ method: 'POST',
613
+ body: formData
614
+ });
615
+
616
+ if (!response.ok) {
617
+ const error = await response.json();
618
+ throw new Error(error.detail || 'Upload failed');
619
+ }
620
+
621
+ const result = await response.json();
622
+ showToast(result.message, 'success');
623
+ loadStatus();
624
+
625
+ } catch (error) {
626
+ console.error('Upload error:', error);
627
+ showToast(error.message, 'error');
628
+ } finally {
629
+ hideLoading();
630
+ fileInput.value = '';
631
+ }
632
+ }
633
+
634
+ async function loadStatus() {
635
+ try {
636
+ const response = await fetch('/api/status');
637
+ const status = await response.json();
638
+
639
+ docCount.textContent = status.documents_count;
640
+
641
+ if (status.initialized && status.documents_count > 0) {
642
+ statusIndicator.className = 'status-indicator ready';
643
+ statusText.textContent = 'Ready';
644
+ clearAllBtn.style.display = 'flex';
645
+ rebuildBtn.style.display = 'flex';
646
+ renderDocuments(status.documents);
647
+ } else {
648
+ statusIndicator.className = 'status-indicator empty';
649
+ statusText.textContent = 'No documents';
650
+ clearAllBtn.style.display = 'none';
651
+ rebuildBtn.style.display = 'none';
652
+ documentList.innerHTML = `
653
+ <div class="empty-state">
654
+ <i class="fas fa-file-pdf"></i>
655
+ <p>No documents uploaded yet</p>
656
+ </div>
657
+ `;
658
+ }
659
+ } catch (error) {
660
+ console.error('Failed to load status:', error);
661
+ }
662
+ }
663
+
664
+ function renderDocuments(documents) {
665
+ if (!documents || documents.length === 0) {
666
+ documentList.innerHTML = `
667
+ <div class="empty-state">
668
+ <i class="fas fa-file-pdf"></i>
669
+ <p>No documents uploaded yet</p>
670
+ </div>
671
+ `;
672
+ return;
673
+ }
674
+
675
+ documentList.innerHTML = documents.map(doc => `
676
+ <div class="document-item">
677
+ <div class="document-info">
678
+ <i class="fas fa-file-pdf"></i>
679
+ <span class="document-name">${doc}</span>
680
+ </div>
681
+ <button class="delete-btn" onclick="deleteDocument('${doc}')">
682
+ <i class="fas fa-trash"></i> Delete
683
+ </button>
684
+ </div>
685
+ `).join('');
686
+ }
687
+
688
+ async function deleteDocument(filename) {
689
+ if (!confirm(`Delete "${filename}"?`)) return;
690
+
691
+ try {
692
+ const response = await fetch(`/api/document/${encodeURIComponent(filename)}`, {
693
+ method: 'DELETE'
694
+ });
695
+
696
+ if (response.ok) {
697
+ showToast('Document deleted', 'success');
698
+ loadStatus();
699
+ }
700
+ } catch (error) {
701
+ showToast('Failed to delete', 'error');
702
+ }
703
+ }
704
+
705
+ async function clearAllDocuments() {
706
+ if (!confirm('Clear all documents? This cannot be undone.')) return;
707
+
708
+ showLoading('Clearing all data...');
709
+
710
+ try {
711
+ const response = await fetch('/api/clear', { method: 'POST' });
712
+ if (response.ok) {
713
+ showToast('All documents cleared', 'success');
714
+ loadStatus();
715
+ }
716
+ } catch (error) {
717
+ showToast('Failed to clear', 'error');
718
+ } finally {
719
+ hideLoading();
720
+ }
721
+ }
722
+
723
+ async function rebuildRag() {
724
+ showLoading('Rebuilding RAG index from all PDFs...');
725
+
726
+ try {
727
+ const response = await fetch('/api/rebuild', { method: 'POST' });
728
+ const result = await response.json();
729
+ if (!response.ok || !result.success) {
730
+ throw new Error(result.message || 'RAG rebuild failed');
731
+ }
732
+ showToast(result.message, 'success');
733
+ loadStatus();
734
+ } catch (error) {
735
+ showToast(error.message || 'Failed to rebuild RAG', 'error');
736
+ } finally {
737
+ hideLoading();
738
+ }
739
+ }
740
+
741
+ function showLoading(text) {
742
+ loadingText.textContent = text;
743
+ loadingOverlay.style.display = 'flex';
744
+ }
745
+
746
+ function hideLoading() {
747
+ loadingOverlay.style.display = 'none';
748
+ }
749
+
750
+ function showToast(message, type) {
751
+ const existing = document.querySelector('.toast');
752
+ if (existing) existing.remove();
753
+
754
+ const toast = document.createElement('div');
755
+ toast.className = `toast ${type}`;
756
+ toast.innerHTML = `
757
+ <i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i>
758
+ <span>${message}</span>
759
+ `;
760
+ document.body.appendChild(toast);
761
+
762
+ setTimeout(() => {
763
+ toast.style.opacity = '0';
764
+ setTimeout(() => toast.remove(), 300);
765
+ }, 3000);
766
+ }
767
+ </script>
768
+ </body>
769
+ </html>
app/templates/index.html ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ο»Ώ<!DOCTYPE html>
2
+ <html lang="si">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sinhala Chatbot</title>
7
+ <link rel="stylesheet" href="/static/css/style.css">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+Sinhala:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
10
+ </head>
11
+ <body>
12
+ <!-- Three.js Background Canvas -->
13
+ <canvas id="bgCanvas"></canvas>
14
+
15
+ <div class="app-wrapper">
16
+ <!-- Hidden Status Indicator (used by JS) -->
17
+ <div class="status-indicator" id="statusIndicator" style="display:none;">
18
+ <span class="status-dot"></span>
19
+ <span class="status-text">Ready</span>
20
+ </div>
21
+
22
+ <!-- Compact Header -->
23
+ <header class="hero compact">
24
+ <div class="hero-top-row">
25
+ <div class="hero-badge">AI-Powered</div>
26
+ <h1 class="hero-title">Sinhala Chatbot</h1>
27
+ </div>
28
+ <p class="hero-desc"><i class="fas fa-wand-magic-sparkles"></i> Press the microphone to start &mdash; Supports Sinhala &amp; English voice input</p>
29
+ </header>
30
+
31
+ <!-- Main Content Area -->
32
+ <main class="main-content" id="voiceChatSection">
33
+ <!-- Mic Control Area (no box) -->
34
+ <div class="mic-area no-box">
35
+ <!-- Recording Timer -->
36
+ <div class="recording-timer" id="recordingTimer">
37
+ <span class="timer-dot"></span>
38
+ <span class="timer-text">00:00</span>
39
+ </div>
40
+
41
+ <!-- Mic Button with Glow -->
42
+ <div class="mic-wrapper">
43
+ <div class="mic-glow-ring ring-1"></div>
44
+ <div class="mic-glow-ring ring-2"></div>
45
+ <div class="mic-glow-ring ring-3"></div>
46
+ <button class="mic-btn" id="micBtn" title="Click to record">
47
+ <i class="fas fa-microphone"></i>
48
+ </button>
49
+ </div>
50
+
51
+ <!-- Audio Visualizer -->
52
+ <div class="visualizer" id="visualizer">
53
+ <div class="bar"></div>
54
+ <div class="bar"></div>
55
+ <div class="bar"></div>
56
+ <div class="bar"></div>
57
+ <div class="bar"></div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Reset Button Row -->
62
+ <div class="reset-row">
63
+ <button class="reset-btn" id="resetBtn" title="Reset">
64
+ <i class="fas fa-rotate-right"></i>
65
+ <span>Reset</span>
66
+ </button>
67
+ </div>
68
+
69
+ <!-- Chat Messages -->
70
+ <div class="chat-messages" id="chatContainer">
71
+ <!-- User Message Card -->
72
+ <div class="message-card user-card" id="inputDisplay">
73
+ <div class="message-avatar user-avatar">
74
+ <i class="fas fa-user-circle"></i>
75
+ </div>
76
+ <div class="message-body">
77
+ <div class="message-label">Your Message</div>
78
+ <div class="message-text" id="userText">
79
+ <p class="placeholder">Your transcribed message will appear here...</p>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Bot Response Card -->
85
+ <div class="message-card bot-card" id="responseDisplay">
86
+ <div class="message-actions-top">
87
+ <button class="action-btn-sm speak-btn" id="speakerBtn" title="Listen to response" disabled>
88
+ <i class="fas fa-volume-up"></i>
89
+ </button>
90
+ <button class="action-btn-sm pause-btn" id="pauseBtn" title="Pause audio">
91
+ <i class="fas fa-pause"></i>
92
+ </button>
93
+ </div>
94
+ <div class="message-avatar bot-avatar">
95
+ <i class="fas fa-robot"></i>
96
+ </div>
97
+ <div class="message-body">
98
+ <div class="message-label">Bot Response</div>
99
+ <div class="message-text" id="botText">
100
+ <p class="placeholder">Bot response will appear here...</p>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </main>
106
+
107
+ <!-- Loading Overlay -->
108
+ <div class="loading-overlay" id="loadingOverlay">
109
+ <div class="loader">
110
+ <div class="loader-visual">
111
+ <div class="loader-ring"></div>
112
+ <div class="loader-ring"></div>
113
+ <div class="loader-ring"></div>
114
+ <div class="loader-core">
115
+ <i class="fas fa-brain"></i>
116
+ </div>
117
+ </div>
118
+ <div class="loader-text-area">
119
+ <p id="loadingText">Processing...</p>
120
+ <div class="loader-dots">
121
+ <span></span><span></span><span></span>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
129
+ <script src="/static/js/script.js"></script>
130
+ <script src="/static/js/bg-animation.js"></script>
131
+ </body>
132
+ </html>
colab_rag_admin_api.ipynb ADDED
@@ -0,0 +1,881 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "cf8f37b5",
6
+ "metadata": {},
7
+ "source": [
8
+ "## 1️⃣ Install Required Packages"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": null,
14
+ "id": "35266b5d",
15
+ "metadata": {},
16
+ "outputs": [
17
+ {
18
+ "name": "stdout",
19
+ "output_type": "stream",
20
+ "text": [
21
+ "βœ… All packages installed!\n"
22
+ ]
23
+ }
24
+ ],
25
+ "source": [
26
+ "import sys\n",
27
+ "import subprocess\n",
28
+ "\n",
29
+ "# Install packages (works in VS Code Jupyter)\n",
30
+ "packages = [\n",
31
+ " 'langchain-community',\n",
32
+ " 'sentence-transformers',\n",
33
+ " 'transformers',\n",
34
+ " 'faiss-cpu',\n",
35
+ " 'pypdf',\n",
36
+ " 'google-generativeai',\n",
37
+ " 'langchain-huggingface',\n",
38
+ " 'langchain-text-splitters',\n",
39
+ " 'fastapi',\n",
40
+ " 'uvicorn',\n",
41
+ " 'nest-asyncio',\n",
42
+ " 'gradio',\n",
43
+ " 'deep-translator'\n",
44
+ "]\n",
45
+ "\n",
46
+ "print(\"πŸ“¦ Installing required packages...\")\n",
47
+ "subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q'] + packages)\n",
48
+ "print(\"βœ… All packages installed!\")"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "markdown",
53
+ "id": "b09a84be",
54
+ "metadata": {},
55
+ "source": [
56
+ "## 2️⃣ Setup Local Directories (Windows)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": 6,
62
+ "id": "760088c8",
63
+ "metadata": {},
64
+ "outputs": [
65
+ {
66
+ "name": "stdout",
67
+ "output_type": "stream",
68
+ "text": [
69
+ "βœ… Local directories created!\n",
70
+ "πŸ“ RAG Data Location: /content/rag_data\n",
71
+ "πŸ“„ PDFs will be stored at: /content/rag_data/pdfs\n",
72
+ "πŸ—„οΈ FAISS index at: /content/rag_data/faiss_index\n"
73
+ ]
74
+ }
75
+ ],
76
+ "source": [
77
+ "import os\n",
78
+ "\n",
79
+ "# Use local directories\n",
80
+ "RAG_DIR = os.path.join(os.getcwd(), 'rag_data')\n",
81
+ "FAISS_PATH = os.path.join(RAG_DIR, 'faiss_index')\n",
82
+ "PDFS_PATH = os.path.join(RAG_DIR, 'pdfs')\n",
83
+ "\n",
84
+ "os.makedirs(FAISS_PATH, exist_ok=True)\n",
85
+ "os.makedirs(PDFS_PATH, exist_ok=True)\n",
86
+ "\n",
87
+ "print(f\"βœ… Local directories created!\")\n",
88
+ "print(f\"πŸ“ RAG Data Location: {RAG_DIR}\")\n",
89
+ "print(f\"πŸ“„ PDFs will be stored at: {PDFS_PATH}\")\n",
90
+ "print(f\"πŸ—„οΈ FAISS index at: {FAISS_PATH}\")"
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "markdown",
95
+ "id": "888d519c",
96
+ "metadata": {},
97
+ "source": [
98
+ "## 3️⃣ Configure Gemini API Key"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "code",
103
+ "execution_count": 7,
104
+ "id": "8902f9ef",
105
+ "metadata": {},
106
+ "outputs": [
107
+ {
108
+ "name": "stdout",
109
+ "output_type": "stream",
110
+ "text": [
111
+ "⚠️ WARNING: Please set your Gemini API key above!\n"
112
+ ]
113
+ }
114
+ ],
115
+ "source": [
116
+ "import google.generativeai as genai\n",
117
+ "\n",
118
+ "# πŸ”‘ REPLACE WITH YOUR GEMINI API KEY\n",
119
+ "# Get it from: https://makersuite.google.com/app/apikey\n",
120
+ "GOOGLE_API_KEY = \"YOUR_GEMINI_API_KEY_HERE\"\n",
121
+ "\n",
122
+ "if GOOGLE_API_KEY == \"YOUR_GEMINI_API_KEY_HERE\":\n",
123
+ " print(\"⚠️ WARNING: Please set your Gemini API key above!\")\n",
124
+ "else:\n",
125
+ " genai.configure(api_key=GOOGLE_API_KEY)\n",
126
+ " print(\"βœ… Gemini API configured!\")"
127
+ ]
128
+ },
129
+ {
130
+ "cell_type": "markdown",
131
+ "id": "5b250359",
132
+ "metadata": {},
133
+ "source": [
134
+ "## 4️⃣ RAG System Functions"
135
+ ]
136
+ },
137
+ {
138
+ "cell_type": "code",
139
+ "execution_count": 8,
140
+ "id": "d292e154",
141
+ "metadata": {},
142
+ "outputs": [
143
+ {
144
+ "name": "stderr",
145
+ "output_type": "stream",
146
+ "text": [
147
+ "WARNING:torchao.kernel.intmm:Warning: Detected no triton, on systems without Triton certain kernels will not work\n"
148
+ ]
149
+ },
150
+ {
151
+ "name": "stdout",
152
+ "output_type": "stream",
153
+ "text": [
154
+ "πŸ” Checking for existing RAG data...\n",
155
+ "ℹ️ No existing vector store found\n",
156
+ "\n",
157
+ "βœ… RAG System Ready!\n"
158
+ ]
159
+ }
160
+ ],
161
+ "source": [
162
+ "import unicodedata\n",
163
+ "import re\n",
164
+ "import shutil\n",
165
+ "from typing import List, Dict, Optional\n",
166
+ "from pathlib import Path\n",
167
+ "from langchain_community.document_loaders.pdf import PyPDFLoader\n",
168
+ "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
169
+ "from langchain_huggingface import HuggingFaceEmbeddings\n",
170
+ "from langchain_community.vectorstores import FAISS\n",
171
+ "from deep_translator import GoogleTranslator\n",
172
+ "\n",
173
+ "# Global variables\n",
174
+ "vectordb = None\n",
175
+ "retriever = None\n",
176
+ "embeddings = None\n",
177
+ "rag_initialized = False\n",
178
+ "uploaded_documents = []\n",
179
+ "\n",
180
+ "\n",
181
+ "def initialize_embeddings():\n",
182
+ " \"\"\"Initialize multilingual embedding model (supports English & Sinhala)\"\"\"\n",
183
+ " global embeddings\n",
184
+ " \n",
185
+ " if embeddings is not None:\n",
186
+ " return embeddings\n",
187
+ " \n",
188
+ " print(\"πŸ“₯ Loading multilingual embedding model...\")\n",
189
+ " embeddings = HuggingFaceEmbeddings(\n",
190
+ " model_name=\"sentence-transformers/paraphrase-multilingual-mpnet-base-v2\"\n",
191
+ " )\n",
192
+ " print(\"βœ… Embedding model loaded!\")\n",
193
+ " return embeddings\n",
194
+ "\n",
195
+ "\n",
196
+ "def clean_text(text: str) -> str:\n",
197
+ " \"\"\"Clean and normalize text for embedding\"\"\"\n",
198
+ " if not isinstance(text, str) or not text.strip():\n",
199
+ " return \"\"\n",
200
+ " \n",
201
+ " normalized_text = unicodedata.normalize('NFKC', text)\n",
202
+ " cleaned_chars = [\n",
203
+ " char for char in normalized_text\n",
204
+ " if unicodedata.category(char) not in ['So', 'Cn', 'Cc', 'Cf', 'Cs']\n",
205
+ " ]\n",
206
+ " cleaned_text = \"\".join(cleaned_chars)\n",
207
+ " cleaned_text = re.sub(r'\\s+', ' ', cleaned_text).strip()\n",
208
+ " return cleaned_text\n",
209
+ "\n",
210
+ "\n",
211
+ "def load_and_process_pdf(pdf_path: str) -> List:\n",
212
+ " \"\"\"Load PDF and split into chunks\"\"\"\n",
213
+ " print(f\"πŸ“„ Loading PDF: {Path(pdf_path).name}\")\n",
214
+ " \n",
215
+ " loader = PyPDFLoader(pdf_path)\n",
216
+ " docs = loader.load()\n",
217
+ " \n",
218
+ " splitter = RecursiveCharacterTextSplitter(\n",
219
+ " chunk_size=300,\n",
220
+ " chunk_overlap=80\n",
221
+ " )\n",
222
+ " chunks = splitter.split_documents(docs)\n",
223
+ " \n",
224
+ " print(f\" βœ… {len(docs)} pages β†’ {len(chunks)} chunks\")\n",
225
+ " return chunks\n",
226
+ "\n",
227
+ "\n",
228
+ "def create_vector_store(chunks: List) -> bool:\n",
229
+ " \"\"\"Create or update FAISS vector store\"\"\"\n",
230
+ " global vectordb, retriever, rag_initialized\n",
231
+ " \n",
232
+ " initialize_embeddings()\n",
233
+ " \n",
234
+ " texts = [doc.page_content for doc in chunks]\n",
235
+ " metadatas = [doc.metadata for doc in chunks]\n",
236
+ " \n",
237
+ " processed_texts = []\n",
238
+ " processed_metadatas = []\n",
239
+ " \n",
240
+ " for i, text in enumerate(texts):\n",
241
+ " cleaned_text = clean_text(text)\n",
242
+ " if cleaned_text:\n",
243
+ " processed_texts.append(cleaned_text)\n",
244
+ " processed_metadatas.append(metadatas[i])\n",
245
+ " \n",
246
+ " if not processed_texts:\n",
247
+ " print(\"⚠️ No valid texts after cleaning\")\n",
248
+ " return False\n",
249
+ " \n",
250
+ " print(f\"πŸ”„ Creating embeddings for {len(processed_texts)} chunks...\")\n",
251
+ " \n",
252
+ " if vectordb is None:\n",
253
+ " vectordb = FAISS.from_texts(processed_texts, embeddings, metadatas=processed_metadatas)\n",
254
+ " else:\n",
255
+ " new_vectordb = FAISS.from_texts(processed_texts, embeddings, metadatas=processed_metadatas)\n",
256
+ " vectordb.merge_from(new_vectordb)\n",
257
+ " \n",
258
+ " retriever = vectordb.as_retriever(search_kwargs={\"k\": 4})\n",
259
+ " rag_initialized = True\n",
260
+ " \n",
261
+ " save_vector_store()\n",
262
+ " return True\n",
263
+ "\n",
264
+ "\n",
265
+ "def save_vector_store():\n",
266
+ " \"\"\"Save FAISS index to local storage\"\"\"\n",
267
+ " if vectordb is None:\n",
268
+ " return\n",
269
+ " \n",
270
+ " vectordb.save_local(FAISS_PATH)\n",
271
+ " print(f\"πŸ’Ύ Vector store saved locally\")\n",
272
+ "\n",
273
+ "\n",
274
+ "def load_vector_store() -> bool:\n",
275
+ " \"\"\"Load FAISS index from local storage\"\"\"\n",
276
+ " global vectordb, retriever, rag_initialized, uploaded_documents\n",
277
+ " \n",
278
+ " index_file = os.path.join(FAISS_PATH, 'index.faiss')\n",
279
+ " if not os.path.exists(index_file):\n",
280
+ " print(\"ℹ️ No existing vector store found\")\n",
281
+ " return False\n",
282
+ " \n",
283
+ " try:\n",
284
+ " initialize_embeddings()\n",
285
+ " vectordb = FAISS.load_local(\n",
286
+ " FAISS_PATH, \n",
287
+ " embeddings,\n",
288
+ " allow_dangerous_deserialization=True\n",
289
+ " )\n",
290
+ " retriever = vectordb.as_retriever(search_kwargs={\"k\": 4})\n",
291
+ " rag_initialized = True\n",
292
+ " \n",
293
+ " # Load document list\n",
294
+ " uploaded_documents = [f for f in os.listdir(PDFS_PATH) if f.endswith('.pdf')]\n",
295
+ " \n",
296
+ " print(f\"βœ… Loaded existing vector store\")\n",
297
+ " print(f\"πŸ“š {len(uploaded_documents)} documents found\")\n",
298
+ " return True\n",
299
+ " except Exception as e:\n",
300
+ " print(f\"⚠️ Failed to load vector store: {e}\")\n",
301
+ " return False\n",
302
+ "\n",
303
+ "\n",
304
+ "def translate_to_english(text: str) -> str:\n",
305
+ " \"\"\"Translate any language to English\"\"\"\n",
306
+ " try:\n",
307
+ " translator = GoogleTranslator(source='auto', target='en')\n",
308
+ " return translator.translate(text)\n",
309
+ " except:\n",
310
+ " return text # Return original if translation fails\n",
311
+ "\n",
312
+ "\n",
313
+ "def rag_answer(question: str, relevance_threshold: float = 2.0, translate: bool = True) -> Dict:\n",
314
+ " \"\"\"Answer question using RAG - check database first, fallback to Gemini\"\"\"\n",
315
+ " global retriever, vectordb\n",
316
+ " \n",
317
+ " # Translate to English if needed\n",
318
+ " original_question = question\n",
319
+ " if translate:\n",
320
+ " question = translate_to_english(question)\n",
321
+ " \n",
322
+ " result = {\n",
323
+ " \"question\": original_question,\n",
324
+ " \"question_english\": question,\n",
325
+ " \"answer\": \"\",\n",
326
+ " \"source\": \"none\",\n",
327
+ " \"context_found\": False,\n",
328
+ " \"relevance_score\": 0.0\n",
329
+ " }\n",
330
+ " \n",
331
+ " if not rag_initialized or retriever is None:\n",
332
+ " print(\"⚠️ RAG not initialized, using Gemini\")\n",
333
+ " result[\"source\"] = \"gemini\"\n",
334
+ " result[\"answer\"] = ask_gemini_directly(question)\n",
335
+ " return result\n",
336
+ " \n",
337
+ " # Search vector database\n",
338
+ " docs_with_scores = vectordb.similarity_search_with_score(question, k=4)\n",
339
+ " \n",
340
+ " if not docs_with_scores:\n",
341
+ " print(\"⚠️ No documents found, using Gemini\")\n",
342
+ " result[\"source\"] = \"gemini\"\n",
343
+ " result[\"answer\"] = ask_gemini_directly(question)\n",
344
+ " return result\n",
345
+ " \n",
346
+ " best_score = docs_with_scores[0][1]\n",
347
+ " result[\"relevance_score\"] = float(best_score)\n",
348
+ " \n",
349
+ " # Check relevance threshold\n",
350
+ " if best_score > relevance_threshold:\n",
351
+ " print(f\"⚠️ Low relevance (score: {best_score:.3f}), using Gemini\")\n",
352
+ " result[\"source\"] = \"gemini\"\n",
353
+ " result[\"answer\"] = ask_gemini_directly(question)\n",
354
+ " return result\n",
355
+ " \n",
356
+ " # Good relevance - use RAG\n",
357
+ " print(f\"βœ… Good relevance (score: {best_score:.3f}), answering from documents\")\n",
358
+ " docs = [doc for doc, score in docs_with_scores]\n",
359
+ " context = \"\\n\\n\".join([d.page_content for d in docs])\n",
360
+ " result[\"context_found\"] = True\n",
361
+ " \n",
362
+ " prompt = f\"\"\"Answer the question based on the following context from PDF documents. If the context doesn't contain enough information, say \"I don't have enough information in the documents.\"\n",
363
+ "\n",
364
+ "Context:\n",
365
+ "{context}\n",
366
+ "\n",
367
+ "Question: {question}\n",
368
+ "\n",
369
+ "Answer:\"\"\"\n",
370
+ " \n",
371
+ " try:\n",
372
+ " model = genai.GenerativeModel(\"models/gemini-1.5-flash\")\n",
373
+ " response = model.generate_content(prompt)\n",
374
+ " result[\"answer\"] = response.text\n",
375
+ " result[\"source\"] = \"rag\"\n",
376
+ " except Exception as e:\n",
377
+ " print(f\"❌ RAG generation error: {e}\")\n",
378
+ " result[\"answer\"] = f\"Error: {str(e)}\"\n",
379
+ " result[\"source\"] = \"error\"\n",
380
+ " \n",
381
+ " return result\n",
382
+ "\n",
383
+ "\n",
384
+ "def ask_gemini_directly(question: str) -> str:\n",
385
+ " \"\"\"Fallback: Ask Gemini directly without RAG\"\"\"\n",
386
+ " try:\n",
387
+ " model = genai.GenerativeModel(\"models/gemini-1.5-flash\")\n",
388
+ " response = model.generate_content(f\"Answer this question: {question}\")\n",
389
+ " return response.text\n",
390
+ " except Exception as e:\n",
391
+ " return f\"Error: {str(e)}\"\n",
392
+ "\n",
393
+ "\n",
394
+ "def process_uploaded_pdf(file_path: str, original_filename: str) -> str:\n",
395
+ " \"\"\"Process uploaded PDF from admin panel\"\"\"\n",
396
+ " try:\n",
397
+ " # Copy to local storage\n",
398
+ " dest_path = os.path.join(PDFS_PATH, original_filename)\n",
399
+ " shutil.copy(file_path, dest_path)\n",
400
+ " \n",
401
+ " # Process PDF\n",
402
+ " chunks = load_and_process_pdf(dest_path)\n",
403
+ " \n",
404
+ " if not chunks:\n",
405
+ " return f\"❌ Failed to extract text from {original_filename}\"\n",
406
+ " \n",
407
+ " # Create/update vector store\n",
408
+ " success = create_vector_store(chunks)\n",
409
+ " \n",
410
+ " if success:\n",
411
+ " if original_filename not in uploaded_documents:\n",
412
+ " uploaded_documents.append(original_filename)\n",
413
+ " return f\"βœ… Successfully processed '{original_filename}'\\n πŸ“Š {len(chunks)} chunks created\\n πŸ“š Total documents: {len(uploaded_documents)}\"\n",
414
+ " else:\n",
415
+ " return f\"❌ Failed to process {original_filename}\"\n",
416
+ " \n",
417
+ " except Exception as e:\n",
418
+ " return f\"❌ Error: {str(e)}\"\n",
419
+ "\n",
420
+ "\n",
421
+ "def get_status() -> Dict:\n",
422
+ " \"\"\"Get RAG system status\"\"\"\n",
423
+ " return {\n",
424
+ " \"initialized\": rag_initialized,\n",
425
+ " \"documents_count\": len(uploaded_documents),\n",
426
+ " \"documents\": uploaded_documents,\n",
427
+ " \"has_vector_store\": vectordb is not None,\n",
428
+ " \"storage_path\": PDFS_PATH\n",
429
+ " }\n",
430
+ "\n",
431
+ "\n",
432
+ "# Try to load existing data\n",
433
+ "print(\"πŸ” Checking for existing RAG data...\")\n",
434
+ "load_vector_store()\n",
435
+ "\n",
436
+ "print(\"\\nβœ… RAG System Ready!\")"
437
+ ]
438
+ },
439
+ {
440
+ "cell_type": "markdown",
441
+ "id": "bee976ec",
442
+ "metadata": {},
443
+ "source": [
444
+ "## 5️⃣ Admin Panel - Upload PDFs Here! πŸ“€"
445
+ ]
446
+ },
447
+ {
448
+ "cell_type": "code",
449
+ "execution_count": 9,
450
+ "id": "7fad545f",
451
+ "metadata": {},
452
+ "outputs": [
453
+ {
454
+ "name": "stderr",
455
+ "output_type": "stream",
456
+ "text": [
457
+ "/tmp/ipython-input-3459415953.py:45: DeprecationWarning: The 'theme' parameter in the Blocks constructor will be removed in Gradio 6.0. You will need to pass 'theme' to Blocks.launch() instead.\n",
458
+ " with gr.Blocks(title=\"RAG Admin Panel\", theme=gr.themes.Soft()) as admin_panel:\n"
459
+ ]
460
+ },
461
+ {
462
+ "name": "stdout",
463
+ "output_type": "stream",
464
+ "text": [
465
+ "\n",
466
+ "πŸŽ›οΈ Launching Admin Panel...\n",
467
+ "\n",
468
+ "Colab notebook detected. This cell will run indefinitely so that you can see errors and logs. To turn off, set debug=False in launch().\n",
469
+ "Note: opening Chrome Inspector may crash demo inside Colab notebooks.\n",
470
+ "* To create a public link, set `share=True` in `launch()`.\n"
471
+ ]
472
+ },
473
+ {
474
+ "data": {
475
+ "application/javascript": "(async (port, path, width, height, cache, element) => {\n if (!google.colab.kernel.accessAllowed && !cache) {\n return;\n }\n element.appendChild(document.createTextNode(''));\n const url = await google.colab.kernel.proxyPort(port, {cache});\n\n const external_link = document.createElement('div');\n external_link.innerHTML = `\n <div style=\"font-family: monospace; margin-bottom: 0.5rem\">\n Running on <a href=${new URL(path, url).toString()} target=\"_blank\">\n https://localhost:${port}${path}\n </a>\n </div>\n `;\n element.appendChild(external_link);\n\n const iframe = document.createElement('iframe');\n iframe.src = new URL(path, url).toString();\n iframe.height = height;\n iframe.allow = \"autoplay; camera; microphone; clipboard-read; clipboard-write;\"\n iframe.width = width;\n iframe.style.border = 0;\n element.appendChild(iframe);\n })(7860, \"/\", \"100%\", 500, false, window.element)",
476
+ "text/plain": [
477
+ "<IPython.core.display.Javascript object>"
478
+ ]
479
+ },
480
+ "metadata": {},
481
+ "output_type": "display_data"
482
+ },
483
+ {
484
+ "name": "stdout",
485
+ "output_type": "stream",
486
+ "text": [
487
+ "Keyboard interruption in main thread... closing server.\n"
488
+ ]
489
+ },
490
+ {
491
+ "data": {
492
+ "text/plain": []
493
+ },
494
+ "execution_count": 9,
495
+ "metadata": {},
496
+ "output_type": "execute_result"
497
+ }
498
+ ],
499
+ "source": [
500
+ "import gradio as gr\n",
501
+ "\n",
502
+ "def upload_pdf_handler(file):\n",
503
+ " \"\"\"Handle PDF upload from Gradio interface\"\"\"\n",
504
+ " if file is None:\n",
505
+ " return \"⚠️ Please select a PDF file\"\n",
506
+ " \n",
507
+ " if not file.name.endswith('.pdf'):\n",
508
+ " return \"❌ Only PDF files are allowed\"\n",
509
+ " \n",
510
+ " filename = os.path.basename(file.name)\n",
511
+ " result = process_uploaded_pdf(file.name, filename)\n",
512
+ " return result\n",
513
+ "\n",
514
+ "\n",
515
+ "def test_query_handler(question, threshold):\n",
516
+ " \"\"\"Test RAG query from admin panel\"\"\"\n",
517
+ " if not question:\n",
518
+ " return \"⚠️ Please enter a question\"\n",
519
+ " \n",
520
+ " result = rag_answer(question, relevance_threshold=threshold)\n",
521
+ " \n",
522
+ " output = f\"\"\"**Question:** {result['question']}\n",
523
+ "**English:** {result['question_english']}\n",
524
+ "**Source:** {result['source'].upper()} ({result['relevance_score']:.3f})\n",
525
+ "\n",
526
+ "**Answer:**\n",
527
+ "{result['answer']}\n",
528
+ "\"\"\"\n",
529
+ " return output\n",
530
+ "\n",
531
+ "\n",
532
+ "def get_status_handler():\n",
533
+ " \"\"\"Get system status\"\"\"\n",
534
+ " status = get_status()\n",
535
+ " return f\"\"\"**RAG System Status:**\n",
536
+ "- Initialized: {status['initialized']}\n",
537
+ "- Documents: {status['documents_count']}\n",
538
+ "- Files: {', '.join(status['documents']) if status['documents'] else 'None'}\n",
539
+ "- Storage: {status['storage_path']}\n",
540
+ "\"\"\"\n",
541
+ "\n",
542
+ "\n",
543
+ "# Create Gradio Interface\n",
544
+ "with gr.Blocks(title=\"RAG Admin Panel\", theme=gr.themes.Soft()) as admin_panel:\n",
545
+ " gr.Markdown(\n",
546
+ " \"\"\"\n",
547
+ " # πŸŽ›οΈ RAG Admin Panel\n",
548
+ " ### Upload PDFs and manage your RAG database\n",
549
+ " \"\"\"\n",
550
+ " )\n",
551
+ " \n",
552
+ " with gr.Tab(\"πŸ“€ Upload PDFs\"):\n",
553
+ " gr.Markdown(\"### Upload PDF Documents\")\n",
554
+ " with gr.Row():\n",
555
+ " with gr.Column():\n",
556
+ " pdf_input = gr.File(\n",
557
+ " label=\"Select PDF File\",\n",
558
+ " file_types=[\".pdf\"],\n",
559
+ " type=\"filepath\"\n",
560
+ " )\n",
561
+ " upload_btn = gr.Button(\"πŸ“€ Upload & Process\", variant=\"primary\")\n",
562
+ " with gr.Column():\n",
563
+ " upload_output = gr.Textbox(\n",
564
+ " label=\"Upload Status\",\n",
565
+ " lines=5,\n",
566
+ " interactive=False\n",
567
+ " )\n",
568
+ " \n",
569
+ " upload_btn.click(\n",
570
+ " fn=upload_pdf_handler,\n",
571
+ " inputs=pdf_input,\n",
572
+ " outputs=upload_output\n",
573
+ " )\n",
574
+ " \n",
575
+ " with gr.Tab(\"πŸ§ͺ Test Queries\"):\n",
576
+ " gr.Markdown(\"### Test your RAG system\")\n",
577
+ " with gr.Row():\n",
578
+ " with gr.Column():\n",
579
+ " question_input = gr.Textbox(\n",
580
+ " label=\"Question (English or Sinhala)\",\n",
581
+ " placeholder=\"What is a wired network?\",\n",
582
+ " lines=2\n",
583
+ " )\n",
584
+ " threshold_slider = gr.Slider(\n",
585
+ " minimum=0.5,\n",
586
+ " maximum=3.0,\n",
587
+ " value=2.0,\n",
588
+ " step=0.1,\n",
589
+ " label=\"Relevance Threshold (lower = stricter)\"\n",
590
+ " )\n",
591
+ " query_btn = gr.Button(\"πŸ” Ask Question\", variant=\"primary\")\n",
592
+ " with gr.Column():\n",
593
+ " query_output = gr.Markdown(label=\"Answer\")\n",
594
+ " \n",
595
+ " query_btn.click(\n",
596
+ " fn=test_query_handler,\n",
597
+ " inputs=[question_input, threshold_slider],\n",
598
+ " outputs=query_output\n",
599
+ " )\n",
600
+ " \n",
601
+ " with gr.Tab(\"πŸ“Š Status\"):\n",
602
+ " gr.Markdown(\"### System Status\")\n",
603
+ " status_output = gr.Markdown()\n",
604
+ " status_btn = gr.Button(\"πŸ”„ Refresh Status\")\n",
605
+ " \n",
606
+ " status_btn.click(\n",
607
+ " fn=get_status_handler,\n",
608
+ " outputs=status_output\n",
609
+ " )\n",
610
+ " \n",
611
+ " # Auto-load status on startup\n",
612
+ " admin_panel.load(fn=get_status_handler, outputs=status_output)\n",
613
+ "\n",
614
+ "# Launch admin panel\n",
615
+ "print(\"\\nπŸŽ›οΈ Launching Admin Panel...\\n\")\n",
616
+ "admin_panel.launch(share=False, server_name=\"127.0.0.1\", server_port=7860, debug=True)"
617
+ ]
618
+ },
619
+ {
620
+ "cell_type": "markdown",
621
+ "id": "3b658bf7",
622
+ "metadata": {},
623
+ "source": [
624
+ "## 6️⃣ Public API - Query from Anywhere! 🌐\n",
625
+ "*Note: This will run on port 8000, make sure Gradio admin panel is already running on port 7860*"
626
+ ]
627
+ },
628
+ {
629
+ "cell_type": "code",
630
+ "execution_count": null,
631
+ "id": "5fd82e6d",
632
+ "metadata": {},
633
+ "outputs": [],
634
+ "source": [
635
+ "from fastapi import FastAPI, HTTPException, UploadFile, File\n",
636
+ "from pydantic import BaseModel\n",
637
+ "import nest_asyncio\n",
638
+ "import uvicorn\n",
639
+ "import threading\n",
640
+ "import tempfile\n",
641
+ "\n",
642
+ "# Allow nested event loops\n",
643
+ "nest_asyncio.apply()\n",
644
+ "\n",
645
+ "# Create FastAPI app\n",
646
+ "app = FastAPI(\n",
647
+ " title=\"RAG API\",\n",
648
+ " description=\"Query RAG database or upload PDFs via API\",\n",
649
+ " version=\"1.0\"\n",
650
+ ")\n",
651
+ "\n",
652
+ "class QuestionRequest(BaseModel):\n",
653
+ " question: str\n",
654
+ " threshold: float = 2.0\n",
655
+ " translate: bool = True\n",
656
+ "\n",
657
+ "class AnswerResponse(BaseModel):\n",
658
+ " question: str\n",
659
+ " question_english: str\n",
660
+ " answer: str\n",
661
+ " source: str\n",
662
+ " relevance_score: float\n",
663
+ " context_found: bool\n",
664
+ "\n",
665
+ "\n",
666
+ "@app.get(\"/\")\n",
667
+ "async def root():\n",
668
+ " return {\n",
669
+ " \"message\": \"πŸš€ RAG API is running!\",\n",
670
+ " \"endpoints\": {\n",
671
+ " \"POST /ask\": \"Ask a question to RAG system\",\n",
672
+ " \"POST /upload\": \"Upload a PDF file\",\n",
673
+ " \"GET /status\": \"Check system status\",\n",
674
+ " \"GET /documents\": \"List uploaded documents\"\n",
675
+ " }\n",
676
+ " }\n",
677
+ "\n",
678
+ "\n",
679
+ "@app.post(\"/ask\", response_model=AnswerResponse)\n",
680
+ "async def ask_question(request: QuestionRequest):\n",
681
+ " \"\"\"Ask a question to RAG system\"\"\"\n",
682
+ " if not request.question:\n",
683
+ " raise HTTPException(status_code=400, detail=\"Question is required\")\n",
684
+ " \n",
685
+ " result = rag_answer(\n",
686
+ " request.question,\n",
687
+ " relevance_threshold=request.threshold,\n",
688
+ " translate=request.translate\n",
689
+ " )\n",
690
+ " \n",
691
+ " return AnswerResponse(\n",
692
+ " question=result[\"question\"],\n",
693
+ " question_english=result[\"question_english\"],\n",
694
+ " answer=result[\"answer\"],\n",
695
+ " source=result[\"source\"],\n",
696
+ " relevance_score=result[\"relevance_score\"],\n",
697
+ " context_found=result[\"context_found\"]\n",
698
+ " )\n",
699
+ "\n",
700
+ "\n",
701
+ "@app.post(\"/upload\")\n",
702
+ "async def upload_pdf_api(file: UploadFile = File(...)):\n",
703
+ " \"\"\"Upload a PDF via API\"\"\"\n",
704
+ " if not file.filename.endswith('.pdf'):\n",
705
+ " raise HTTPException(status_code=400, detail=\"Only PDF files allowed\")\n",
706
+ " \n",
707
+ " try:\n",
708
+ " # Save temporarily\n",
709
+ " with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:\n",
710
+ " content = await file.read()\n",
711
+ " temp_file.write(content)\n",
712
+ " temp_path = temp_file.name\n",
713
+ " \n",
714
+ " # Process\n",
715
+ " result = process_uploaded_pdf(temp_path, file.filename)\n",
716
+ " \n",
717
+ " # Clean up temp file\n",
718
+ " try:\n",
719
+ " os.unlink(temp_path)\n",
720
+ " except:\n",
721
+ " pass\n",
722
+ " \n",
723
+ " return {\n",
724
+ " \"success\": \"βœ…\" in result,\n",
725
+ " \"message\": result,\n",
726
+ " \"filename\": file.filename\n",
727
+ " }\n",
728
+ " except Exception as e:\n",
729
+ " raise HTTPException(status_code=500, detail=str(e))\n",
730
+ "\n",
731
+ "\n",
732
+ "@app.get(\"/status\")\n",
733
+ "async def api_status():\n",
734
+ " \"\"\"Get RAG system status\"\"\"\n",
735
+ " return get_status()\n",
736
+ "\n",
737
+ "\n",
738
+ "@app.get(\"/documents\")\n",
739
+ "async def list_documents():\n",
740
+ " \"\"\"List all uploaded documents\"\"\"\n",
741
+ " return {\n",
742
+ " \"count\": len(uploaded_documents),\n",
743
+ " \"documents\": uploaded_documents\n",
744
+ " }\n",
745
+ "\n",
746
+ "\n",
747
+ "def run_server():\n",
748
+ " \"\"\"Run the FastAPI server in a thread\"\"\"\n",
749
+ " uvicorn.run(app, host=\"127.0.0.1\", port=8000, log_level=\"info\")\n",
750
+ "\n",
751
+ "\n",
752
+ "# Start server in background thread\n",
753
+ "server_thread = threading.Thread(target=run_server, daemon=True)\n",
754
+ "server_thread.start()\n",
755
+ "\n",
756
+ "print(\"\\n\" + \"=\"*70)\n",
757
+ "print(\"🌐 LOCAL API SERVER STARTED!\")\n",
758
+ "print(\"=\"*70)\n",
759
+ "print(\"\\nπŸ“Œ API Endpoints:\")\n",
760
+ "print(\" POST http://localhost:8000/ask - Ask a question\")\n",
761
+ "print(\" POST http://localhost:8000/upload - Upload PDF\")\n",
762
+ "print(\" GET http://localhost:8000/status - System status\")\n",
763
+ "print(\" GET http://localhost:8000/documents - List documents\")\n",
764
+ "print(\" GET http://localhost:8000/docs - API documentation\")\n",
765
+ "print(\"\\nπŸ’‘ Example curl command:\")\n",
766
+ "print(' curl -X POST \"http://localhost:8000/ask\" ^')\n",
767
+ "print(' -H \"Content-Type: application/json\" ^')\n",
768
+ "print(' -d \"{\\\\\"question\\\\\": \\\\\"What is a network?\\\\\", \\\\\"threshold\\\\\": 2.0}\"')\n",
769
+ "print(\"\\nπŸ”„ API Server is running in background...\")\n",
770
+ "print(\" (Server will stop when notebook kernel is restarted)\\n\")"
771
+ ]
772
+ },
773
+ {
774
+ "cell_type": "markdown",
775
+ "id": "a8c7b576",
776
+ "metadata": {},
777
+ "source": [
778
+ "---\n",
779
+ "\n",
780
+ "## πŸŽ‰ You're Done! Here's What You Have:\n",
781
+ "\n",
782
+ "### βœ… Admin Panel (Cell 5)\n",
783
+ "- Drag & drop PDF upload interface\n",
784
+ "- Test queries in real-time\n",
785
+ "- View system status\n",
786
+ "- **Access at:** http://localhost:7860\n",
787
+ "\n",
788
+ "### βœ… Public API (Cell 6)\n",
789
+ "- RESTful API endpoints\n",
790
+ "- Query from any app/website\n",
791
+ "- Upload PDFs programmatically\n",
792
+ "- **Access at:** http://localhost:8000\n",
793
+ "- **API Docs:** http://localhost:8000/docs\n",
794
+ "\n",
795
+ "### βœ… Local Storage\n",
796
+ "- All data saved to `rag_data/` folder in your project\n",
797
+ "- Survives notebook restarts\n",
798
+ "- Easy to backup\n",
799
+ "\n",
800
+ "---\n",
801
+ "\n",
802
+ "## πŸ”₯ Integration Examples:\n",
803
+ "\n",
804
+ "### Python:\n",
805
+ "```python\n",
806
+ "import requests\n",
807
+ "\n",
808
+ "url = \"http://localhost:8000/ask\"\n",
809
+ "response = requests.post(url, json={\n",
810
+ " \"question\": \"What is a wired network?\",\n",
811
+ " \"threshold\": 2.0\n",
812
+ "})\n",
813
+ "print(response.json()['answer'])\n",
814
+ "```\n",
815
+ "\n",
816
+ "### JavaScript:\n",
817
+ "```javascript\n",
818
+ "fetch('http://localhost:8000/ask', {\n",
819
+ " method: 'POST',\n",
820
+ " headers: { 'Content-Type': 'application/json' },\n",
821
+ " body: JSON.stringify({ \n",
822
+ " question: 'What is a network?',\n",
823
+ " threshold: 2.0 \n",
824
+ " })\n",
825
+ "})\n",
826
+ ".then(r => r.json())\n",
827
+ ".then(data => console.log(data.answer));\n",
828
+ "```\n",
829
+ "\n",
830
+ "### Your Chatbot:\n",
831
+ "Update your chatbot to call `http://localhost:8000/ask` instead of the old endpoint!\n",
832
+ "\n",
833
+ "---\n",
834
+ "\n",
835
+ "## πŸ“ Usage Instructions:\n",
836
+ "\n",
837
+ "1. **Run Cells 1-4** to setup (one time)\n",
838
+ "2. **Run Cell 5** to start Admin Panel at http://localhost:7860\n",
839
+ "3. **Upload PDFs** via the Admin Panel\n",
840
+ "4. **Run Cell 6** to start API Server at http://localhost:8000\n",
841
+ "5. **Test queries** via Admin Panel or API\n",
842
+ "\n",
843
+ "## πŸ› οΈ Troubleshooting:\n",
844
+ "\n",
845
+ "- **Port already in use?** Change `server_port=7860` or `port=8000` to different numbers\n",
846
+ "- **Can't access?** Make sure Windows Firewall allows local connections\n",
847
+ "- **Need to access from other devices?** Change `127.0.0.1` to `0.0.0.0` (security risk!)\n",
848
+ "\n",
849
+ "## πŸš€ Next Steps:\n",
850
+ "\n",
851
+ "- Upload PDFs via Admin Panel (drag & drop)\n",
852
+ "- Test queries in Admin Panel\n",
853
+ "- Integrate API with your chatbot app\n",
854
+ "- Adjust relevance threshold as needed\n",
855
+ "\n",
856
+ "**Need help?** Re-run any cell to restart that component!"
857
+ ]
858
+ }
859
+ ],
860
+ "metadata": {
861
+ "kernelspec": {
862
+ "display_name": "Python 3 (ipykernel)",
863
+ "language": "python",
864
+ "name": "python3"
865
+ },
866
+ "language_info": {
867
+ "codemirror_mode": {
868
+ "name": "ipython",
869
+ "version": 3
870
+ },
871
+ "file_extension": ".py",
872
+ "mimetype": "text/x-python",
873
+ "name": "python",
874
+ "nbconvert_exporter": "python",
875
+ "pygments_lexer": "ipython3",
876
+ "version": "3.12.12"
877
+ }
878
+ },
879
+ "nbformat": 4,
880
+ "nbformat_minor": 5
881
+ }
colab_rag_api.ipynb ADDED
@@ -0,0 +1,792 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "fdfc1b2a",
6
+ "metadata": {},
7
+ "source": [
8
+ "## 1. Install Required Packages"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": 18,
14
+ "id": "e0f621d9",
15
+ "metadata": {},
16
+ "outputs": [
17
+ {
18
+ "name": "stdout",
19
+ "output_type": "stream",
20
+ "text": [
21
+ "πŸ“¦ Installing required packages...\n",
22
+ "βœ… All packages installed!\n"
23
+ ]
24
+ }
25
+ ],
26
+ "source": [
27
+ "import sys\n",
28
+ "import subprocess\n",
29
+ "\n",
30
+ "# Install packages (works in VS Code Jupyter)\n",
31
+ "packages = [\n",
32
+ " 'langchain-community',\n",
33
+ " 'sentence-transformers',\n",
34
+ " 'transformers',\n",
35
+ " 'faiss-cpu',\n",
36
+ " 'pypdf',\n",
37
+ " 'google-generativeai',\n",
38
+ " 'langchain-huggingface',\n",
39
+ " 'langchain-text-splitters',\n",
40
+ " 'fastapi',\n",
41
+ " 'uvicorn',\n",
42
+ " 'nest-asyncio'\n",
43
+ "]\n",
44
+ "\n",
45
+ "print(\"πŸ“¦ Installing required packages...\")\n",
46
+ "subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q'] + packages)\n",
47
+ "print(\"βœ… All packages installed!\")"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "markdown",
52
+ "id": "6c5a12c2",
53
+ "metadata": {},
54
+ "source": [
55
+ "## 2. Setup Local Directories (Windows)"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": 19,
61
+ "id": "fbe27891",
62
+ "metadata": {},
63
+ "outputs": [
64
+ {
65
+ "name": "stdout",
66
+ "output_type": "stream",
67
+ "text": [
68
+ "βœ… Local directories created!\n",
69
+ "πŸ“ RAG data will be stored at: /content/rag_data\n"
70
+ ]
71
+ }
72
+ ],
73
+ "source": [
74
+ "import os\n",
75
+ "\n",
76
+ "# Use local directories instead of Google Drive\n",
77
+ "RAG_DIR = os.path.join(os.getcwd(), 'rag_data')\n",
78
+ "FAISS_PATH = os.path.join(RAG_DIR, 'faiss_index')\n",
79
+ "PDFS_PATH = os.path.join(RAG_DIR, 'pdfs')\n",
80
+ "\n",
81
+ "os.makedirs(FAISS_PATH, exist_ok=True)\n",
82
+ "os.makedirs(PDFS_PATH, exist_ok=True)\n",
83
+ "\n",
84
+ "print(f\"βœ… Local directories created!\")\n",
85
+ "print(f\"πŸ“ RAG data will be stored at: {RAG_DIR}\")"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "markdown",
90
+ "id": "b75dabae",
91
+ "metadata": {},
92
+ "source": [
93
+ "## 3. Configure Gemini API Key"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": 20,
99
+ "id": "330b1f65",
100
+ "metadata": {},
101
+ "outputs": [
102
+ {
103
+ "name": "stdout",
104
+ "output_type": "stream",
105
+ "text": [
106
+ "βœ… Gemini API configured!\n"
107
+ ]
108
+ }
109
+ ],
110
+ "source": [
111
+ "import google.generativeai as genai\n",
112
+ "\n",
113
+ "# Replace with your API key\n",
114
+ "GOOGLE_API_KEY = \"AIzaSyC7tkb3uFgmh8YSuOVHYgIDywyL2lzICBA\" # Get from https://makersuite.google.com/app/apikey\n",
115
+ "\n",
116
+ "genai.configure(api_key=GOOGLE_API_KEY)\n",
117
+ "print(\"βœ… Gemini API configured!\")"
118
+ ]
119
+ },
120
+ {
121
+ "cell_type": "markdown",
122
+ "id": "49f2b49c",
123
+ "metadata": {},
124
+ "source": [
125
+ "## 4. RAG Functions - Load, Process, Query"
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "code",
130
+ "execution_count": 21,
131
+ "id": "c296fc8b",
132
+ "metadata": {},
133
+ "outputs": [
134
+ {
135
+ "name": "stdout",
136
+ "output_type": "stream",
137
+ "text": [
138
+ "βœ… RAG functions defined!\n"
139
+ ]
140
+ }
141
+ ],
142
+ "source": [
143
+ "import unicodedata\n",
144
+ "import re\n",
145
+ "from typing import List, Dict\n",
146
+ "from langchain_community.document_loaders.pdf import PyPDFLoader\n",
147
+ "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
148
+ "from langchain_huggingface import HuggingFaceEmbeddings\n",
149
+ "from langchain_community.vectorstores import FAISS\n",
150
+ "\n",
151
+ "# Global variables\n",
152
+ "vectordb = None\n",
153
+ "retriever = None\n",
154
+ "embeddings = None\n",
155
+ "rag_initialized = False\n",
156
+ "uploaded_documents = []\n",
157
+ "\n",
158
+ "\n",
159
+ "def initialize_embeddings():\n",
160
+ " \"\"\"Initialize multilingual embedding model\"\"\"\n",
161
+ " global embeddings\n",
162
+ " \n",
163
+ " if embeddings is not None:\n",
164
+ " return embeddings\n",
165
+ " \n",
166
+ " print(\"Loading multilingual embedding model...\")\n",
167
+ " embeddings = HuggingFaceEmbeddings(\n",
168
+ " model_name=\"sentence-transformers/paraphrase-multilingual-mpnet-base-v2\"\n",
169
+ " )\n",
170
+ " print(\"βœ… Embedding model loaded!\")\n",
171
+ " return embeddings\n",
172
+ "\n",
173
+ "\n",
174
+ "def clean_text(text: str) -> str:\n",
175
+ " \"\"\"Clean and normalize text\"\"\"\n",
176
+ " if not isinstance(text, str) or not text.strip():\n",
177
+ " return \"\"\n",
178
+ " \n",
179
+ " normalized_text = unicodedata.normalize('NFKC', text)\n",
180
+ " cleaned_chars = [\n",
181
+ " char for char in normalized_text\n",
182
+ " if unicodedata.category(char) not in ['So', 'Cn', 'Cc', 'Cf', 'Cs']\n",
183
+ " ]\n",
184
+ " cleaned_text = \"\".join(cleaned_chars)\n",
185
+ " cleaned_text = re.sub(r'\\s+', ' ', cleaned_text).strip()\n",
186
+ " return cleaned_text\n",
187
+ "\n",
188
+ "\n",
189
+ "def load_and_process_pdf(pdf_path: str) -> List:\n",
190
+ " \"\"\"Load PDF and split into chunks\"\"\"\n",
191
+ " print(f\"Loading PDF: {pdf_path}\")\n",
192
+ " \n",
193
+ " loader = PyPDFLoader(pdf_path)\n",
194
+ " docs = loader.load()\n",
195
+ " \n",
196
+ " splitter = RecursiveCharacterTextSplitter(\n",
197
+ " chunk_size=300,\n",
198
+ " chunk_overlap=80\n",
199
+ " )\n",
200
+ " chunks = splitter.split_documents(docs)\n",
201
+ " \n",
202
+ " print(f\"βœ… Loaded {len(docs)} pages, created {len(chunks)} chunks\")\n",
203
+ " return chunks\n",
204
+ "\n",
205
+ "\n",
206
+ "def create_vector_store(chunks: List) -> bool:\n",
207
+ " \"\"\"Create or update FAISS vector store\"\"\"\n",
208
+ " global vectordb, retriever, rag_initialized\n",
209
+ " \n",
210
+ " initialize_embeddings()\n",
211
+ " \n",
212
+ " texts = [doc.page_content for doc in chunks]\n",
213
+ " metadatas = [doc.metadata for doc in chunks]\n",
214
+ " \n",
215
+ " processed_texts = []\n",
216
+ " processed_metadatas = []\n",
217
+ " \n",
218
+ " for i, text in enumerate(texts):\n",
219
+ " cleaned_text = clean_text(text)\n",
220
+ " if cleaned_text:\n",
221
+ " processed_texts.append(cleaned_text)\n",
222
+ " processed_metadatas.append(metadatas[i])\n",
223
+ " \n",
224
+ " if not processed_texts:\n",
225
+ " print(\"⚠ No valid texts after cleaning\")\n",
226
+ " return False\n",
227
+ " \n",
228
+ " print(f\"Creating embeddings for {len(processed_texts)} chunks...\")\n",
229
+ " \n",
230
+ " if vectordb is None:\n",
231
+ " vectordb = FAISS.from_texts(processed_texts, embeddings, metadatas=processed_metadatas)\n",
232
+ " else:\n",
233
+ " new_vectordb = FAISS.from_texts(processed_texts, embeddings, metadatas=processed_metadatas)\n",
234
+ " vectordb.merge_from(new_vectordb)\n",
235
+ " \n",
236
+ " retriever = vectordb.as_retriever(search_kwargs={\"k\": 4})\n",
237
+ " rag_initialized = True\n",
238
+ " \n",
239
+ " # Save to Google Drive\n",
240
+ " save_vector_store()\n",
241
+ " \n",
242
+ " print(\"βœ… Vector store created/updated!\")\n",
243
+ " return True\n",
244
+ "\n",
245
+ "\n",
246
+ "def save_vector_store():\n",
247
+ " \"\"\"Save FAISS index to Google Drive\"\"\"\n",
248
+ " if vectordb is None:\n",
249
+ " return\n",
250
+ " \n",
251
+ " vectordb.save_local(FAISS_PATH)\n",
252
+ " print(f\"βœ… Vector store saved to Google Drive: {FAISS_PATH}\")\n",
253
+ "\n",
254
+ "\n",
255
+ "def load_vector_store() -> bool:\n",
256
+ " \"\"\"Load FAISS index from Google Drive\"\"\"\n",
257
+ " global vectordb, retriever, rag_initialized\n",
258
+ " \n",
259
+ " if not os.path.exists(FAISS_PATH):\n",
260
+ " print(\"β„Ή No existing vector store found\")\n",
261
+ " return False\n",
262
+ " \n",
263
+ " try:\n",
264
+ " initialize_embeddings()\n",
265
+ " vectordb = FAISS.load_local(\n",
266
+ " FAISS_PATH, \n",
267
+ " embeddings,\n",
268
+ " allow_dangerous_deserialization=True\n",
269
+ " )\n",
270
+ " retriever = vectordb.as_retriever(search_kwargs={\"k\": 4})\n",
271
+ " rag_initialized = True\n",
272
+ " print(\"βœ… Loaded existing vector store from Google Drive\")\n",
273
+ " return True\n",
274
+ " except Exception as e:\n",
275
+ " print(f\"⚠ Failed to load vector store: {e}\")\n",
276
+ " return False\n",
277
+ "\n",
278
+ "\n",
279
+ "def rag_answer(question: str, relevance_threshold: float = 1.5) -> Dict:\n",
280
+ " \"\"\"Answer question using RAG - check database first, fallback to Gemini\"\"\"\n",
281
+ " global retriever, vectordb\n",
282
+ " \n",
283
+ " result = {\n",
284
+ " \"answer\": \"\",\n",
285
+ " \"source\": \"none\",\n",
286
+ " \"context_found\": False,\n",
287
+ " \"relevance_score\": 0.0\n",
288
+ " }\n",
289
+ " \n",
290
+ " if not rag_initialized or retriever is None:\n",
291
+ " result[\"source\"] = \"gemini\"\n",
292
+ " result[\"answer\"] = ask_gemini_directly(question)\n",
293
+ " return result\n",
294
+ " \n",
295
+ " # Search vector database\n",
296
+ " docs_with_scores = vectordb.similarity_search_with_score(question, k=4)\n",
297
+ " \n",
298
+ " if not docs_with_scores:\n",
299
+ " result[\"source\"] = \"gemini\"\n",
300
+ " result[\"answer\"] = ask_gemini_directly(question)\n",
301
+ " return result\n",
302
+ " \n",
303
+ " best_score = docs_with_scores[0][1]\n",
304
+ " result[\"relevance_score\"] = float(best_score)\n",
305
+ " \n",
306
+ " # Check relevance threshold\n",
307
+ " if best_score > relevance_threshold:\n",
308
+ " print(f\"⚠ Low relevance (score: {best_score:.3f}), using Gemini\")\n",
309
+ " result[\"source\"] = \"gemini\"\n",
310
+ " result[\"answer\"] = ask_gemini_directly(question)\n",
311
+ " return result\n",
312
+ " \n",
313
+ " # Good relevance - use RAG\n",
314
+ " print(f\"βœ… Good relevance (score: {best_score:.3f}), answering from documents\")\n",
315
+ " docs = [doc for doc, score in docs_with_scores]\n",
316
+ " context = \"\\n\\n\".join([d.page_content for d in docs])\n",
317
+ " result[\"context_found\"] = True\n",
318
+ " \n",
319
+ " prompt = f\"\"\"Answer the question based ONLY on the following context from the PDF documents. If the context doesn't contain enough information, say \"I don't have enough information in the documents to answer this.\"\n",
320
+ "\n",
321
+ "Context from PDFs:\n",
322
+ "{context}\n",
323
+ "\n",
324
+ "Question: {question}\n",
325
+ "\n",
326
+ "Answer:\"\"\"\n",
327
+ " \n",
328
+ " try:\n",
329
+ " model = genai.GenerativeModel(\"models/gemini-1.5-flash\")\n",
330
+ " response = model.generate_content(prompt)\n",
331
+ " result[\"answer\"] = response.text\n",
332
+ " result[\"source\"] = \"rag\"\n",
333
+ " except Exception as e:\n",
334
+ " print(f\"❌ RAG generation error: {e}\")\n",
335
+ " result[\"answer\"] = f\"Error: {str(e)}\"\n",
336
+ " result[\"source\"] = \"error\"\n",
337
+ " \n",
338
+ " return result\n",
339
+ "\n",
340
+ "\n",
341
+ "def ask_gemini_directly(question: str) -> str:\n",
342
+ " \"\"\"Fallback: Ask Gemini directly\"\"\"\n",
343
+ " try:\n",
344
+ " model = genai.GenerativeModel(\"models/gemini-1.5-flash\")\n",
345
+ " response = model.generate_content(f\"Answer this question: {question}\")\n",
346
+ " return response.text\n",
347
+ " except Exception as e:\n",
348
+ " return f\"Error: {str(e)}\"\n",
349
+ "\n",
350
+ "\n",
351
+ "print(\"βœ… RAG functions defined!\")"
352
+ ]
353
+ },
354
+ {
355
+ "cell_type": "markdown",
356
+ "id": "2b98c801",
357
+ "metadata": {},
358
+ "source": [
359
+ "## 5. Load PDFs from Local Directory"
360
+ ]
361
+ },
362
+ {
363
+ "cell_type": "code",
364
+ "execution_count": 22,
365
+ "id": "6aecdbe9",
366
+ "metadata": {},
367
+ "outputs": [
368
+ {
369
+ "name": "stdout",
370
+ "output_type": "stream",
371
+ "text": [
372
+ "Loading multilingual embedding model...\n",
373
+ "βœ… Embedding model loaded!\n",
374
+ "⚠ Failed to load vector store: Error in faiss::FileIOReader::FileIOReader(const char*) at /project/third-party/faiss/faiss/impl/io.cpp:69: Error: 'f' failed: could not open /content/rag_data/faiss_index/index.faiss for reading: No such file or directory\n",
375
+ "πŸ“ Place your PDF files in: /content/rag_data/pdfs\n",
376
+ " Current directory: /content\n",
377
+ "\n",
378
+ "⚠️ No PDF files found!\n",
379
+ " Please add PDF files to: /content/rag_data/pdfs\n"
380
+ ]
381
+ }
382
+ ],
383
+ "source": [
384
+ "import glob\n",
385
+ "\n",
386
+ "# Try to load existing vector store first\n",
387
+ "load_vector_store()\n",
388
+ "\n",
389
+ "# Option 1: Manually place PDFs in the rag_data/pdfs folder, then run this\n",
390
+ "print(f\"πŸ“ Place your PDF files in: {PDFS_PATH}\")\n",
391
+ "print(f\" Current directory: {os.getcwd()}\")\n",
392
+ "\n",
393
+ "# Find all PDFs in the pdfs folder\n",
394
+ "pdf_files = glob.glob(os.path.join(PDFS_PATH, \"*.pdf\"))\n",
395
+ "\n",
396
+ "if not pdf_files:\n",
397
+ " print(\"\\n⚠️ No PDF files found!\")\n",
398
+ " print(f\" Please add PDF files to: {PDFS_PATH}\")\n",
399
+ "else:\n",
400
+ " print(f\"\\nπŸ“š Found {len(pdf_files)} PDF file(s):\")\n",
401
+ " \n",
402
+ " # Process each PDF\n",
403
+ " for pdf_path in pdf_files:\n",
404
+ " filename = os.path.basename(pdf_path)\n",
405
+ " print(f\"\\n Processing: {filename}\")\n",
406
+ " \n",
407
+ " # Skip if already processed\n",
408
+ " if filename in uploaded_documents:\n",
409
+ " print(f\" ⏭️ Already processed, skipping...\")\n",
410
+ " continue\n",
411
+ " \n",
412
+ " # Process PDF\n",
413
+ " chunks = load_and_process_pdf(pdf_path)\n",
414
+ " create_vector_store(chunks)\n",
415
+ " uploaded_documents.append(filename)\n",
416
+ " \n",
417
+ " print(f\"\\nβœ… Processed {len(uploaded_documents)} PDF(s) total\")\n",
418
+ " print(f\"πŸ“š Documents in database: {uploaded_documents}\")"
419
+ ]
420
+ },
421
+ {
422
+ "cell_type": "markdown",
423
+ "id": "ff67dfb7",
424
+ "metadata": {},
425
+ "source": [
426
+ "## 6. Test RAG Query (Simple)"
427
+ ]
428
+ },
429
+ {
430
+ "cell_type": "code",
431
+ "execution_count": 23,
432
+ "id": "86dc46cd",
433
+ "metadata": {},
434
+ "outputs": [
435
+ {
436
+ "name": "stdout",
437
+ "output_type": "stream",
438
+ "text": [
439
+ "❓ Question: What is a wired network?\n",
440
+ "\n"
441
+ ]
442
+ },
443
+ {
444
+ "ename": "KeyboardInterrupt",
445
+ "evalue": "",
446
+ "output_type": "error",
447
+ "traceback": [
448
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
449
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
450
+ "\u001b[0;32m/tmp/ipython-input-1251978023.py\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"❓ Question: {test_question}\\n\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrag_answer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtest_question\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrelevance_threshold\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m2.0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# Increased threshold\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"πŸ“Š Source: {result['source'].upper()}\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
451
+ "\u001b[0;32m/tmp/ipython-input-2893062687.py\u001b[0m in \u001b[0;36mrag_answer\u001b[0;34m(question, relevance_threshold)\u001b[0m\n\u001b[1;32m 148\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mrag_initialized\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mretriever\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 149\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"source\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"gemini\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 150\u001b[0;31m \u001b[0mresult\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"answer\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mask_gemini_directly\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mquestion\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 151\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 152\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
452
+ "\u001b[0;32m/tmp/ipython-input-2893062687.py\u001b[0m in \u001b[0;36mask_gemini_directly\u001b[0;34m(question)\u001b[0m\n\u001b[1;32m 201\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 202\u001b[0m \u001b[0mmodel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mgenai\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mGenerativeModel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"models/gemini-1.5-flash\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 203\u001b[0;31m \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgenerate_content\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"Answer this question: {question}\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 204\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtext\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 205\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
453
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/generativeai/generative_models.py\u001b[0m in \u001b[0;36mgenerate_content\u001b[0;34m(self, contents, generation_config, safety_settings, stream, tools, tool_config, request_options)\u001b[0m\n\u001b[1;32m 329\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mgeneration_types\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mGenerateContentResponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_iterator\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miterator\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 330\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 331\u001b[0;31m response = self._client.generate_content(\n\u001b[0m\u001b[1;32m 332\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 333\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mrequest_options\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
454
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/ai/generativelanguage_v1beta/services/generative_service/client.py\u001b[0m in \u001b[0;36mgenerate_content\u001b[0;34m(self, request, model, contents, retry, timeout, metadata)\u001b[0m\n\u001b[1;32m 833\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 834\u001b[0m \u001b[0;31m# Send the request.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 835\u001b[0;31m response = rpc(\n\u001b[0m\u001b[1;32m 836\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 837\u001b[0m \u001b[0mretry\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mretry\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
455
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/api_core/gapic_v1/method.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, timeout, retry, compression, *args, **kwargs)\u001b[0m\n\u001b[1;32m 129\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"compression\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcompression\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 130\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 131\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mwrapped_func\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 132\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 133\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
456
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/api_core/retry/retry_unary.py\u001b[0m in \u001b[0;36mretry_wrapped_func\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_initial\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_maximum\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmultiplier\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_multiplier\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m )\n\u001b[0;32m--> 294\u001b[0;31m return retry_target(\n\u001b[0m\u001b[1;32m 295\u001b[0m \u001b[0mtarget\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 296\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_predicate\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
457
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/api_core/retry/retry_unary.py\u001b[0m in \u001b[0;36mretry_target\u001b[0;34m(target, predicate, sleep_generator, timeout, on_error, exception_factory, **kwargs)\u001b[0m\n\u001b[1;32m 145\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 146\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 147\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtarget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 148\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0minspect\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0misawaitable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 149\u001b[0m \u001b[0mwarnings\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwarn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_ASYNC_RETRY_WARNING\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
458
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/api_core/timeout.py\u001b[0m in \u001b[0;36mfunc_with_timeout\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 128\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"timeout\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mremaining_timeout\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 129\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 130\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 131\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 132\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfunc_with_timeout\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
459
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/api_core/grpc_helpers.py\u001b[0m in \u001b[0;36merror_remapped_callable\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 73\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0merror_remapped_callable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 74\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 75\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mcallable_\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 76\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mgrpc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mRpcError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 77\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mexceptions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_grpc_error\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexc\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
460
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/ai/generativelanguage_v1beta/services/generative_service/transports/rest.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, request, retry, timeout, metadata)\u001b[0m\n\u001b[1;32m 1146\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1147\u001b[0m \u001b[0;31m# Send the request\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1148\u001b[0;31m response = GenerativeServiceRestTransport._GenerateContent._get_response(\n\u001b[0m\u001b[1;32m 1149\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_host\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1150\u001b[0m \u001b[0mmetadata\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
461
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/ai/generativelanguage_v1beta/services/generative_service/transports/rest.py\u001b[0m in \u001b[0;36m_get_response\u001b[0;34m(host, metadata, query_params, session, timeout, transcoded_request, body)\u001b[0m\n\u001b[1;32m 1046\u001b[0m \u001b[0mheaders\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmetadata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1047\u001b[0m \u001b[0mheaders\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"Content-Type\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"application/json\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1048\u001b[0;31m response = getattr(session, method)(\n\u001b[0m\u001b[1;32m 1049\u001b[0m \u001b[0;34m\"{host}{uri}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhost\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mhost\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0muri\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0muri\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1050\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mtimeout\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
462
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/requests/sessions.py\u001b[0m in \u001b[0;36mpost\u001b[0;34m(self, url, data, json, **kwargs)\u001b[0m\n\u001b[1;32m 635\u001b[0m \"\"\"\n\u001b[1;32m 636\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 637\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrequest\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"POST\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mjson\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mjson\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 638\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 639\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mput\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
463
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/auth/transport/requests.py\u001b[0m in \u001b[0;36mrequest\u001b[0;34m(self, method, url, data, headers, max_allowed_time, timeout, **kwargs)\u001b[0m\n\u001b[1;32m 533\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mTimeoutGuard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mremaining_time\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mguard\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 534\u001b[0m \u001b[0m_helpers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrequest_log\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_LOGGER\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mheaders\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 535\u001b[0;31m response = super(AuthorizedSession, self).request(\n\u001b[0m\u001b[1;32m 536\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 537\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
464
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/requests/sessions.py\u001b[0m in \u001b[0;36mrequest\u001b[0;34m(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)\u001b[0m\n\u001b[1;32m 587\u001b[0m }\n\u001b[1;32m 588\u001b[0m \u001b[0msend_kwargs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupdate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msettings\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 589\u001b[0;31m \u001b[0mresp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprep\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0msend_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 590\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 591\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
465
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/requests/sessions.py\u001b[0m in \u001b[0;36msend\u001b[0;34m(self, request, **kwargs)\u001b[0m\n\u001b[1;32m 701\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 702\u001b[0m \u001b[0;31m# Send the request\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 703\u001b[0;31m \u001b[0mr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0madapter\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrequest\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 704\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 705\u001b[0m \u001b[0;31m# Total elapsed time of the request (approximately)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
466
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/requests/adapters.py\u001b[0m in \u001b[0;36msend\u001b[0;34m(self, request, stream, timeout, verify, cert, proxies)\u001b[0m\n\u001b[1;32m 642\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 643\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 644\u001b[0;31m resp = conn.urlopen(\n\u001b[0m\u001b[1;32m 645\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mrequest\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmethod\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 646\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
467
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/urllib3/connectionpool.py\u001b[0m in \u001b[0;36murlopen\u001b[0;34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)\u001b[0m\n\u001b[1;32m 785\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 786\u001b[0m \u001b[0;31m# Make the request on the HTTPConnection object\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 787\u001b[0;31m response = self._make_request(\n\u001b[0m\u001b[1;32m 788\u001b[0m \u001b[0mconn\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 789\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
468
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/urllib3/connectionpool.py\u001b[0m in \u001b[0;36m_make_request\u001b[0;34m(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)\u001b[0m\n\u001b[1;32m 532\u001b[0m \u001b[0;31m# Receive the response from the server\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 533\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 534\u001b[0;31m \u001b[0mresponse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mconn\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetresponse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 535\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mBaseSSLError\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mOSError\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 536\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_raise_timeout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0merr\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0murl\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0murl\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout_value\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mread_timeout\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
469
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/urllib3/connection.py\u001b[0m in \u001b[0;36mgetresponse\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 563\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 564\u001b[0m \u001b[0;31m# Get the response from http.client.HTTPConnection\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 565\u001b[0;31m \u001b[0mhttplib_response\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetresponse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 566\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 567\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
470
+ "\u001b[0;32m/usr/lib/python3.12/http/client.py\u001b[0m in \u001b[0;36mgetresponse\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1428\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1429\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1430\u001b[0;31m \u001b[0mresponse\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbegin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1431\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mConnectionError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1432\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
471
+ "\u001b[0;32m/usr/lib/python3.12/http/client.py\u001b[0m in \u001b[0;36mbegin\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 329\u001b[0m \u001b[0;31m# read until we get a non-100 response\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 330\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 331\u001b[0;31m \u001b[0mversion\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstatus\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreason\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_read_status\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 332\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mstatus\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mCONTINUE\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 333\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
472
+ "\u001b[0;32m/usr/lib/python3.12/http/client.py\u001b[0m in \u001b[0;36m_read_status\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 291\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_read_status\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 292\u001b[0;31m \u001b[0mline\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreadline\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_MAXLINE\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"iso-8859-1\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 293\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mline\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0m_MAXLINE\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 294\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mLineTooLong\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"status line\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
473
+ "\u001b[0;32m/usr/lib/python3.12/socket.py\u001b[0m in \u001b[0;36mreadinto\u001b[0;34m(self, b)\u001b[0m\n\u001b[1;32m 718\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 719\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 720\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_sock\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrecv_into\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 721\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mtimeout\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 722\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_timeout_occurred\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
474
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
475
+ ]
476
+ }
477
+ ],
478
+ "source": [
479
+ "# Test with a question\n",
480
+ "test_question = \"What is a wired network?\" # Change this to your question\n",
481
+ "\n",
482
+ "print(f\"❓ Question: {test_question}\\n\")\n",
483
+ "result = rag_answer(test_question, relevance_threshold=2.0) # Increased threshold\n",
484
+ "\n",
485
+ "print(f\"πŸ“Š Source: {result['source'].upper()}\")\n",
486
+ "print(f\"πŸ“Š Relevance Score: {result['relevance_score']:.3f}\")\n",
487
+ "print(f\"\\nπŸ’¬ Answer:\\n{result['answer']}\")"
488
+ ]
489
+ },
490
+ {
491
+ "cell_type": "markdown",
492
+ "id": "04937fbd",
493
+ "metadata": {},
494
+ "source": [
495
+ "## 7. Create FastAPI Server + ngrok (Public API)"
496
+ ]
497
+ },
498
+ {
499
+ "cell_type": "code",
500
+ "execution_count": null,
501
+ "id": "708b25ca",
502
+ "metadata": {},
503
+ "outputs": [
504
+ {
505
+ "name": "stdout",
506
+ "output_type": "stream",
507
+ "text": [
508
+ "βœ… FastAPI app created!\n"
509
+ ]
510
+ }
511
+ ],
512
+ "source": [
513
+ "from fastapi import FastAPI, HTTPException\n",
514
+ "from pydantic import BaseModel\n",
515
+ "import nest_asyncio\n",
516
+ "\n",
517
+ "# Allow nested event loops (for Jupyter)\n",
518
+ "nest_asyncio.apply()\n",
519
+ "\n",
520
+ "# Create FastAPI app\n",
521
+ "app = FastAPI(title=\"RAG API\", version=\"1.0\")\n",
522
+ "\n",
523
+ "class QuestionRequest(BaseModel):\n",
524
+ " question: str\n",
525
+ " threshold: float = 2.0 # Default threshold\n",
526
+ "\n",
527
+ "class AnswerResponse(BaseModel):\n",
528
+ " question: str\n",
529
+ " answer: str\n",
530
+ " source: str\n",
531
+ " relevance_score: float\n",
532
+ " context_found: bool\n",
533
+ "\n",
534
+ "@app.get(\"/\")\n",
535
+ "async def root():\n",
536
+ " return {\n",
537
+ " \"message\": \"RAG API is running!\",\n",
538
+ " \"endpoints\": {\n",
539
+ " \"/ask\": \"POST - Ask a question\",\n",
540
+ " \"/status\": \"GET - Check system status\"\n",
541
+ " }\n",
542
+ " }\n",
543
+ "\n",
544
+ "@app.post(\"/ask\", response_model=AnswerResponse)\n",
545
+ "async def ask_question(request: QuestionRequest):\n",
546
+ " \"\"\"Ask a question to RAG system\"\"\"\n",
547
+ " if not request.question:\n",
548
+ " raise HTTPException(status_code=400, detail=\"Question is required\")\n",
549
+ " \n",
550
+ " result = rag_answer(request.question, relevance_threshold=request.threshold)\n",
551
+ " \n",
552
+ " return AnswerResponse(\n",
553
+ " question=request.question,\n",
554
+ " answer=result[\"answer\"],\n",
555
+ " source=result[\"source\"],\n",
556
+ " relevance_score=result[\"relevance_score\"],\n",
557
+ " context_found=result[\"context_found\"]\n",
558
+ " )\n",
559
+ "\n",
560
+ "@app.get(\"/status\")\n",
561
+ "async def get_status():\n",
562
+ " \"\"\"Get RAG system status\"\"\"\n",
563
+ " return {\n",
564
+ " \"initialized\": rag_initialized,\n",
565
+ " \"documents_count\": len(uploaded_documents),\n",
566
+ " \"documents\": uploaded_documents,\n",
567
+ " \"has_vector_store\": vectordb is not None\n",
568
+ " }\n",
569
+ "\n",
570
+ "print(\"βœ… FastAPI app created!\")"
571
+ ]
572
+ },
573
+ {
574
+ "cell_type": "markdown",
575
+ "id": "bd49f8a1",
576
+ "metadata": {},
577
+ "source": [
578
+ "## 8. Start Server Locally (Access at http://localhost:8000)"
579
+ ]
580
+ },
581
+ {
582
+ "cell_type": "code",
583
+ "execution_count": null,
584
+ "id": "0e4c8558",
585
+ "metadata": {},
586
+ "outputs": [
587
+ {
588
+ "name": "stdout",
589
+ "output_type": "stream",
590
+ "text": [
591
+ "\n",
592
+ "============================================================\n",
593
+ "🌐 LOCAL API SERVER STARTED!\n",
594
+ "============================================================\n",
595
+ "\n",
596
+ "πŸ“Œ API Endpoints:\n",
597
+ " POST http://localhost:8000/ask - Ask a question\n",
598
+ " GET http://localhost:8000/status - Check status\n",
599
+ " GET http://localhost:8000/docs - API documentation\n",
600
+ "\n",
601
+ "πŸ’‘ Test in browser: http://localhost:8000/docs\n",
602
+ "\n",
603
+ "πŸ’‘ Example curl command:\n",
604
+ " curl -X POST \"http://localhost:8000/ask\" ^\n",
605
+ " -H \"Content-Type: application/json\" ^\n",
606
+ " -d \"{\\\"question\\\": \\\"What is a wired network?\\\", \\\"threshold\\\": 2.0}\"\n",
607
+ "\n",
608
+ "πŸ”„ Server is running in background...\n",
609
+ " (Server will stop when notebook kernel is restarted)\n",
610
+ "\n"
611
+ ]
612
+ },
613
+ {
614
+ "name": "stderr",
615
+ "output_type": "stream",
616
+ "text": [
617
+ "/usr/local/lib/python3.12/dist-packages/uvicorn/server.py:67: RuntimeWarning: coroutine 'Server.serve' was never awaited\n",
618
+ " return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory())\n",
619
+ "RuntimeWarning: Enable tracemalloc to get the object allocation traceback\n",
620
+ "Exception in thread Thread-6 (run_server):\n",
621
+ "Traceback (most recent call last):\n",
622
+ " File \"/usr/lib/python3.12/threading.py\", line 1075, in _bootstrap_inner\n",
623
+ " self.run()\n",
624
+ " File \"/usr/lib/python3.12/threading.py\", line 1012, in run\n",
625
+ " self._target(*self._args, **self._kwargs)\n",
626
+ " File \"/tmp/ipython-input-2073060122.py\", line 6, in run_server\n",
627
+ " File \"/usr/local/lib/python3.12/dist-packages/uvicorn/main.py\", line 593, in run\n",
628
+ " server.run()\n",
629
+ " File \"/usr/local/lib/python3.12/dist-packages/uvicorn/server.py\", line 67, in run\n",
630
+ " return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory())\n",
631
+ " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
632
+ "TypeError: _patch_asyncio.<locals>.run() got an unexpected keyword argument 'loop_factory'\n"
633
+ ]
634
+ }
635
+ ],
636
+ "source": [
637
+ "import uvicorn\n",
638
+ "import threading\n",
639
+ "\n",
640
+ "def run_server():\n",
641
+ " \"\"\"Run the FastAPI server in a thread\"\"\"\n",
642
+ " uvicorn.run(app, host=\"127.0.0.1\", port=8000, log_level=\"info\")\n",
643
+ "\n",
644
+ "# Start server in background thread\n",
645
+ "server_thread = threading.Thread(target=run_server, daemon=True)\n",
646
+ "server_thread.start()\n",
647
+ "\n",
648
+ "print(\"\\n\" + \"=\"*60)\n",
649
+ "print(\"🌐 LOCAL API SERVER STARTED!\")\n",
650
+ "print(\"=\"*60)\n",
651
+ "print(\"\\nπŸ“Œ API Endpoints:\")\n",
652
+ "print(\" POST http://localhost:8000/ask - Ask a question\")\n",
653
+ "print(\" GET http://localhost:8000/status - Check status\")\n",
654
+ "print(\" GET http://localhost:8000/docs - API documentation\")\n",
655
+ "print(\"\\nπŸ’‘ Test in browser: http://localhost:8000/docs\")\n",
656
+ "print(\"\\nπŸ’‘ Example curl command:\")\n",
657
+ "print(' curl -X POST \"http://localhost:8000/ask\" ^')\n",
658
+ "print(' -H \"Content-Type: application/json\" ^')\n",
659
+ "print(' -d \"{\\\\\"question\\\\\": \\\\\"What is a wired network?\\\\\", \\\\\"threshold\\\\\": 2.0}\"')\n",
660
+ "print(\"\\nπŸ”„ Server is running in background...\")\n",
661
+ "print(\" (Server will stop when notebook kernel is restarted)\\n\")"
662
+ ]
663
+ },
664
+ {
665
+ "cell_type": "markdown",
666
+ "id": "a025b750",
667
+ "metadata": {},
668
+ "source": [
669
+ "## 9. Test API from Another Cell (While Server is Running)"
670
+ ]
671
+ },
672
+ {
673
+ "cell_type": "code",
674
+ "execution_count": null,
675
+ "id": "b368a3ac",
676
+ "metadata": {},
677
+ "outputs": [
678
+ {
679
+ "name": "stdout",
680
+ "output_type": "stream",
681
+ "text": [
682
+ "πŸ“‘ Testing API at http://localhost:8000/ask\n",
683
+ "\n",
684
+ "❌ Connection error: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /ask (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x79ffcd92bd40>: Failed to establish a new connection: [Errno 111] Connection refused'))\n",
685
+ " Make sure the server is running (cell 8)\n"
686
+ ]
687
+ }
688
+ ],
689
+ "source": [
690
+ "import requests\n",
691
+ "import json\n",
692
+ "import time\n",
693
+ "\n",
694
+ "# Give server a moment to start\n",
695
+ "time.sleep(2)\n",
696
+ "\n",
697
+ "# Local API URL\n",
698
+ "API_URL = \"http://localhost:8000\"\n",
699
+ "\n",
700
+ "# Test question\n",
701
+ "test_data = {\n",
702
+ " \"question\": \"What is a wireless network?\",\n",
703
+ " \"threshold\": 2.0\n",
704
+ "}\n",
705
+ "\n",
706
+ "print(f\"πŸ“‘ Testing API at {API_URL}/ask\\n\")\n",
707
+ "\n",
708
+ "try:\n",
709
+ " # Make API request\n",
710
+ " response = requests.post(\n",
711
+ " f\"{API_URL}/ask\",\n",
712
+ " json=test_data,\n",
713
+ " headers={\"Content-Type\": \"application/json\"}\n",
714
+ " )\n",
715
+ " \n",
716
+ " if response.status_code == 200:\n",
717
+ " result = response.json()\n",
718
+ " print(f\"❓ Question: {result['question']}\")\n",
719
+ " print(f\"πŸ“Š Source: {result['source'].upper()}\")\n",
720
+ " print(f\"πŸ“Š Score: {result['relevance_score']:.3f}\")\n",
721
+ " print(f\"\\nπŸ’¬ Answer:\\n{result['answer']}\")\n",
722
+ " else:\n",
723
+ " print(f\"❌ Error: {response.status_code}\")\n",
724
+ " print(response.text)\n",
725
+ "except Exception as e:\n",
726
+ " print(f\"❌ Connection error: {e}\")\n",
727
+ " print(\" Make sure the server is running (cell 8)\")"
728
+ ]
729
+ },
730
+ {
731
+ "cell_type": "markdown",
732
+ "id": "86a8d4bb",
733
+ "metadata": {},
734
+ "source": [
735
+ "---\n",
736
+ "\n",
737
+ "## βœ… Summary - Local Windows Setup\n",
738
+ "\n",
739
+ "Your RAG API is now configured for **local Windows** use:\n",
740
+ "\n",
741
+ "### How to Use:\n",
742
+ "1. βœ… **Run cells 1-4** to install packages and load functions\n",
743
+ "2. βœ… **Add PDFs** to the `rag_data/pdfs` folder in your project directory\n",
744
+ "3. βœ… **Run cell 5** to process PDFs and build the vector database\n",
745
+ "4. βœ… **Run cell 6** to test RAG queries directly\n",
746
+ "5. βœ… **Run cell 8** to start the local API server\n",
747
+ "6. βœ… **Access API docs** at http://localhost:8000/docs\n",
748
+ "\n",
749
+ "### Key Features:\n",
750
+ "- πŸ“ Data stored locally in `rag_data/` folder\n",
751
+ "- πŸ” Answers from PDF documents first\n",
752
+ "- πŸ€– Falls back to Gemini API when needed\n",
753
+ "- 🌐 Local API server at http://localhost:8000\n",
754
+ "- πŸ’Ύ FAISS index persists between sessions\n",
755
+ "\n",
756
+ "### Quick Test:\n",
757
+ "```python\n",
758
+ "# Direct RAG query (no API)\n",
759
+ "result = rag_answer(\"Your question here\", relevance_threshold=2.0)\n",
760
+ "print(result['answer'])\n",
761
+ "```\n",
762
+ "\n",
763
+ "### Next Steps:\n",
764
+ "- Add more PDFs to `rag_data/pdfs/` folder\n",
765
+ "- Rerun cell 5 to add them to the database\n",
766
+ "- Adjust `relevance_threshold` (lower = stricter, higher = more lenient)\n",
767
+ "- Access interactive API docs at http://localhost:8000/docs"
768
+ ]
769
+ }
770
+ ],
771
+ "metadata": {
772
+ "kernelspec": {
773
+ "display_name": "Python 3 (ipykernel)",
774
+ "language": "python",
775
+ "name": "python3"
776
+ },
777
+ "language_info": {
778
+ "codemirror_mode": {
779
+ "name": "ipython",
780
+ "version": 3
781
+ },
782
+ "file_extension": ".py",
783
+ "mimetype": "text/x-python",
784
+ "name": "python",
785
+ "nbconvert_exporter": "python",
786
+ "pygments_lexer": "ipython3",
787
+ "version": "3.12.12"
788
+ }
789
+ },
790
+ "nbformat": 4,
791
+ "nbformat_minor": 5
792
+ }
requirements.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sinhala Chatbot - Dependencies
2
+
3
+ # Web Framework
4
+ fastapi==0.109.0
5
+ uvicorn[standard]==0.27.0
6
+ python-multipart==0.0.6
7
+ jinja2==3.1.3
8
+
9
+ # Google Gemini AI
10
+ google-generativeai==0.3.2
11
+
12
+ # Speech Recognition (Whisper)
13
+ # Optional for local ASR: transformers, torch, soundfile, scipy
14
+
15
+ # Text-to-Speech
16
+ gTTS==2.5.0
17
+
18
+ # Environment Variables
19
+ python-dotenv==1.0.0
20
+
21
+ # Utilities
22
+ numpy==1.26.3
23
+ scipy==1.11.4
24
+
25
+ # Translation
26
+ deep-translator>=1.11.4
27
+
28
+ # Free LLM API
29
+ huggingface-hub>=0.20.0
30
+
31
+ # RAG (Retrieval-Augmented Generation)
32
+ langchain-community>=0.0.20
33
+ langchain-huggingface>=0.0.1
34
+ langchain-text-splitters>=0.0.1
35
+ sentence-transformers>=2.2.0
36
+ faiss-cpu==1.8.0.post1
37
+ pypdf>=3.17.0