Spaces:
Sleeping
Sleeping
CHAMATH commited on
Commit Β·
464b72a
0
Parent(s):
Deploy Space with optional ASR mode
Browse files- .dockerignore +30 -0
- .env.example +3 -0
- .gitignore +85 -0
- Dockerfile +24 -0
- README.md +173 -0
- app/__init__.py +1 -0
- app/admin.py +141 -0
- app/hf_space.py +10 -0
- app/main.py +470 -0
- app/rag.py +429 -0
- app/static/css/style.css +1054 -0
- app/static/js/bg-animation.js +223 -0
- app/static/js/script.js +628 -0
- app/templates/admin.html +769 -0
- app/templates/index.html +132 -0
- colab_rag_admin_api.ipynb +881 -0
- colab_rag_api.ipynb +792 -0
- requirements.txt +37 -0
.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 — Supports Sinhala & 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
|