Faysal4200 commited on
Commit
ec855e6
·
verified ·
1 Parent(s): c49f87a

Upload 45 files

Browse files
Files changed (45) hide show
  1. .env.example +40 -0
  2. .gitignore +2 -0
  3. Dockerfile +37 -0
  4. README.md +252 -10
  5. app/__init__.py +2 -0
  6. app/__pycache__/__init__.cpython-311.pyc +0 -0
  7. app/__pycache__/__init__.cpython-312.pyc +0 -0
  8. app/__pycache__/auth.cpython-311.pyc +0 -0
  9. app/__pycache__/auth.cpython-312.pyc +0 -0
  10. app/__pycache__/config.cpython-311.pyc +0 -0
  11. app/__pycache__/config.cpython-312.pyc +0 -0
  12. app/__pycache__/logger.cpython-311.pyc +0 -0
  13. app/__pycache__/logger.cpython-312.pyc +0 -0
  14. app/__pycache__/main.cpython-311.pyc +0 -0
  15. app/__pycache__/main.cpython-312.pyc +0 -0
  16. app/auth.py +23 -0
  17. app/config.py +72 -0
  18. app/controllers/__init__.py +1 -0
  19. app/controllers/__pycache__/__init__.cpython-311.pyc +0 -0
  20. app/controllers/__pycache__/__init__.cpython-312.pyc +0 -0
  21. app/controllers/__pycache__/embed_controller.cpython-311.pyc +0 -0
  22. app/controllers/__pycache__/embed_controller.cpython-312.pyc +0 -0
  23. app/controllers/embed_controller.py +54 -0
  24. app/logger.py +59 -0
  25. app/main.py +106 -0
  26. app/models/__init__.py +1 -0
  27. app/models/__pycache__/__init__.cpython-311.pyc +0 -0
  28. app/models/__pycache__/__init__.cpython-312.pyc +0 -0
  29. app/models/__pycache__/request_models.cpython-311.pyc +0 -0
  30. app/models/__pycache__/request_models.cpython-312.pyc +0 -0
  31. app/models/request_models.py +25 -0
  32. app/services/__init__.py +1 -0
  33. app/services/__pycache__/__init__.cpython-311.pyc +0 -0
  34. app/services/__pycache__/__init__.cpython-312.pyc +0 -0
  35. app/services/__pycache__/async_embeddings_service.cpython-311.pyc +0 -0
  36. app/services/__pycache__/embeddings_service.cpython-311.pyc +0 -0
  37. app/services/__pycache__/embeddings_service.cpython-312.pyc +0 -0
  38. app/services/async_embeddings_service.py +50 -0
  39. app/services/embeddings_service.py +169 -0
  40. docker-compose.yml +31 -0
  41. prompt.py +113 -0
  42. requirements.lock.txt +10 -0
  43. requirements.txt +10 -0
  44. scripts/__pycache__/convert_safetensors.cpython-311.pyc +0 -0
  45. 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
- title: Embedding Server
3
- emoji: 🚀
4
- colorFrom: yellow
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()