Spaces:
Sleeping
Sleeping
Upload 45 files
Browse files- .env.example +40 -0
- .gitignore +2 -0
- Dockerfile +37 -0
- README.md +252 -10
- app/__init__.py +2 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/auth.cpython-311.pyc +0 -0
- app/__pycache__/auth.cpython-312.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-312.pyc +0 -0
- app/__pycache__/logger.cpython-311.pyc +0 -0
- app/__pycache__/logger.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/auth.py +23 -0
- app/config.py +72 -0
- app/controllers/__init__.py +1 -0
- app/controllers/__pycache__/__init__.cpython-311.pyc +0 -0
- app/controllers/__pycache__/__init__.cpython-312.pyc +0 -0
- app/controllers/__pycache__/embed_controller.cpython-311.pyc +0 -0
- app/controllers/__pycache__/embed_controller.cpython-312.pyc +0 -0
- app/controllers/embed_controller.py +54 -0
- app/logger.py +59 -0
- app/main.py +106 -0
- app/models/__init__.py +1 -0
- app/models/__pycache__/__init__.cpython-311.pyc +0 -0
- app/models/__pycache__/__init__.cpython-312.pyc +0 -0
- app/models/__pycache__/request_models.cpython-311.pyc +0 -0
- app/models/__pycache__/request_models.cpython-312.pyc +0 -0
- app/models/request_models.py +25 -0
- app/services/__init__.py +1 -0
- app/services/__pycache__/__init__.cpython-311.pyc +0 -0
- app/services/__pycache__/__init__.cpython-312.pyc +0 -0
- app/services/__pycache__/async_embeddings_service.cpython-311.pyc +0 -0
- app/services/__pycache__/embeddings_service.cpython-311.pyc +0 -0
- app/services/__pycache__/embeddings_service.cpython-312.pyc +0 -0
- app/services/async_embeddings_service.py +50 -0
- app/services/embeddings_service.py +169 -0
- docker-compose.yml +31 -0
- prompt.py +113 -0
- requirements.lock.txt +10 -0
- requirements.txt +10 -0
- scripts/__pycache__/convert_safetensors.cpython-311.pyc +0 -0
- scripts/convert_safetensors.py +87 -0
.env.example
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .env.example - copy to .env and edit
|
| 2 |
+
|
| 3 |
+
# -----------------------------
|
| 4 |
+
# Security / Access
|
| 5 |
+
# -----------------------------
|
| 6 |
+
# Static API key used to guard endpoints (REQUIRED )
|
| 7 |
+
EMBED_API_KEY=MY_SECURE_KEY
|
| 8 |
+
# Private hugging face token (REQUIRED)
|
| 9 |
+
HUGGING_FACE_TOKEN=your_free_token_with_repo_read_access
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# -----------------------------
|
| 13 |
+
# Model & Device Settings
|
| 14 |
+
# -----------------------------
|
| 15 |
+
# HuggingFace model identifier
|
| 16 |
+
MODEL_NAME=Faysal4200/bge-m3-private
|
| 17 |
+
|
| 18 |
+
# Target device: "cuda" for GPU, "cpu" for CPU, or leave empty for auto-detection
|
| 19 |
+
DEVICE=
|
| 20 |
+
|
| 21 |
+
# How many texts to process in one model forward pass
|
| 22 |
+
BATCH_SIZE=8
|
| 23 |
+
|
| 24 |
+
# Maximum input sequence length (BGE-M3 supports up to 8192)
|
| 25 |
+
MAX_LENGTH=1024
|
| 26 |
+
|
| 27 |
+
# -----------------------------
|
| 28 |
+
# Server & Reliability
|
| 29 |
+
# -----------------------------
|
| 30 |
+
RETRY_ATTEMPTS=3
|
| 31 |
+
RETRY_BACKOFF_SECONDS=2.0
|
| 32 |
+
HOST=0.0.0.0
|
| 33 |
+
PORT=7860
|
| 34 |
+
WORKERS=1
|
| 35 |
+
|
| 36 |
+
# -----------------------------
|
| 37 |
+
# Logging (Premium Style)
|
| 38 |
+
# -----------------------------
|
| 39 |
+
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
| 40 |
+
LOG_LEVEL=INFO
|
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# env with credential removed
|
| 2 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile - GPU & CPU friendly image
|
| 2 |
+
# Uses NVIDIA CUDA runtime as base for GPU support.
|
| 3 |
+
# Falls back to CPU automatically if no GPU is detected by the application.
|
| 4 |
+
|
| 5 |
+
FROM nvidia/cuda:12.1.1-runtime-ubuntu22.04
|
| 6 |
+
|
| 7 |
+
# Prevent interactive prompts during apt-get
|
| 8 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 9 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 10 |
+
ENV PYTHONUNBUFFERED=1
|
| 11 |
+
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
# Install Python and minimal system dependencies
|
| 15 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 16 |
+
python3.11 \
|
| 17 |
+
python3-pip \
|
| 18 |
+
python3.11-dev \
|
| 19 |
+
build-essential \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
# Set python3.11 as the default python
|
| 23 |
+
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 \
|
| 24 |
+
&& update-alternatives --set python3 /usr/bin/python3.11 \
|
| 25 |
+
&& python3 -m pip install --upgrade pip
|
| 26 |
+
|
| 27 |
+
# Copy requirements and install
|
| 28 |
+
# We install torch with cuda 12.1 index for explicit GPU support, but it works on CPU too.
|
| 29 |
+
COPY ./requirements.txt /app/requirements.txt
|
| 30 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt --extra-index-url https://download.pytorch.org/whl/cu121
|
| 31 |
+
|
| 32 |
+
# Copy app code
|
| 33 |
+
COPY ./app /app/app
|
| 34 |
+
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
|
| 37 |
+
CMD ["python3", "-m", "app.main"]
|
README.md
CHANGED
|
@@ -1,10 +1,252 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BGE-M3 Multilingual Embedding Server
|
| 2 |
+
|
| 3 |
+
A high-performance, robust, and production-ready embedding server utilizing the **BGE-M3** model. This server is designed to provide dense, multilingual embeddings with features like batch processing, automatic GPU/CPU detection, and strict security guards.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🚀 Key Features
|
| 8 |
+
|
| 9 |
+
- **State-of-the-Art Model**: Uses `BAAI/bge-m3` for high-quality multilingual embeddings. U can Change it also. with any huggigface model name just make sure it gives 1024 size vector embedding for consistency.
|
| 10 |
+
- **FastAPI Powered**: Asynchronous, high-performance API endpoints.
|
| 11 |
+
- **Intelligent Device Management**: Automatically detects NVIDIA GPUs (CUDA) or falls back to CPU.
|
| 12 |
+
- **Robust Batching**: Efficiently processes large lists of text with automatic chunking and OOM (Out of Memory) recovery.
|
| 13 |
+
- **Strict Security**: Built-in guard for API Key enforcement.
|
| 14 |
+
- **Premium Logging**: Beautiful, colorized, and structured logs for easy debugging and monitoring.
|
| 15 |
+
- **Reliability**: Integrated retry logic for model loading and processing.
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 🛠️ Tech Stack
|
| 20 |
+
|
| 21 |
+
- **Language**: Python 3.11+
|
| 22 |
+
- **Web Framework**: FastAPI
|
| 23 |
+
- **Deep Learning**: PyTorch & HuggingFace Transformers
|
| 24 |
+
- **Validation**: Pydantic V2 & Pydantic Settings
|
| 25 |
+
- **Logging**: Colorlog
|
| 26 |
+
- **Process Manager**: Uvicorn
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 📥 Installation
|
| 31 |
+
|
| 32 |
+
It is highly recommended to use a virtual environment to keep dependencies local to the project.
|
| 33 |
+
|
| 34 |
+
1. **Clone the project** and navigate to the directory.
|
| 35 |
+
2. **Create a virtual environment**:
|
| 36 |
+
```cmd
|
| 37 |
+
python -m venv venv
|
| 38 |
+
```
|
| 39 |
+
3. **Activate the environment**:
|
| 40 |
+
- Windows: `venv\Scripts\activate`
|
| 41 |
+
- Linux/Mac: `source venv/bin/activate`
|
| 42 |
+
4. **Install dependencies**:
|
| 43 |
+
```cmd
|
| 44 |
+
pip install -r requirements.txt
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## ⚙️ Configuration
|
| 50 |
+
|
| 51 |
+
Copy the `.env.example` file to `.env` and configure your settings. The application uses these variables to control everything from security to model performance.
|
| 52 |
+
|
| 53 |
+
```env
|
| 54 |
+
# -----------------------------
|
| 55 |
+
# Security / Access
|
| 56 |
+
# -----------------------------
|
| 57 |
+
EMBED_API_KEY=your_secret_key_here
|
| 58 |
+
HUGGING_FACE_TOKEN=your_free_token_with_repo_read_access
|
| 59 |
+
|
| 60 |
+
# -----------------------------
|
| 61 |
+
# Model & Device Settings
|
| 62 |
+
# -----------------------------
|
| 63 |
+
MODEL_NAME=BAAI/bge-m3 # Best if u copy this repo to your personal repo and use updated model name
|
| 64 |
+
DEVICE= # Leave empty for auto-detection, or use "cuda" / "cpu"
|
| 65 |
+
BATCH_SIZE=8 # Adjust based on your VRAM/RAM
|
| 66 |
+
MAX_LENGTH=1024 # Max token length
|
| 67 |
+
|
| 68 |
+
# -----------------------------
|
| 69 |
+
# Server & Reliability
|
| 70 |
+
# -----------------------------
|
| 71 |
+
RETRY_ATTEMPTS=3 # Retries for model loading/processing
|
| 72 |
+
RETRY_BACKOFF_SECONDS=2.0 # Wait time between retries
|
| 73 |
+
HOST=0.0.0.0
|
| 74 |
+
PORT=8000
|
| 75 |
+
WORKERS=1 # Number of Uvicorn workers
|
| 76 |
+
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 🏁 Running Locally
|
| 82 |
+
|
| 83 |
+
Start the server using Python's module runner:
|
| 84 |
+
|
| 85 |
+
```cmd
|
| 86 |
+
python -m app.main
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
The server will automatically log the detected device (GPU/CPU) and start listening on the configured port.
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 🐳 Docker Deployment
|
| 94 |
+
|
| 95 |
+
You can deploy the server easily using Docker and Docker Compose. This is the recommended way for production.
|
| 96 |
+
|
| 97 |
+
### 1. Build and Run
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
docker-compose up --build -d
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
This will start the server on the port defined in your `.env` (default `8000`).
|
| 104 |
+
# Note: After building the image if u want to run the image without code just use this command
|
| 105 |
+
# docker run -d --restart unless-stopped --name embed-server -p 8000:8000 --env-file .env vector-embedding-server-embed-server:latest
|
| 106 |
+
# here vector-embedding-server-embed-server:latest is the image name which is build
|
| 107 |
+
# .env should have the Configuration specified in the .env.example file
|
| 108 |
+
|
| 109 |
+
### 2. GPU Acceleration (NVIDIA)
|
| 110 |
+
|
| 111 |
+
To use your NVIDIA GPU with Docker, you must have the **NVIDIA Container Toolkit** installed on your host system.
|
| 112 |
+
|
| 113 |
+
1. Open `docker-compose.yml`.
|
| 114 |
+
2. Uncomment the `deploy` section at the bottom of the file:
|
| 115 |
+
```yaml
|
| 116 |
+
deploy:
|
| 117 |
+
resources:
|
| 118 |
+
reservations:
|
| 119 |
+
devices:
|
| 120 |
+
- driver: nvidia
|
| 121 |
+
count: all
|
| 122 |
+
capabilities: [gpu]
|
| 123 |
+
```
|
| 124 |
+
3. Restart the container:
|
| 125 |
+
```bash
|
| 126 |
+
docker-compose up -d --force-recreate
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
The server logs will confirm if `cuda` is being used.
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## 🚀 Run Docker Image (Without Source Code)
|
| 134 |
+
|
| 135 |
+
After building the image, run the container using:
|
| 136 |
+
```
|
| 137 |
+
docker run -d --restart unless-stopped --name embed-server -p 8000:8000 --env-file .env vector-embedding-server-embed-server:latest
|
| 138 |
+
```
|
| 139 |
+
# Requirements
|
| 140 |
+
|
| 141 |
+
- The Docker image vector-embedding-server-embed-server:latest must already be built.
|
| 142 |
+
- A .env file must exist in the same directory.
|
| 143 |
+
|
| 144 |
+
# Environment Setup
|
| 145 |
+
|
| 146 |
+
Create your .env file based on:
|
| 147 |
+
```
|
| 148 |
+
.env.example
|
| 149 |
+
```
|
| 150 |
+
Example:
|
| 151 |
+
```
|
| 152 |
+
cp .env.example .env
|
| 153 |
+
```
|
| 154 |
+
Update the values inside .env before running the container.
|
| 155 |
+
|
| 156 |
+
# Check Running Container
|
| 157 |
+
```
|
| 158 |
+
docker ps
|
| 159 |
+
```
|
| 160 |
+
You should see embed-server listed as running.
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
## 📡 API Endpoints
|
| 165 |
+
|
| 166 |
+
### 1. Health Check
|
| 167 |
+
|
| 168 |
+
`GET /embed/health`
|
| 169 |
+
Returns the server status and whether the model is loaded.
|
| 170 |
+
|
| 171 |
+
### 2. Single Embedding
|
| 172 |
+
|
| 173 |
+
`POST /embed/single`
|
| 174 |
+
Generates an embedding for a single text string.
|
| 175 |
+
|
| 176 |
+
### 3. Batch Embedding
|
| 177 |
+
|
| 178 |
+
`POST /embed/batch`
|
| 179 |
+
Generates embeddings for a list of strings efficiently.
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## 🧪 Documentation & Testing
|
| 184 |
+
|
| 185 |
+
Once the server is running, you can access the interactive API documentation at:
|
| 186 |
+
|
| 187 |
+
- **Swagger UI**: `http://localhost:7860/docs`
|
| 188 |
+
- **Redoc**: `http://localhost:7860/redoc`
|
| 189 |
+
|
| 190 |
+
### Test with CURL
|
| 191 |
+
|
| 192 |
+
**Note**: The server requires `x-api-key` header as configured in your `.env`.
|
| 193 |
+
|
| 194 |
+
#### Single Embedding Request:
|
| 195 |
+
|
| 196 |
+
```bash
|
| 197 |
+
curl -X POST http://localhost:7860/embed/single \
|
| 198 |
+
-H "Content-Type: application/json" \
|
| 199 |
+
-H "x-api-key: MY_SECURE_KEY" \
|
| 200 |
+
-d '{"text": "Hello world, this is a test."}'
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
#### Batch Embedding Request:
|
| 204 |
+
|
| 205 |
+
```bash
|
| 206 |
+
curl -X POST http://localhost:7860/embed/batch \
|
| 207 |
+
-H "Content-Type: application/json" \
|
| 208 |
+
-H "x-api-key: MY_SECURE_KEY" \
|
| 209 |
+
-d '{
|
| 210 |
+
"texts": [
|
| 211 |
+
"First sentence to embed.",
|
| 212 |
+
"BGE-M3 handles multilingual text very well."
|
| 213 |
+
]
|
| 214 |
+
}'
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
## 🔧 Troubleshooting
|
| 220 |
+
|
| 221 |
+
### `OSError: Could not create safetensors conversion PR`
|
| 222 |
+
|
| 223 |
+
If you see this error in your logs, it means the `transformers` library is trying to auto-convert your PyTorch model to SafeTensors format but failing (often due to repo permissions or structure).
|
| 224 |
+
|
| 225 |
+
**Solution:**
|
| 226 |
+
|
| 227 |
+
We have included a script to manually convert and upload the SafeTensors model to your Hugging Face repository.
|
| 228 |
+
|
| 229 |
+
**Prerequisites:**
|
| 230 |
+
1. **Write Access**: You must have **write access** to the Hugging Face repository defined in `MODEL_NAME`.
|
| 231 |
+
2. **Environment Variable**: Ensure your `HUGGING_FACE_TOKEN` in `.env` has **write permissions**.
|
| 232 |
+
|
| 233 |
+
**Instructions:**
|
| 234 |
+
Run this script **ONLY ONCE** and **ONLY** if you are facing the error:
|
| 235 |
+
|
| 236 |
+
```bash
|
| 237 |
+
python -m scripts.convert_safetensors
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
This will:
|
| 241 |
+
1. Load your current model.
|
| 242 |
+
2. Convert it to SafeTensors format locally.
|
| 243 |
+
3. Verify the converted model loads correctly.
|
| 244 |
+
4. Upload the `model.safetensors` file to your repository.
|
| 245 |
+
|
| 246 |
+
Once done, the "safetensors not found" error will disappear, and your model loading speed will improve.
|
| 247 |
+
|
| 248 |
+
---
|
| 249 |
+
|
| 250 |
+
## 👨💻 Developed By
|
| 251 |
+
|
| 252 |
+
**FAYSAL AHMMED**
|
app/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BGE-M3 Multilingual Embedding Server
|
| 2 |
+
__version__ = "1.0.0"
|
app/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (192 Bytes). View file
|
|
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (193 Bytes). View file
|
|
|
app/__pycache__/auth.cpython-311.pyc
ADDED
|
Binary file (1.4 kB). View file
|
|
|
app/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (4.46 kB). View file
|
|
|
app/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (2.87 kB). View file
|
|
|
app/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (3.48 kB). View file
|
|
|
app/__pycache__/logger.cpython-311.pyc
ADDED
|
Binary file (2.11 kB). View file
|
|
|
app/__pycache__/logger.cpython-312.pyc
ADDED
|
Binary file (1.98 kB). View file
|
|
|
app/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (5.81 kB). View file
|
|
|
app/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (5.7 kB). View file
|
|
|
app/auth.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/auth.py
|
| 2 |
+
"""
|
| 3 |
+
Authorization dependency.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import Request, HTTPException, status, Header
|
| 7 |
+
from .config import settings
|
| 8 |
+
from .logger import logger
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
async def api_key_guard(
|
| 12 |
+
request: Request,
|
| 13 |
+
x_api_key: str = Header(..., alias="x-api-key", description="API Key for authentication"),
|
| 14 |
+
) -> None:
|
| 15 |
+
# Check API key
|
| 16 |
+
# FastAPI's Header(...) ensures the key is present. behavior if missing is 422.
|
| 17 |
+
# We still check the value.
|
| 18 |
+
if x_api_key != settings.EMBED_API_KEY:
|
| 19 |
+
logger.warning(f"Unauthorized: Invalid API key. Provided: {x_api_key[:4] if x_api_key else 'None'}...")
|
| 20 |
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid API key")
|
| 21 |
+
|
| 22 |
+
logger.debug(f"Auth passed for request from {request.client.host if request.client else 'unknown'}")
|
| 23 |
+
|
app/config.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/config.py
|
| 2 |
+
"""
|
| 3 |
+
Application configuration using Pydantic BaseSettings.
|
| 4 |
+
All configuration MUST be provided via environment variables or the .env file.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
from pydantic import Field, field_validator
|
| 9 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 10 |
+
from typing import List, Optional, Any
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Settings(BaseSettings):
|
| 14 |
+
# -----------------------------
|
| 15 |
+
# Security / Access
|
| 16 |
+
# -----------------------------
|
| 17 |
+
EMBED_API_KEY: str = Field(..., validation_alias="EMBED_API_KEY")
|
| 18 |
+
HUGGING_FACE_TOKEN: str = Field(..., validation_alias="HUGGING_FACE_TOKEN")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# -----------------------------
|
| 23 |
+
# Model & Device Settings
|
| 24 |
+
# -----------------------------
|
| 25 |
+
MODEL_NAME: str = Field(..., validation_alias="MODEL_NAME")
|
| 26 |
+
DEVICE: Optional[str] = Field(..., validation_alias="DEVICE") # Can be empty in .env for auto-detection
|
| 27 |
+
BATCH_SIZE: int = Field(8, validation_alias="BATCH_SIZE")
|
| 28 |
+
MAX_LENGTH: int = Field(1024, validation_alias="MAX_LENGTH")
|
| 29 |
+
|
| 30 |
+
# -----------------------------
|
| 31 |
+
# Server & Reliability
|
| 32 |
+
# -----------------------------
|
| 33 |
+
RETRY_ATTEMPTS: int = Field(3, validation_alias="RETRY_ATTEMPTS")
|
| 34 |
+
RETRY_BACKOFF_SECONDS: float = Field(2.0, validation_alias="RETRY_BACKOFF_SECONDS")
|
| 35 |
+
HOST: str = Field("0.0.0.0", validation_alias="HOST")
|
| 36 |
+
PORT: int = Field(7860, validation_alias="PORT")
|
| 37 |
+
WORKERS: int = Field(1, validation_alias="WORKERS")
|
| 38 |
+
|
| 39 |
+
# -----------------------------
|
| 40 |
+
# Logging
|
| 41 |
+
# -----------------------------
|
| 42 |
+
LOG_LEVEL: str = Field("INFO", validation_alias="LOG_LEVEL")
|
| 43 |
+
|
| 44 |
+
# Pydantic Settings Configuration
|
| 45 |
+
model_config = SettingsConfigDict(
|
| 46 |
+
env_file=".env",
|
| 47 |
+
env_file_encoding="utf-8",
|
| 48 |
+
extra="ignore"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# Load settings (Strict mode: will raise error if any key is missing)
|
| 55 |
+
try:
|
| 56 |
+
settings = Settings()
|
| 57 |
+
# ------------------------------------------------------------------
|
| 58 |
+
# Explicitly set HF environment variables so all HF-based libraries
|
| 59 |
+
# (transformers, huggingface_hub, etc.) pick up the token globally.
|
| 60 |
+
# If this not done then hugging face token will not work
|
| 61 |
+
# ------------------------------------------------------------------
|
| 62 |
+
import os
|
| 63 |
+
if settings.HUGGING_FACE_TOKEN:
|
| 64 |
+
os.environ["HF_TOKEN"] = settings.HUGGING_FACE_TOKEN
|
| 65 |
+
os.environ["HUGGINGFACE_HUB_TOKEN"] = settings.HUGGING_FACE_TOKEN
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
import sys
|
| 69 |
+
print(f"\n[CRITICAL ERROR] Configuration failed to load from environment/.env:")
|
| 70 |
+
print(f"Missing or invalid keys: {e}")
|
| 71 |
+
print("\nPlease ensure your .env file is complete according to .env.example\n")
|
| 72 |
+
sys.exit(1)
|
app/controllers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .embed_controller import router as embed_router
|
app/controllers/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (259 Bytes). View file
|
|
|
app/controllers/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (243 Bytes). View file
|
|
|
app/controllers/__pycache__/embed_controller.cpython-311.pyc
ADDED
|
Binary file (3.37 kB). View file
|
|
|
app/controllers/__pycache__/embed_controller.cpython-312.pyc
ADDED
|
Binary file (2.99 kB). View file
|
|
|
app/controllers/embed_controller.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API router for embedding endpoints.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 6 |
+
from typing import Any, Dict
|
| 7 |
+
|
| 8 |
+
from ..models.request_models import SingleRequest, BatchRequest
|
| 9 |
+
from ..services.async_embeddings_service import AsyncEmbeddingsService
|
| 10 |
+
from ..auth import api_key_guard
|
| 11 |
+
from ..logger import logger
|
| 12 |
+
|
| 13 |
+
router = APIRouter(prefix="/embed", tags=["embed"])
|
| 14 |
+
|
| 15 |
+
# service is a module-level singleton
|
| 16 |
+
service = AsyncEmbeddingsService()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@router.post("/single")
|
| 20 |
+
async def embed_single(req: SingleRequest, _: Any = Depends(api_key_guard)):
|
| 21 |
+
"""Generate embedding for one text."""
|
| 22 |
+
try:
|
| 23 |
+
emb = await service.generate_single_embedding(req.text)
|
| 24 |
+
return {"embedding": emb}
|
| 25 |
+
except ValueError as ve:
|
| 26 |
+
# Warning since it's a client usage error (400)
|
| 27 |
+
logger.warning(f"Validation Error: {ve}")
|
| 28 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve))
|
| 29 |
+
except Exception as e:
|
| 30 |
+
# Error level since it's a server failure (500)
|
| 31 |
+
logger.error(f"Failed /embed/single request")
|
| 32 |
+
# Traceback will be handled by global exception handler
|
| 33 |
+
raise
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@router.post("/batch")
|
| 37 |
+
async def embed_batch(req: BatchRequest, _: Any = Depends(api_key_guard)):
|
| 38 |
+
"""Efficient batch embedding endpoint."""
|
| 39 |
+
try:
|
| 40 |
+
embeddings = await service.generate_batch_embeddings(req.texts)
|
| 41 |
+
return {"embeddings": embeddings}
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"Failed /embed/batch request for {len(req.texts)} items")
|
| 44 |
+
raise
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@router.get("/health")
|
| 48 |
+
async def health_check(_: Any = Depends(api_key_guard)) -> Dict[str, Any]:
|
| 49 |
+
"""Verify server status and model readiness."""
|
| 50 |
+
return {
|
| 51 |
+
"status": "healthy",
|
| 52 |
+
"model_loaded": service.is_ready,
|
| 53 |
+
"device": str(service.device) if service.is_ready else "not_loaded"
|
| 54 |
+
}
|
app/logger.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/logger.py
|
| 2 |
+
"""
|
| 3 |
+
Structured, colorized logger used across the app.
|
| 4 |
+
|
| 5 |
+
- Uses colorlog to add color to log levels in console.
|
| 6 |
+
- Central logger configuration prevents duplicate handlers.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
import sys
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
import colorlog
|
| 14 |
+
from .config import settings
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
| 18 |
+
logger = logging.getLogger(name)
|
| 19 |
+
if logger.handlers:
|
| 20 |
+
# already configured
|
| 21 |
+
return logger
|
| 22 |
+
|
| 23 |
+
# Get level from settings or default to INFO
|
| 24 |
+
log_level_str = settings.LOG_LEVEL.upper()
|
| 25 |
+
level = getattr(logging, log_level_str, logging.INFO)
|
| 26 |
+
|
| 27 |
+
# Console Handler with Color
|
| 28 |
+
handler = colorlog.StreamHandler(stream=sys.stdout)
|
| 29 |
+
handler.setLevel(level)
|
| 30 |
+
|
| 31 |
+
# Enhanced Formatter with better spacing and colors
|
| 32 |
+
formatter = colorlog.ColoredFormatter(
|
| 33 |
+
"%(log_color)s%(levelname)-8s%(reset)s | %(log_color)s%(asctime)s%(reset)s | %(cyan)s%(name)s%(reset)s | %(message)s",
|
| 34 |
+
datefmt="%H:%M:%S",
|
| 35 |
+
log_colors={
|
| 36 |
+
"DEBUG": "blue",
|
| 37 |
+
"INFO": "green",
|
| 38 |
+
"WARNING": "yellow",
|
| 39 |
+
"ERROR": "bold_red",
|
| 40 |
+
"CRITICAL": "bold_red,bg_white",
|
| 41 |
+
},
|
| 42 |
+
secondary_log_colors={
|
| 43 |
+
"message": {
|
| 44 |
+
"ERROR": "red",
|
| 45 |
+
"CRITICAL": "red",
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
style="%"
|
| 49 |
+
)
|
| 50 |
+
handler.setFormatter(formatter)
|
| 51 |
+
|
| 52 |
+
logger.setLevel(level)
|
| 53 |
+
logger.addHandler(handler)
|
| 54 |
+
logger.propagate = False
|
| 55 |
+
return logger
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# Global app logger
|
| 59 |
+
logger = get_logger("bge-server")
|
app/main.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/main.py
|
| 2 |
+
"""
|
| 3 |
+
Application entrypoint.
|
| 4 |
+
|
| 5 |
+
- Configures FastAPI app and CORS middleware.
|
| 6 |
+
- Includes embed router.
|
| 7 |
+
- Starts uvicorn when run as __main__.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from contextlib import asynccontextmanager
|
| 12 |
+
import uvicorn
|
| 13 |
+
from fastapi import FastAPI, Request
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from fastapi.responses import JSONResponse
|
| 16 |
+
|
| 17 |
+
from .config import settings
|
| 18 |
+
from .logger import logger
|
| 19 |
+
from .controllers.embed_controller import router as embed_router, service as embedding_service
|
| 20 |
+
|
| 21 |
+
@asynccontextmanager
|
| 22 |
+
async def lifespan(app: FastAPI):
|
| 23 |
+
# Startup logic
|
| 24 |
+
gpu_available = torch.cuda.is_available()
|
| 25 |
+
device_to_use = settings.DEVICE or ("cuda" if gpu_available else "cpu")
|
| 26 |
+
|
| 27 |
+
logger.info("┌────────────────────────────────────────┐")
|
| 28 |
+
logger.info("│ Embedding Server Start │")
|
| 29 |
+
logger.info("└────────────────────────────────────────┘")
|
| 30 |
+
|
| 31 |
+
if gpu_available:
|
| 32 |
+
logger.info(f"GPU Detected: [bold]{torch.cuda.get_device_name(0)}[/bold]")
|
| 33 |
+
else:
|
| 34 |
+
logger.warning("GPU not detected, falling back to CPU.")
|
| 35 |
+
|
| 36 |
+
logger.info(f"Active Device: {device_to_use}")
|
| 37 |
+
logger.info(f"Serving on: {settings.HOST}:{settings.PORT}")
|
| 38 |
+
|
| 39 |
+
# Eagerly load the model to RAM/VRAM on startup
|
| 40 |
+
try:
|
| 41 |
+
await embedding_service.load_model()
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error("CRITICAL: Failed to load model during startup.")
|
| 44 |
+
logger.exception(e)
|
| 45 |
+
# We don't exit here to allow the health check to remain accessible,
|
| 46 |
+
# but the model status will be 'unloaded'.
|
| 47 |
+
|
| 48 |
+
yield
|
| 49 |
+
|
| 50 |
+
# Shutdown logic
|
| 51 |
+
logger.info("Shutting down server...")
|
| 52 |
+
|
| 53 |
+
app = FastAPI(
|
| 54 |
+
title="BGE-M3 Multilingual Embedding Server",
|
| 55 |
+
summary="High-performance API for BAAI/bge-m3 embeddings.",
|
| 56 |
+
description="""
|
| 57 |
+
## Features
|
| 58 |
+
- **Multilingual Support**: Embed text in 100+ languages.
|
| 59 |
+
- **GPU Acceleration**: Automatically uses CUDA if available.
|
| 60 |
+
- **Batch Processing**: Efficiently handle lists of text.
|
| 61 |
+
- **Security**: Strict API Key protection.
|
| 62 |
+
|
| 63 |
+
## Usage
|
| 64 |
+
- **Required Header**: `x-api-key` must be provided for all `POST` / `GET` requests.
|
| 65 |
+
- **Interactive Docs**: Use the **Try it out** button to test endpoints directly from the browser.
|
| 66 |
+
""",
|
| 67 |
+
version="1.0.0",
|
| 68 |
+
lifespan=lifespan,
|
| 69 |
+
docs_url="/docs",
|
| 70 |
+
redoc_url="/redoc"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Global Exception Handler for unhandled errors
|
| 74 |
+
@app.exception_handler(Exception)
|
| 75 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 76 |
+
logger.error(f"Unhandled Exception: {type(exc).__name__}")
|
| 77 |
+
logger.error(f"Path: {request.url.path}")
|
| 78 |
+
logger.exception(exc) # This will log the full traceback in red
|
| 79 |
+
return JSONResponse(
|
| 80 |
+
status_code=500,
|
| 81 |
+
content={"detail": "An internal server error occurred.", "type": type(exc).__name__},
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# CORS middleware - allow all (security handled by API Key guard)
|
| 85 |
+
app.add_middleware(
|
| 86 |
+
CORSMiddleware,
|
| 87 |
+
allow_origins=["*"],
|
| 88 |
+
allow_credentials=True,
|
| 89 |
+
allow_methods=["GET", "POST"],
|
| 90 |
+
allow_headers=["*"],
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
import time
|
| 94 |
+
@app.middleware("http")
|
| 95 |
+
async def add_process_time_header(request: Request, call_next):
|
| 96 |
+
start_time = time.perf_counter()
|
| 97 |
+
response = await call_next(request)
|
| 98 |
+
process_time = (time.perf_counter() - start_time) * 1000
|
| 99 |
+
logger.info(f"Request: {request.method} {request.url.path} - Completed in {process_time:.2f}ms")
|
| 100 |
+
return response
|
| 101 |
+
|
| 102 |
+
app.include_router(embed_router)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, workers=settings.WORKERS)
|
app/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .request_models import SingleRequest, BatchRequest
|
app/models/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (274 Bytes). View file
|
|
|
app/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (255 Bytes). View file
|
|
|
app/models/__pycache__/request_models.cpython-311.pyc
ADDED
|
Binary file (1.35 kB). View file
|
|
|
app/models/__pycache__/request_models.cpython-312.pyc
ADDED
|
Binary file (1.12 kB). View file
|
|
|
app/models/request_models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/models/request_models.py
|
| 2 |
+
"""
|
| 3 |
+
Pydantic request models for the API endpoints.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import List
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class SingleRequest(BaseModel):
|
| 11 |
+
text: str = Field(
|
| 12 |
+
...,
|
| 13 |
+
description="The text content to be embedded.",
|
| 14 |
+
min_length=1,
|
| 15 |
+
examples=["Hello, this is a test sentence."]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class BatchRequest(BaseModel):
|
| 20 |
+
texts: List[str] = Field(
|
| 21 |
+
...,
|
| 22 |
+
description="List of text strings to embed.",
|
| 23 |
+
min_items=1,
|
| 24 |
+
examples=[["Hello world", "This is another sentence"]]
|
| 25 |
+
)
|
app/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .embeddings_service import EmbeddingsService
|
app/services/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (255 Bytes). View file
|
|
|
app/services/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (239 Bytes). View file
|
|
|
app/services/__pycache__/async_embeddings_service.cpython-311.pyc
ADDED
|
Binary file (3.72 kB). View file
|
|
|
app/services/__pycache__/embeddings_service.cpython-311.pyc
ADDED
|
Binary file (9.31 kB). View file
|
|
|
app/services/__pycache__/embeddings_service.cpython-312.pyc
ADDED
|
Binary file (8.49 kB). View file
|
|
|
app/services/async_embeddings_service.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AsyncEmbeddingsService:
|
| 3 |
+
- Wraps the synchronous EmbeddingsService to provide async interface
|
| 4 |
+
- Uses asyncio.to_thread to offload blocking model operations to a thread pool
|
| 5 |
+
- Manages concurrency with asyncio.Lock
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
from .embeddings_service import EmbeddingsService
|
| 11 |
+
|
| 12 |
+
class AsyncEmbeddingsService:
|
| 13 |
+
def __init__(self, model_name: Optional[str] = None):
|
| 14 |
+
# We compose the synchronous service
|
| 15 |
+
self._sync_service = EmbeddingsService(model_name)
|
| 16 |
+
self._lock = asyncio.Lock()
|
| 17 |
+
|
| 18 |
+
@property
|
| 19 |
+
def is_ready(self) -> bool:
|
| 20 |
+
return self._sync_service.is_ready
|
| 21 |
+
|
| 22 |
+
@property
|
| 23 |
+
def device(self):
|
| 24 |
+
return self._sync_service.device
|
| 25 |
+
|
| 26 |
+
async def load_model(self) -> None:
|
| 27 |
+
"""Async wrapper for loading the model."""
|
| 28 |
+
if self.is_ready:
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
async with self._lock:
|
| 32 |
+
if self.is_ready:
|
| 33 |
+
return
|
| 34 |
+
# Run the heavy lifting in a thread
|
| 35 |
+
await asyncio.to_thread(self._sync_service.load_model)
|
| 36 |
+
|
| 37 |
+
async def generate_single_embedding(self, text: str) -> List[float]:
|
| 38 |
+
"""
|
| 39 |
+
Generate a single embedding asynchronously.
|
| 40 |
+
"""
|
| 41 |
+
# Ensure model is loaded first (non-blocking check mostly)
|
| 42 |
+
await self.load_model()
|
| 43 |
+
return await asyncio.to_thread(self._sync_service.generate_single_embedding, text)
|
| 44 |
+
|
| 45 |
+
async def generate_batch_embeddings(self, texts: List[str]) -> List[List[float]]:
|
| 46 |
+
"""
|
| 47 |
+
Efficient batch processing asynchronously.
|
| 48 |
+
"""
|
| 49 |
+
await self.load_model()
|
| 50 |
+
return await asyncio.to_thread(self._sync_service.generate_batch_embeddings, texts)
|
app/services/embeddings_service.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
EmbeddingsService:
|
| 3 |
+
- Loads tokenizer + model with retries (tenacity)
|
| 4 |
+
- Efficiently computes embeddings for large batches by chunking into BATCH_SIZE
|
| 5 |
+
- Mean-pools the last hidden states to produce embeddings
|
| 6 |
+
- Handles OOM errors by falling back to micro-batch single-item processing
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from ..config import settings
|
| 10 |
+
from ..logger import logger
|
| 11 |
+
|
| 12 |
+
import math
|
| 13 |
+
from typing import List, Optional
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
from transformers import AutoTokenizer, AutoModel
|
| 17 |
+
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class EmbeddingsService:
|
| 21 |
+
def __init__(self, model_name: Optional[str] = None):
|
| 22 |
+
self.model_name = model_name or settings.MODEL_NAME
|
| 23 |
+
# If DEVICE is set in env, honor it; otherwise auto-detect GPU if available
|
| 24 |
+
if settings.DEVICE:
|
| 25 |
+
self.device = torch.device(settings.DEVICE)
|
| 26 |
+
else:
|
| 27 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 28 |
+
|
| 29 |
+
self.model: Optional[AutoModel] = None
|
| 30 |
+
self.tokenizer: Optional[AutoTokenizer] = None
|
| 31 |
+
self.is_ready: bool = False
|
| 32 |
+
|
| 33 |
+
# Retry decorator: will attempt to load model multiple times with exponential backoff
|
| 34 |
+
@retry(
|
| 35 |
+
stop=stop_after_attempt(settings.RETRY_ATTEMPTS),
|
| 36 |
+
wait=wait_exponential(multiplier=settings.RETRY_BACKOFF_SECONDS, max=20), # max wait 20 second for next retry
|
| 37 |
+
retry=retry_if_exception_type(Exception),
|
| 38 |
+
reraise=True,
|
| 39 |
+
)
|
| 40 |
+
def load_model(self) -> None:
|
| 41 |
+
if self.is_ready:
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
logger.info(f"Loading model: {self.model_name}")
|
| 45 |
+
logger.info(f"Target device: {self.device}")
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
#-------------------------------------------------
|
| 49 |
+
# Using private Hugging Face token to fetch model
|
| 50 |
+
self.tokenizer = AutoTokenizer.from_pretrained(
|
| 51 |
+
self.model_name,
|
| 52 |
+
use_fast=True,
|
| 53 |
+
token=settings.HUGGING_FACE_TOKEN
|
| 54 |
+
)
|
| 55 |
+
self.model = AutoModel.from_pretrained(
|
| 56 |
+
self.model_name,
|
| 57 |
+
use_safetensors=True,
|
| 58 |
+
token=settings.HUGGING_FACE_TOKEN
|
| 59 |
+
)
|
| 60 |
+
#-------------------------------------------------
|
| 61 |
+
self.model.to(self.device)
|
| 62 |
+
self.model.eval()
|
| 63 |
+
self.is_ready = True
|
| 64 |
+
logger.info(f"Model '{self.model_name}' loaded and ready.")
|
| 65 |
+
except Exception as exc:
|
| 66 |
+
logger.error(f"Failed to load model '{self.model_name}'")
|
| 67 |
+
logger.exception(exc)
|
| 68 |
+
raise
|
| 69 |
+
|
| 70 |
+
def _pool_embeddings(self, last_hidden_state: torch.Tensor) -> torch.Tensor:
|
| 71 |
+
"""Mean pooling across token dimension: (B, T, D) -> (B, D)."""
|
| 72 |
+
return last_hidden_state.mean(dim=1)
|
| 73 |
+
|
| 74 |
+
@torch.no_grad()
|
| 75 |
+
def generate_single_embedding(self, text: str) -> List[float]:
|
| 76 |
+
"""
|
| 77 |
+
Generate a single embedding synchronously.
|
| 78 |
+
"""
|
| 79 |
+
self.load_model()
|
| 80 |
+
if not text:
|
| 81 |
+
logger.warning("Received empty text for embedding")
|
| 82 |
+
raise ValueError("Empty text provided for embedding.")
|
| 83 |
+
|
| 84 |
+
log_text = (text[:100] + '...') if len(text) > 100 else text
|
| 85 |
+
logger.debug(f"Processing single embedding for text: '{log_text}'")
|
| 86 |
+
|
| 87 |
+
# debuging here text length by tokens
|
| 88 |
+
# token_count = len(self.tokenizer.tokenize(text))
|
| 89 |
+
# if token_count > settings.MAX_LENGTH:
|
| 90 |
+
# logger.warning(f"Text token count {token_count} is longer than max length ({settings.MAX_LENGTH}), truncating...")
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
inputs = self.tokenizer(
|
| 94 |
+
text,
|
| 95 |
+
return_tensors="pt",
|
| 96 |
+
truncation=True,
|
| 97 |
+
padding=True,
|
| 98 |
+
max_length=settings.MAX_LENGTH,
|
| 99 |
+
).to(self.device)
|
| 100 |
+
|
| 101 |
+
outputs = self.model(**inputs)
|
| 102 |
+
emb = self._pool_embeddings(outputs.last_hidden_state)
|
| 103 |
+
|
| 104 |
+
return emb.cpu().numpy().flatten().tolist()
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.error("Critical error during single embedding generation")
|
| 107 |
+
logger.exception(e)
|
| 108 |
+
raise
|
| 109 |
+
|
| 110 |
+
@torch.no_grad()
|
| 111 |
+
def generate_batch_embeddings(self, texts: List[str]) -> List[List[float]]:
|
| 112 |
+
"""
|
| 113 |
+
Efficient batch processing with OOM recovery (Sync).
|
| 114 |
+
"""
|
| 115 |
+
self.load_model()
|
| 116 |
+
if not texts:
|
| 117 |
+
return []
|
| 118 |
+
|
| 119 |
+
batch_size = max(1, settings.BATCH_SIZE)
|
| 120 |
+
embeddings: List[List[float]] = []
|
| 121 |
+
|
| 122 |
+
total = len(texts)
|
| 123 |
+
num_chunks = math.ceil(total / batch_size)
|
| 124 |
+
logger.info(f"Processing {total} texts in {num_chunks} chunks (batch_size={batch_size})")
|
| 125 |
+
|
| 126 |
+
for i in range(0, total, batch_size):
|
| 127 |
+
chunk = texts[i : i + batch_size]
|
| 128 |
+
try:
|
| 129 |
+
# debuging here every text
|
| 130 |
+
for index, text in enumerate(chunk):
|
| 131 |
+
log_text = (text[:100] + '...') if len(text) > 100 else text
|
| 132 |
+
logger.debug(f"Processing chunk-{i+1}, embedding for text-{index+1}: '{log_text}'")
|
| 133 |
+
|
| 134 |
+
# debuging here text length by tokens
|
| 135 |
+
# token_count = len(self.tokenizer.tokenize(text))
|
| 136 |
+
# if token_count > settings.MAX_LENGTH:
|
| 137 |
+
# logger.warning(f"Text token count {token_count} is longer than max length ({settings.MAX_LENGTH}), truncating...")
|
| 138 |
+
|
| 139 |
+
inputs = self.tokenizer(
|
| 140 |
+
chunk,
|
| 141 |
+
return_tensors="pt",
|
| 142 |
+
truncation=True,
|
| 143 |
+
padding=True,
|
| 144 |
+
max_length=settings.MAX_LENGTH,
|
| 145 |
+
).to(self.device)
|
| 146 |
+
|
| 147 |
+
outputs = self.model(**inputs)
|
| 148 |
+
chunk_embs = self._pool_embeddings(outputs.last_hidden_state)
|
| 149 |
+
embeddings.extend(chunk_embs.cpu().numpy().tolist())
|
| 150 |
+
except RuntimeError as e:
|
| 151 |
+
# Basic check for OOM
|
| 152 |
+
msg = str(e).lower()
|
| 153 |
+
if "out of memory" in msg or "cuda out of memory" in msg:
|
| 154 |
+
logger.warning(f"GPU OOM on chunk {i//batch_size + 1}. Attempting micro-batch fallback...")
|
| 155 |
+
try:
|
| 156 |
+
torch.cuda.empty_cache()
|
| 157 |
+
except: pass
|
| 158 |
+
for t in chunk:
|
| 159 |
+
embeddings.append(self.generate_single_embedding(t))
|
| 160 |
+
else:
|
| 161 |
+
logger.error(f"Runtime error in batch processing at chunk {i//batch_size + 1}")
|
| 162 |
+
logger.exception(e)
|
| 163 |
+
raise
|
| 164 |
+
except Exception as e:
|
| 165 |
+
logger.error(f"Unexpected error in batch processing at chunk {i//batch_size + 1}")
|
| 166 |
+
logger.exception(e)
|
| 167 |
+
raise
|
| 168 |
+
|
| 169 |
+
return embeddings
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker Compose file for BGE Embedding Server
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
embed-server:
|
| 5 |
+
build: .
|
| 6 |
+
container_name: bge_embed_server
|
| 7 |
+
environment:
|
| 8 |
+
- EMBED_API_KEY=${EMBED_API_KEY}
|
| 9 |
+
- MODEL_NAME=${MODEL_NAME}
|
| 10 |
+
- BATCH_SIZE=${BATCH_SIZE:-8}
|
| 11 |
+
- MAX_LENGTH=${MAX_LENGTH:-1024}
|
| 12 |
+
- RETRY_ATTEMPTS=${RETRY_ATTEMPTS:-3}
|
| 13 |
+
- RETRY_BACKOFF_SECONDS=${RETRY_BACKOFF_SECONDS:-2}
|
| 14 |
+
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
| 15 |
+
- PORT=${PORT}
|
| 16 |
+
- HOST=${HOST}
|
| 17 |
+
- WORKERS=${WORKERS}
|
| 18 |
+
- DEVICE=${DEVICE}
|
| 19 |
+
ports:
|
| 20 |
+
- "${PORT:-7860}:7860"
|
| 21 |
+
volumes:
|
| 22 |
+
- ./:/app
|
| 23 |
+
restart: unless-stopped
|
| 24 |
+
# If i have GPU server i simply uncomment it to use gpu
|
| 25 |
+
# deploy:
|
| 26 |
+
# resources:
|
| 27 |
+
# reservations:
|
| 28 |
+
# devices:
|
| 29 |
+
# - driver: nvidia
|
| 30 |
+
# count: all
|
| 31 |
+
# capabilities: [ gpu ]
|
prompt.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
You are an AI Sales Assistant representing {{ORGANIZATION_NAME}}.
|
| 3 |
+
Communicate in the {{organization_prefered}} style.
|
| 4 |
+
|
| 5 |
+
You will receive each turn:
|
| 6 |
+
- ORGANIZATION_SUMMARY: {{ORGANIZATION_SUMMARY}}
|
| 7 |
+
- CHAT_HISTORY: {{CHAT_HISTORY}}
|
| 8 |
+
- CURRENT USER MESSAGE: {{USER_MESSAGE}}
|
| 9 |
+
- LEAD_FIELDS: {{LEAD_FIELDS_JSON}}
|
| 10 |
+
- ORGANIZATION_EXTRA_RULE (optional_field): {{ORGANIZATION_EXTRA_RULE}}
|
| 11 |
+
- OLD_CHAT_SUMMARY (optional_field): {{OLD_CHAT_SUMMARY}}
|
| 12 |
+
- EXTRA_DEATILS_ABOUT_THIS_LEAD (optional_field): {{EXTRA_DEATILS_ABOUT_THIS_LEAD}}
|
| 13 |
+
|
| 14 |
+
CRITICAL: Return ONLY valid JSON. No markdown, no explanation, no extra text.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
CORE RULES
|
| 18 |
+
---
|
| 19 |
+
1. "user_reply" is the actual SMS sent to user. Max 4/5 sentences if possible. Keep it conversational and brief.
|
| 20 |
+
2. Ask at most ONE follow-up question per reply about ONE null field in LEAD_FIELDS.
|
| 21 |
+
3. Auto-fill fields from context/images when possible before asking.
|
| 22 |
+
4. NEVER overwrite non-null LEAD_FIELDS unless user explicitly corrects it.
|
| 23 |
+
5. If user provides info for a field, update that field in lead_fields.
|
| 24 |
+
6. If calling a tool, return ONLY tool_call JSON (see TOOLS section).
|
| 25 |
+
7. If user is abusive/spam, respond politely once, set "auto_reply_off": true, "escalate_to_human": false.
|
| 26 |
+
8. If project is significantly larger than typical (check ORGANIZATION_SUMMARY and ORGANIZATION_EXTRA_RULE), set "escalate_to_human": true and say "Our senior team will contact you very soon."
|
| 27 |
+
9. NEVER hallucinate. If you don't have info, tell user politely you'll have them contacted shortly.
|
| 28 |
+
10. If user wants to book a meeting, allow it anytime - don't wait for other details. Try to get preferred date/time; if they say "anytime" put that in optional_sms.
|
| 29 |
+
11. If all LEAD_FIELDS are filled (no nulls), suggest booking a meeting to discuss their project.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
TOOLS
|
| 33 |
+
---
|
| 34 |
+
If using a tool, return ONLY this:
|
| 35 |
+
|
| 36 |
+
{
|
| 37 |
+
"tool_call": {
|
| 38 |
+
"tool_name": "<tool_name>",
|
| 39 |
+
"parameters": { ... }
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
Available tools:
|
| 44 |
+
|
| 45 |
+
search_product
|
| 46 |
+
{ "query": "description of product/image" }
|
| 47 |
+
Use: user asks about products or sends product image
|
| 48 |
+
|
| 49 |
+
request_quotation
|
| 50 |
+
{
|
| 51 |
+
"message": "brief summary of what user wants quoted/ordered",
|
| 52 |
+
"details": "specific details, quantities, specs, generated demo quotation etc.",
|
| 53 |
+
"urgent": true or false
|
| 54 |
+
}
|
| 55 |
+
Use: user requests quotation, wants to order, or asks for pricing on specific items
|
| 56 |
+
|
| 57 |
+
book_meeting
|
| 58 |
+
{
|
| 59 |
+
"preferred_date": "YYYY-MM-DD or null",
|
| 60 |
+
"preferred_time": "HH:MM or null",
|
| 61 |
+
"contact_name": "string or null",
|
| 62 |
+
"optional_sms": "any additional notes like 'anytime works' or meeting purpose"
|
| 63 |
+
}
|
| 64 |
+
Use: user requests meeting/scheduling (allow anytime, even before getting other details)
|
| 65 |
+
|
| 66 |
+
set_reminder
|
| 67 |
+
{
|
| 68 |
+
"reminder_time": "YYYY-MM-DD HH:MM",
|
| 69 |
+
"note": "context note"
|
| 70 |
+
}
|
| 71 |
+
Use: user asks to be reminded later
|
| 72 |
+
|
| 73 |
+
notify_human
|
| 74 |
+
{ "notification_message": "what needs human attention" }
|
| 75 |
+
Use: user asks question you can't answer or needs immediate human help
|
| 76 |
+
|
| 77 |
+
TOOL RESPONSE:
|
| 78 |
+
Next turn after tool_call, you'll receive PREVIOUS_TOOL_CALL and TOOL_RESPONSE. Incorporate the result into conversation (max 4/5 sentences) and update lead_fields if applicable. For request_quotation and book_meeting, confirm to user they'll be contacted soon by the team.
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
OUTPUT FORMAT (when NOT calling tool)
|
| 82 |
+
---
|
| 83 |
+
{
|
| 84 |
+
"user_reply": "<SMS text, max 4/5 sentences>",
|
| 85 |
+
"lead_fields": {
|
| 86 |
+
"budget": <string|null>,
|
| 87 |
+
"project_duration": <string|null>,
|
| 88 |
+
"deadline": <string|null>,
|
| 89 |
+
"project_type": <string|null>
|
| 90 |
+
},
|
| 91 |
+
"escalate_to_human": <true|false>,
|
| 92 |
+
"auto_reply_off": <true|false>,
|
| 93 |
+
"tool_call": null
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
Rules:
|
| 97 |
+
- lead_fields: updated state after this turn. Keep existing non-null values.
|
| 98 |
+
- escalate_to_human: true only for large leads or urgent human-required situations
|
| 99 |
+
- auto_reply_off: true only for spam/abuse/non-leads
|
| 100 |
+
- tool_call: must be null in this format
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
BEHAVIOR
|
| 104 |
+
---
|
| 105 |
+
- Auto-fill fields from user message/image before asking questions
|
| 106 |
+
- Ask ONE high-value question if user shows buying intent
|
| 107 |
+
- Use appropriate tool when user requests product info, quotation, meeting, reminder, or asks unanswerable question
|
| 108 |
+
- Allow meeting booking anytime - don't require other details first
|
| 109 |
+
- If all lead_fields are filled, suggest booking a meeting to discuss their project
|
| 110 |
+
- Never make up pricing, availability, discount, hallucinate, or product details
|
| 111 |
+
- Be concise and natural like human in user_reply
|
| 112 |
+
|
| 113 |
+
"""
|
requirements.lock.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.129.0
|
| 2 |
+
uvicorn[standard]==0.40.0
|
| 3 |
+
transformers==5.1.0
|
| 4 |
+
torch==2.10.0
|
| 5 |
+
accelerate==1.12.0
|
| 6 |
+
pydantic==2.12.5
|
| 7 |
+
python-dotenv==1.2.1
|
| 8 |
+
tenacity==9.1.4
|
| 9 |
+
colorlog==6.10.1
|
| 10 |
+
pydantic-settings==2.13.0
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.95.0
|
| 2 |
+
uvicorn[standard]>=0.22.0
|
| 3 |
+
transformers>=4.35.0
|
| 4 |
+
torch>=2.0.0
|
| 5 |
+
accelerate>=0.20.0
|
| 6 |
+
pydantic>=1.10.0
|
| 7 |
+
python-dotenv>=1.0.0
|
| 8 |
+
tenacity>=8.2.0
|
| 9 |
+
colorlog>=6.7.0
|
| 10 |
+
pydantic-settings>=2.0.0
|
scripts/__pycache__/convert_safetensors.cpython-311.pyc
ADDED
|
Binary file (3.54 kB). View file
|
|
|
scripts/convert_safetensors.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#=====================================================================================================
|
| 2 |
+
# This script converts a PyTorch model to SafeTensors format and uploads it to Hugging Face Hub.
|
| 3 |
+
# It is used to fix the "safetensors not found" error.
|
| 4 |
+
# Usage: python scripts/convert_safetensors.py
|
| 5 |
+
# It need to Run only once if the "safetensors not found" error came otherwise don't run it.
|
| 6 |
+
# NOTE: U need write access to the repo to upload the model.
|
| 7 |
+
#=====================================================================================================
|
| 8 |
+
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
import shutil
|
| 12 |
+
|
| 13 |
+
# Adjust path to find app module
|
| 14 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 15 |
+
parent_dir = os.path.dirname(current_dir)
|
| 16 |
+
sys.path.append(parent_dir)
|
| 17 |
+
|
| 18 |
+
from app.config import settings
|
| 19 |
+
from transformers import AutoModel, AutoTokenizer
|
| 20 |
+
from huggingface_hub import HfApi
|
| 21 |
+
|
| 22 |
+
def convert():
|
| 23 |
+
model_name = settings.MODEL_NAME
|
| 24 |
+
token = settings.HUGGING_FACE_TOKEN
|
| 25 |
+
temp_dir = os.path.join(parent_dir, "temp_safe_model")
|
| 26 |
+
|
| 27 |
+
print(f"Loading original model: {model_name}...")
|
| 28 |
+
try:
|
| 29 |
+
# Load the PyTorch version explicitly
|
| 30 |
+
model = AutoModel.from_pretrained(
|
| 31 |
+
model_name,
|
| 32 |
+
use_safetensors=False,
|
| 33 |
+
token=token
|
| 34 |
+
)
|
| 35 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 36 |
+
model_name,
|
| 37 |
+
token=token
|
| 38 |
+
)
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Failed to load original model: {e}")
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
print("Saving model locally with SafeTensors format...")
|
| 44 |
+
try:
|
| 45 |
+
if os.path.exists(temp_dir):
|
| 46 |
+
shutil.rmtree(temp_dir)
|
| 47 |
+
os.makedirs(temp_dir, exist_ok=True)
|
| 48 |
+
|
| 49 |
+
model.save_pretrained(temp_dir, safe_serialization=True)
|
| 50 |
+
tokenizer.save_pretrained(temp_dir)
|
| 51 |
+
|
| 52 |
+
# --- Verification Step ---
|
| 53 |
+
print("Verifying converted model by loading it back...")
|
| 54 |
+
try:
|
| 55 |
+
# Try to load the model from the temporary directory using SafeTensors
|
| 56 |
+
check_model = AutoModel.from_pretrained(temp_dir, use_safetensors=True)
|
| 57 |
+
print("Verification successful! Model loaded correctly from SafeTensors.")
|
| 58 |
+
# memory cleanup
|
| 59 |
+
del check_model
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Verification FAILED: {e}")
|
| 62 |
+
print("Aborting upload.")
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
# --- Upload Step ---
|
| 66 |
+
print("Model verified. Now uploading to Hub...")
|
| 67 |
+
|
| 68 |
+
api = HfApi(token=token)
|
| 69 |
+
api.upload_folder(
|
| 70 |
+
folder_path=temp_dir,
|
| 71 |
+
repo_id=model_name,
|
| 72 |
+
repo_type="model"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
print("Success! The model has been converted, verified, and pushed to your repository.")
|
| 76 |
+
print("The auto-conversion error should now be resolved.")
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"An error occurred during the process: {e}")
|
| 80 |
+
finally:
|
| 81 |
+
# Cleanup
|
| 82 |
+
if os.path.exists(temp_dir):
|
| 83 |
+
shutil.rmtree(temp_dir)
|
| 84 |
+
print("Cleaned up temporary files.")
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
convert()
|