AI Development Team commited on
Commit
8a34091
Β·
1 Parent(s): 5c4bfe8

Fix backend API files for HF deployment

Browse files
Files changed (4) hide show
  1. README.md +16 -208
  2. api/auth.py +123 -89
  3. api/translation.py +79 -67
  4. requirements.txt +15 -18
README.md CHANGED
@@ -1,212 +1,20 @@
1
- # AI Backend with RAG + Authentication
 
 
 
 
 
 
 
 
2
 
3
- A scalable backend featuring authentication, RAG capabilities, and integration with external services. The system uses Better Auth for authentication, Qdrant for vector storage, Neon Postgres for relational data, and Google's Gemini models for embeddings and chat functionality.
4
 
5
- ## Architecture Overview
6
-
7
- ```
8
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
9
- β”‚ Frontend │────│ FastAPI │────│ Better Auth β”‚
10
- β”‚ (Future) β”‚ β”‚ Backend β”‚ β”‚ Service β”‚
11
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
12
- β”‚
13
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
14
- β”‚ β”‚ β”‚
15
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
16
- β”‚ Qdrant β”‚ β”‚ Neon β”‚ β”‚ Gemini β”‚
17
- β”‚ Vector DB β”‚ β”‚ Postgres β”‚ β”‚ API β”‚
18
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
19
- ```
20
 
21
  ## Features
22
-
23
- - **Authentication**: JWT-based authentication with Better Auth
24
- - **RAG Pipeline**: Retrieval-Augmented Generation with Qdrant vector database
25
- - **AI Integration**: Google Gemini for embeddings and chat responses
26
- - **Database**: Neon Postgres for user data and chat history
27
- - **Security**: Password hashing, JWT validation, user isolation
28
- - **Scalability**: Async architecture with connection pooling
29
-
30
- ## Prerequisites
31
-
32
- - Python 3.9+
33
- - Qdrant vector database instance
34
- - Neon Postgres database
35
- - Google Gemini API key
36
- - Node.js (for development tools, optional)
37
-
38
- ## Setup
39
-
40
- ### 1. Clone the repository
41
-
42
- ```bash
43
- git clone <repository-url>
44
- cd backend
45
- ```
46
-
47
- ### 2. Create a virtual environment
48
-
49
- ```bash
50
- python -m venv venv
51
- source venv/bin/activate # On Windows: venv\Scripts\activate
52
- ```
53
-
54
- ### 3. Install dependencies
55
-
56
- ```bash
57
- pip install -r requirements.txt
58
- ```
59
-
60
- ### 4. Configure environment variables
61
-
62
- Copy the example environment file:
63
-
64
- ```bash
65
- cp .env.example .env
66
- ```
67
-
68
- Then edit `.env` with your actual configuration:
69
-
70
- ```env
71
- # API Configuration
72
- GEMINI_API_KEY=your_gemini_api_key_here
73
- QDRANT_URL=your_qdrant_url_here
74
- QDRANT_API_KEY=your_qdrant_api_key_here
75
- NEON_DB_URL=your_neon_db_connection_string_here
76
-
77
- # JWT Configuration
78
- SECRET_KEY=your_secret_key_here # Use a strong, random secret key
79
- JWT_EXPIRES_IN=3600
80
-
81
- # Application Configuration
82
- DEBUG=false
83
- LOG_LEVEL=info
84
- ```
85
-
86
- ### 5. Run the application
87
-
88
- ```bash
89
- cd src
90
- python main.py
91
- ```
92
-
93
- Or using uvicorn directly:
94
-
95
- ```bash
96
- cd src
97
- uvicorn main:app --reload --host 0.0.0.0 --port 8000
98
- ```
99
-
100
- The application will be available at `http://localhost:8000`
101
-
102
- ## API Endpoints
103
-
104
- ### Authentication
105
- - `POST /auth/signup` - User registration
106
- - `POST /auth/login` - User login
107
- - `GET /auth/me` - Get current user info
108
-
109
- ### RAG & Embeddings
110
- - `POST /embed` - Generate embeddings for text
111
- - `POST /save-document` - Save and embed a document
112
- - `POST /search` - Semantic search in documents
113
- - `POST /chat` - Chat with RAG context
114
-
115
- ### History
116
- - `GET /history` - Get chat history
117
- - `GET /history/{conversation_id}` - Get specific conversation
118
-
119
- ### Health
120
- - `GET /health` - Health check endpoint
121
-
122
- ## Project Structure
123
-
124
- ```
125
- backend/
126
- β”œβ”€β”€ src/
127
- β”‚ β”œβ”€β”€ __init__.py
128
- β”‚ β”œβ”€β”€ main.py # Application entry point
129
- β”‚ β”œβ”€β”€ config/ # Configuration management
130
- β”‚ β”‚ β”œβ”€β”€ __init__.py
131
- β”‚ β”‚ β”œβ”€β”€ settings.py # App settings and env vars
132
- β”‚ β”‚ └── database.py # Database configuration
133
- β”‚ β”œβ”€β”€ auth/ # Authentication module
134
- β”‚ β”œβ”€β”€ db/ # Database module
135
- β”‚ β”‚ β”œβ”€β”€ __init__.py
136
- β”‚ β”‚ β”œβ”€β”€ base.py # Base model class
137
- β”‚ β”‚ β”œβ”€β”€ models/ # SQLAlchemy models
138
- β”‚ β”‚ β”œβ”€β”€ database.py # Database connection
139
- β”‚ β”‚ └── crud.py # CRUD operations
140
- β”‚ β”œβ”€β”€ qdrant/ # Vector database module
141
- β”‚ β”œβ”€β”€ embeddings/ # Embedding module
142
- β”‚ β”œβ”€β”€ rag/ # RAG pipeline module
143
- β”‚ β”œβ”€β”€ routes/ # API routes
144
- β”‚ β”œβ”€β”€ models/ # Pydantic models
145
- β”‚ β”œβ”€β”€ utils/ # Utility functions
146
- β”‚ └── scripts/ # Utility scripts
147
- β”œβ”€β”€ tests/ # Test suite
148
- β”œβ”€β”€ requirements.txt # Python dependencies
149
- β”œβ”€β”€ .env.example # Environment variables template
150
- └── README.md # Documentation
151
- ```
152
-
153
- ## Development
154
-
155
- ### Running tests
156
-
157
- ```bash
158
- cd backend
159
- python -m pytest tests/ -v
160
- ```
161
-
162
- ### Running with auto-reload during development
163
-
164
- ```bash
165
- cd src
166
- uvicorn main:app --reload
167
- ```
168
-
169
- ## Environment Variables
170
-
171
- | Variable | Description | Required |
172
- |----------|-------------|----------|
173
- | GEMINI_API_KEY | Google Gemini API key | Yes |
174
- | QDRANT_URL | Qdrant vector database URL | Yes |
175
- | QDRANT_API_KEY | Qdrant API key (if secured) | No |
176
- | NEON_DB_URL | Neon Postgres connection string | Yes |
177
- | SECRET_KEY | JWT secret key | Yes |
178
- | JWT_EXPIRES_IN | JWT expiration time in seconds | No (default: 3600) |
179
- | DEBUG | Enable debug mode | No (default: false) |
180
- | LOG_LEVEL | Logging level | No (default: info) |
181
-
182
- ## Security Considerations
183
-
184
- - Always use HTTPS in production
185
- - Store secrets securely (not in version control)
186
- - Validate and sanitize all user inputs
187
- - Use parameterized queries to prevent SQL injection
188
- - Implement rate limiting to prevent abuse
189
- - Use strong, randomly generated secret keys
190
-
191
- ## Performance
192
-
193
- - Async architecture for high concurrency
194
- - Connection pooling for database operations
195
- - Caching mechanisms for frequently accessed data
196
- - Optimized vector search with Qdrant
197
- - Efficient embedding processing pipeline
198
-
199
- ## Contributing
200
-
201
- 1. Fork the repository
202
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
203
- 3. Make your changes
204
- 4. Add tests if applicable
205
- 5. Run tests (`python -m pytest`)
206
- 6. Commit your changes (`git commit -m 'Add amazing feature'`)
207
- 7. Push to the branch (`git push origin feature/amazing-feature`)
208
- 8. Open a Pull Request
209
-
210
- ## License
211
-
212
- [Add your license here]
 
1
+ ο»Ώ---
2
+ title: AI Textbook Backend
3
+ emoji: πŸ€–
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_file: main.py
8
+ pinned: false
9
+ ---
10
 
11
+ # AI Native Textbook Backend
12
 
13
+ RAG-based chatbot backend for Physical AI & Humanoid Robotics textbook.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  ## Features
16
+ - RAG-based Q&A with Qdrant vector database
17
+ - OpenRouter LLM integration
18
+ - Urdu translation support
19
+ - User authentication with JWT
20
+ - Personalized learning roadmaps
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/auth.py CHANGED
@@ -2,24 +2,25 @@ from fastapi import APIRouter, HTTPException, Depends, Header
2
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
  from pydantic import BaseModel, EmailStr
4
  from typing import Optional
5
- from sqlalchemy.orm import Session
6
- from sqlalchemy.exc import IntegrityError
7
  import logging
8
 
 
 
 
 
9
  # Import database utilities
10
  try:
11
  from database.db import get_db
12
  from database.models import User as DBUser
13
  from auth.jwt_utils import hash_password, verify_password, create_access_token, get_current_user_id_from_token
 
 
14
  DB_ENABLED = True
15
  except ImportError:
16
  DB_ENABLED = False
 
17
  logging.warning("Database modules not available. Auth will use mock mode.")
18
 
19
- logger = logging.getLogger(__name__)
20
- router = APIRouter()
21
- security = HTTPBearer()
22
-
23
  class SignupRequest(BaseModel):
24
  email: EmailStr
25
  password: str
@@ -44,11 +45,19 @@ class UserProfileResponse(BaseModel):
44
  hardware_background: Optional[str]
45
  experience_level: Optional[str]
46
 
 
 
 
 
 
 
 
 
47
  @router.post("/auth/signup", response_model=AuthResponse)
48
- async def signup(request: SignupRequest, db: Session = Depends(get_db) if DB_ENABLED else None):
49
  """Handle user registration with background information"""
50
  try:
51
- if not DB_ENABLED or db is None:
52
  # Mock mode for testing without database
53
  return AuthResponse(
54
  user_id="mock_user_id",
@@ -57,59 +66,68 @@ async def signup(request: SignupRequest, db: Session = Depends(get_db) if DB_ENA
57
  token_type="bearer"
58
  )
59
 
60
- # Validate password strength
61
- if len(request.password) < 8:
62
- raise HTTPException(status_code=400, detail="Password must be at least 8 characters long")
63
-
64
- # Validate background fields
65
- if request.software_background and len(request.software_background) < 10:
66
- raise HTTPException(status_code=400, detail="Software background must be at least 10 characters")
67
- if request.hardware_background and len(request.hardware_background) < 10:
68
- raise HTTPException(status_code=400, detail="Hardware background must be at least 10 characters")
69
-
70
- # Hash the password
71
- hashed_password = hash_password(request.password)
72
-
73
- # Create new user in database
74
- db_user = DBUser(
75
- email=request.email,
76
- password_hash=hashed_password,
77
- software_background=request.software_background,
78
- hardware_background=request.hardware_background,
79
- experience_level=request.experience_level or "Intermediate"
80
- )
81
-
82
- db.add(db_user)
83
- db.commit()
84
- db.refresh(db_user)
85
-
86
- # Generate JWT token
87
- access_token = create_access_token(data={
88
- "sub": str(db_user.id),
89
- "email": db_user.email
90
- })
91
-
92
- return AuthResponse(
93
- user_id=str(db_user.id),
94
- email=db_user.email,
95
- access_token=access_token,
96
- token_type="bearer"
97
- )
98
-
99
- except IntegrityError:
100
- db.rollback()
101
- raise HTTPException(status_code=400, detail="Email already registered")
 
 
 
 
 
 
 
 
 
 
102
  except Exception as e:
103
  logger.error(f"Error during signup: {str(e)}")
104
- if db:
105
- db.rollback()
106
  raise HTTPException(status_code=500, detail=f"Error during signup: {str(e)}")
107
 
 
108
  @router.post("/auth/login", response_model=AuthResponse)
109
- async def login(request: LoginRequest, db: Session = Depends(get_db) if DB_ENABLED else None):
110
  """Handle user login"""
111
  try:
112
- if not DB_ENABLED or db is None:
113
  # Mock mode for testing without database
114
  return AuthResponse(
115
  user_id="mock_user_id",
@@ -118,27 +136,35 @@ async def login(request: LoginRequest, db: Session = Depends(get_db) if DB_ENABL
118
  token_type="bearer"
119
  )
120
 
121
- # Find user by email
122
- user = db.query(DBUser).filter(DBUser.email == request.email).first()
123
- if not user:
124
- raise HTTPException(status_code=401, detail="Invalid credentials")
125
 
126
- # Verify password
127
- if not verify_password(request.password, user.password_hash):
128
- raise HTTPException(status_code=401, detail="Invalid credentials")
 
 
129
 
130
- # Generate JWT token
131
- access_token = create_access_token(data={
132
- "sub": str(user.id),
133
- "email": user.email
134
- })
135
 
136
- return AuthResponse(
137
- user_id=str(user.id),
138
- email=user.email,
139
- access_token=access_token,
140
- token_type="bearer"
141
- )
 
 
 
 
 
 
 
 
 
142
 
143
  except HTTPException:
144
  raise
@@ -146,15 +172,15 @@ async def login(request: LoginRequest, db: Session = Depends(get_db) if DB_ENABL
146
  logger.error(f"Error during login: {str(e)}")
147
  raise HTTPException(status_code=500, detail="Error during login")
148
 
 
149
  @router.get("/auth/profile", response_model=UserProfileResponse)
150
  async def get_profile(
151
  authorization: Optional[str] = Header(None),
152
- credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
153
- db: Session = Depends(get_db) if DB_ENABLED else None
154
  ):
155
  """Get user profile information"""
156
  try:
157
- if not DB_ENABLED or db is None:
158
  # Mock mode for testing without database
159
  return UserProfileResponse(
160
  user_id="mock_user_id",
@@ -166,10 +192,10 @@ async def get_profile(
166
 
167
  # Get token from Authorization header or credentials
168
  token = None
169
- if authorization:
170
- token = authorization
171
- elif credentials:
172
  token = credentials.credentials
 
 
173
 
174
  if not token:
175
  raise HTTPException(status_code=401, detail="Authorization token required")
@@ -179,18 +205,25 @@ async def get_profile(
179
  if not user_id:
180
  raise HTTPException(status_code=401, detail="Invalid or expired token")
181
 
182
- # Get user from database
183
- user = db.query(DBUser).filter(DBUser.id == user_id).first()
184
- if not user:
185
- raise HTTPException(status_code=404, detail="User not found")
186
 
187
- return UserProfileResponse(
188
- user_id=str(user.id),
189
- email=user.email,
190
- software_background=user.software_background,
191
- hardware_background=user.hardware_background,
192
- experience_level=user.experience_level
193
- )
 
 
 
 
 
 
 
 
194
 
195
  except HTTPException:
196
  raise
@@ -198,6 +231,7 @@ async def get_profile(
198
  logger.error(f"Error retrieving profile: {str(e)}")
199
  raise HTTPException(status_code=500, detail="Error retrieving profile")
200
 
 
201
  @router.get("/auth/health")
202
  async def auth_health():
203
  """Health check for auth service"""
 
2
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
  from pydantic import BaseModel, EmailStr
4
  from typing import Optional
 
 
5
  import logging
6
 
7
+ logger = logging.getLogger(__name__)
8
+ router = APIRouter()
9
+ security = HTTPBearer(auto_error=False)
10
+
11
  # Import database utilities
12
  try:
13
  from database.db import get_db
14
  from database.models import User as DBUser
15
  from auth.jwt_utils import hash_password, verify_password, create_access_token, get_current_user_id_from_token
16
+ from sqlalchemy.orm import Session
17
+ from sqlalchemy.exc import IntegrityError
18
  DB_ENABLED = True
19
  except ImportError:
20
  DB_ENABLED = False
21
+ get_db = None
22
  logging.warning("Database modules not available. Auth will use mock mode.")
23
 
 
 
 
 
24
  class SignupRequest(BaseModel):
25
  email: EmailStr
26
  password: str
 
45
  hardware_background: Optional[str]
46
  experience_level: Optional[str]
47
 
48
+
49
+ def get_db_session():
50
+ """Get database session or None if DB not enabled"""
51
+ if DB_ENABLED and get_db:
52
+ return Depends(get_db)
53
+ return None
54
+
55
+
56
  @router.post("/auth/signup", response_model=AuthResponse)
57
+ async def signup(request: SignupRequest):
58
  """Handle user registration with background information"""
59
  try:
60
+ if not DB_ENABLED:
61
  # Mock mode for testing without database
62
  return AuthResponse(
63
  user_id="mock_user_id",
 
66
  token_type="bearer"
67
  )
68
 
69
+ # Get database session
70
+ from database.db import SessionLocal
71
+ db = SessionLocal()
72
+
73
+ try:
74
+ # Validate password strength
75
+ if len(request.password) < 8:
76
+ raise HTTPException(status_code=400, detail="Password must be at least 8 characters long")
77
+
78
+ # Validate background fields
79
+ if request.software_background and len(request.software_background) < 10:
80
+ raise HTTPException(status_code=400, detail="Software background must be at least 10 characters")
81
+ if request.hardware_background and len(request.hardware_background) < 10:
82
+ raise HTTPException(status_code=400, detail="Hardware background must be at least 10 characters")
83
+
84
+ # Hash the password
85
+ hashed_password = hash_password(request.password)
86
+
87
+ # Create new user in database
88
+ db_user = DBUser(
89
+ email=request.email,
90
+ password_hash=hashed_password,
91
+ software_background=request.software_background,
92
+ hardware_background=request.hardware_background,
93
+ experience_level=request.experience_level or "Intermediate"
94
+ )
95
+
96
+ db.add(db_user)
97
+ db.commit()
98
+ db.refresh(db_user)
99
+
100
+ # Generate JWT token
101
+ access_token = create_access_token(data={
102
+ "sub": str(db_user.id),
103
+ "email": db_user.email
104
+ })
105
+
106
+ return AuthResponse(
107
+ user_id=str(db_user.id),
108
+ email=db_user.email,
109
+ access_token=access_token,
110
+ token_type="bearer"
111
+ )
112
+
113
+ except IntegrityError:
114
+ db.rollback()
115
+ raise HTTPException(status_code=400, detail="Email already registered")
116
+ finally:
117
+ db.close()
118
+
119
+ except HTTPException:
120
+ raise
121
  except Exception as e:
122
  logger.error(f"Error during signup: {str(e)}")
 
 
123
  raise HTTPException(status_code=500, detail=f"Error during signup: {str(e)}")
124
 
125
+
126
  @router.post("/auth/login", response_model=AuthResponse)
127
+ async def login(request: LoginRequest):
128
  """Handle user login"""
129
  try:
130
+ if not DB_ENABLED:
131
  # Mock mode for testing without database
132
  return AuthResponse(
133
  user_id="mock_user_id",
 
136
  token_type="bearer"
137
  )
138
 
139
+ # Get database session
140
+ from database.db import SessionLocal
141
+ db = SessionLocal()
 
142
 
143
+ try:
144
+ # Find user by email
145
+ user = db.query(DBUser).filter(DBUser.email == request.email).first()
146
+ if not user:
147
+ raise HTTPException(status_code=401, detail="Invalid credentials")
148
 
149
+ # Verify password
150
+ if not verify_password(request.password, user.password_hash):
151
+ raise HTTPException(status_code=401, detail="Invalid credentials")
 
 
152
 
153
+ # Generate JWT token
154
+ access_token = create_access_token(data={
155
+ "sub": str(user.id),
156
+ "email": user.email
157
+ })
158
+
159
+ return AuthResponse(
160
+ user_id=str(user.id),
161
+ email=user.email,
162
+ access_token=access_token,
163
+ token_type="bearer"
164
+ )
165
+
166
+ finally:
167
+ db.close()
168
 
169
  except HTTPException:
170
  raise
 
172
  logger.error(f"Error during login: {str(e)}")
173
  raise HTTPException(status_code=500, detail="Error during login")
174
 
175
+
176
  @router.get("/auth/profile", response_model=UserProfileResponse)
177
  async def get_profile(
178
  authorization: Optional[str] = Header(None),
179
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
 
180
  ):
181
  """Get user profile information"""
182
  try:
183
+ if not DB_ENABLED:
184
  # Mock mode for testing without database
185
  return UserProfileResponse(
186
  user_id="mock_user_id",
 
192
 
193
  # Get token from Authorization header or credentials
194
  token = None
195
+ if credentials:
 
 
196
  token = credentials.credentials
197
+ elif authorization and authorization.startswith("Bearer "):
198
+ token = authorization.replace("Bearer ", "")
199
 
200
  if not token:
201
  raise HTTPException(status_code=401, detail="Authorization token required")
 
205
  if not user_id:
206
  raise HTTPException(status_code=401, detail="Invalid or expired token")
207
 
208
+ # Get database session
209
+ from database.db import SessionLocal
210
+ db = SessionLocal()
 
211
 
212
+ try:
213
+ # Get user from database
214
+ user = db.query(DBUser).filter(DBUser.id == user_id).first()
215
+ if not user:
216
+ raise HTTPException(status_code=404, detail="User not found")
217
+
218
+ return UserProfileResponse(
219
+ user_id=str(user.id),
220
+ email=user.email,
221
+ software_background=user.software_background,
222
+ hardware_background=user.hardware_background,
223
+ experience_level=user.experience_level
224
+ )
225
+ finally:
226
+ db.close()
227
 
228
  except HTTPException:
229
  raise
 
231
  logger.error(f"Error retrieving profile: {str(e)}")
232
  raise HTTPException(status_code=500, detail="Error retrieving profile")
233
 
234
+
235
  @router.get("/auth/health")
236
  async def auth_health():
237
  """Health check for auth service"""
api/translation.py CHANGED
@@ -1,30 +1,38 @@
1
- from fastapi import APIRouter, HTTPException, Depends, Header
2
  from pydantic import BaseModel
3
  from typing import Optional
4
- from sqlalchemy.orm import Session
5
- from sqlalchemy.exc import IntegrityError
6
  import hashlib
7
  import logging
8
  import os
9
 
 
 
 
10
  # Import dependencies
11
  try:
12
  from services.translation_service import TranslationService
13
  from services.rate_limiter import RateLimiter
14
- from database.db import get_db
15
- from database.models import Translation
16
  from auth.jwt_utils import get_current_user_id_from_token
 
 
 
 
 
 
 
 
17
  DB_ENABLED = True
18
  except ImportError as e:
19
  DB_ENABLED = False
20
- logging.warning(f"Database or dependencies not available: {str(e)}")
21
-
22
- logger = logging.getLogger(__name__)
23
- router = APIRouter()
24
 
25
  # Initialize services
26
- translation_service = TranslationService()
27
- rate_limiter = RateLimiter(max_requests=10, window_seconds=3600) # 10 translations/hour
 
 
 
 
28
 
29
  # Cost and performance metrics (in-memory)
30
  translation_metrics = {
@@ -42,9 +50,9 @@ def log_metrics():
42
  if total > 0:
43
  cache_hit_rate = (translation_metrics["cache_hits"] / total) * 100
44
  avg_latency = translation_metrics["total_latency_ms"] / total
45
- cost_savings = (translation_metrics["cache_hits"] / total) * 100 # % of API calls saved
46
  logger.info(
47
- f"πŸ“Š Translation Metrics: "
48
  f"Total={total}, Cache Hit Rate={cache_hit_rate:.1f}%, "
49
  f"API Calls={translation_metrics['api_calls']}, "
50
  f"Avg Latency={avg_latency:.0f}ms, "
@@ -75,26 +83,19 @@ class TranslationResponse(BaseModel):
75
  source_lang: str
76
  target_lang: str
77
 
 
78
  @router.post("/translate/urdu", response_model=UrduTranslationResponse)
79
  async def translate_to_urdu(
80
  request: UrduTranslationRequest,
81
- authorization: Optional[str] = Header(None),
82
- db: Session = Depends(get_db) if DB_ENABLED else None
83
  ):
84
  """
85
  Translate chapter content to Urdu with JWT authentication, caching, and rate limiting
86
-
87
- **Requirements**:
88
- - JWT token in Authorization header
89
- - Content hash must match SHA-256 of content
90
- - Rate limit: 10 translations per user per hour
91
-
92
- **Returns**:
93
- - translated_content: Urdu translation
94
- - cached: Whether result came from cache
95
- - translation_id: UUID of translation record
96
  """
97
  try:
 
 
 
98
  # 1. Verify JWT authentication
99
  if not authorization or not authorization.startswith("Bearer "):
100
  raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
@@ -130,49 +131,56 @@ async def translate_to_urdu(
130
  )
131
 
132
  # 4. Check database cache (if enabled)
133
- if DB_ENABLED and db:
134
- from database.models import Translation as DBTranslation
135
-
136
- cached_translation = db.query(DBTranslation).filter(
137
- DBTranslation.chapter_id == request.chapter_id,
138
- DBTranslation.content_hash == request.content_hash,
139
- DBTranslation.target_language == "urdu"
140
- ).first()
141
-
142
- if cached_translation:
143
- latency_ms = (time.time() - start_time) * 1000
144
- translation_metrics["cache_hits"] += 1
145
- translation_metrics["total_latency_ms"] += latency_ms
146
- logger.info(f"βœ… Cache HIT for chapter {request.chapter_id} | Latency: {latency_ms:.0f}ms")
147
- log_metrics() # Log aggregated metrics every 10 requests
148
- return UrduTranslationResponse(
149
- translated_content=cached_translation.translated_content,
150
- cached=True,
151
- translation_id=str(cached_translation.id)
152
- )
 
 
 
 
 
 
 
153
 
154
  # 5. Cache MISS - Translate using OpenRouter
155
  translation_metrics["cache_misses"] += 1
156
  translation_metrics["api_calls"] += 1
157
- logger.info(f"❌ Cache MISS for chapter {request.chapter_id} - calling OpenRouter API")
158
 
159
  api_start_time = time.time()
160
  try:
161
  translated_text = translation_service.translate_to_urdu(request.content)
162
  api_latency_ms = (time.time() - api_start_time) * 1000
163
- logger.info(f"🌐 OpenRouter API call successful | API Latency: {api_latency_ms:.0f}ms")
164
  except Exception as api_error:
165
  translation_metrics["api_failures"] += 1
166
- logger.error(f"❌ OpenRouter API error: {str(api_error)}")
167
  # Retry once with exponential backoff
168
  time.sleep(2)
169
  try:
170
  translated_text = translation_service.translate_to_urdu(request.content)
171
  api_latency_ms = (time.time() - api_start_time) * 1000
172
- logger.info(f"βœ… Retry successful after {api_latency_ms:.0f}ms")
173
  except Exception as retry_error:
174
  translation_metrics["api_failures"] += 1
175
- logger.error(f"❌ Retry failed: {str(retry_error)}")
176
  raise HTTPException(
177
  status_code=503,
178
  detail="Translation service temporarily unavailable. Please try again."
@@ -180,12 +188,12 @@ async def translate_to_urdu(
180
 
181
  # 6. Save to database (if enabled)
182
  translation_id = None
183
- if DB_ENABLED and db:
184
  try:
185
- from database.models import Translation as DBTranslation
186
  import uuid
 
187
 
188
- db_translation = DBTranslation(
189
  id=uuid.uuid4(),
190
  chapter_id=request.chapter_id,
191
  content_hash=request.content_hash,
@@ -203,22 +211,18 @@ async def translate_to_urdu(
203
  translation_id = str(db_translation.id)
204
  logger.info(f"Saved translation to database: {translation_id}")
205
 
206
- except IntegrityError as e:
207
- logger.warning(f"Duplicate translation detected (race condition): {str(e)}")
208
- db.rollback()
209
- # Query again to get existing translation
210
- cached_translation = db.query(DBTranslation).filter(
211
- DBTranslation.chapter_id == request.chapter_id,
212
- DBTranslation.content_hash == request.content_hash,
213
- DBTranslation.target_language == "urdu"
214
- ).first()
215
- if cached_translation:
216
- translation_id = str(cached_translation.id)
217
 
218
  # Log final metrics
219
  total_latency_ms = (time.time() - start_time) * 1000
220
  translation_metrics["total_latency_ms"] += total_latency_ms
221
- logger.info(f"πŸ’Ύ Translation complete | Total Latency: {total_latency_ms:.0f}ms | Cached: False")
222
  log_metrics()
223
 
224
  return UrduTranslationResponse(
@@ -239,9 +243,11 @@ async def translate_to_urdu(
239
  async def translate_text(request: TranslationRequest):
240
  """
241
  Legacy translation endpoint - translate text between languages
242
- (Kept for backward compatibility with existing frontend)
243
  """
244
  try:
 
 
 
245
  if request.source_lang == "en" and request.target_lang == "ur":
246
  translated_text = translation_service.translate_to_urdu(request.text)
247
  elif request.source_lang == "ur" and request.target_lang == "en":
@@ -259,6 +265,8 @@ async def translate_text(request: TranslationRequest):
259
  target_lang=request.target_lang
260
  )
261
 
 
 
262
  except Exception as e:
263
  logger.error(f"Translation error: {str(e)}")
264
  raise HTTPException(status_code=500, detail=f"Error during translation: {str(e)}")
@@ -269,8 +277,9 @@ async def translation_health():
269
  """Health check for translation service"""
270
  return {
271
  "status": "translation service is running",
 
272
  "database_enabled": DB_ENABLED,
273
- "rate_limiter_enabled": True
274
  }
275
 
276
 
@@ -280,6 +289,9 @@ async def translation_stats(
280
  ):
281
  """Get translation statistics for current user"""
282
  try:
 
 
 
283
  if not authorization or not authorization.startswith("Bearer "):
284
  raise HTTPException(status_code=401, detail="Missing authorization header")
285
 
 
1
+ from fastapi import APIRouter, HTTPException, Header
2
  from pydantic import BaseModel
3
  from typing import Optional
 
 
4
  import hashlib
5
  import logging
6
  import os
7
 
8
+ logger = logging.getLogger(__name__)
9
+ router = APIRouter()
10
+
11
  # Import dependencies
12
  try:
13
  from services.translation_service import TranslationService
14
  from services.rate_limiter import RateLimiter
 
 
15
  from auth.jwt_utils import get_current_user_id_from_token
16
+ SERVICES_ENABLED = True
17
+ except ImportError as e:
18
+ SERVICES_ENABLED = False
19
+ logging.warning(f"Services not available: {str(e)}")
20
+
21
+ try:
22
+ from database.db import SessionLocal
23
+ from database.models import Translation
24
  DB_ENABLED = True
25
  except ImportError as e:
26
  DB_ENABLED = False
27
+ logging.warning(f"Database not available: {str(e)}")
 
 
 
28
 
29
  # Initialize services
30
+ if SERVICES_ENABLED:
31
+ translation_service = TranslationService()
32
+ rate_limiter = RateLimiter(max_requests=10, window_seconds=3600)
33
+ else:
34
+ translation_service = None
35
+ rate_limiter = None
36
 
37
  # Cost and performance metrics (in-memory)
38
  translation_metrics = {
 
50
  if total > 0:
51
  cache_hit_rate = (translation_metrics["cache_hits"] / total) * 100
52
  avg_latency = translation_metrics["total_latency_ms"] / total
53
+ cost_savings = (translation_metrics["cache_hits"] / total) * 100
54
  logger.info(
55
+ f"Translation Metrics: "
56
  f"Total={total}, Cache Hit Rate={cache_hit_rate:.1f}%, "
57
  f"API Calls={translation_metrics['api_calls']}, "
58
  f"Avg Latency={avg_latency:.0f}ms, "
 
83
  source_lang: str
84
  target_lang: str
85
 
86
+
87
  @router.post("/translate/urdu", response_model=UrduTranslationResponse)
88
  async def translate_to_urdu(
89
  request: UrduTranslationRequest,
90
+ authorization: Optional[str] = Header(None)
 
91
  ):
92
  """
93
  Translate chapter content to Urdu with JWT authentication, caching, and rate limiting
 
 
 
 
 
 
 
 
 
 
94
  """
95
  try:
96
+ if not SERVICES_ENABLED:
97
+ raise HTTPException(status_code=503, detail="Translation service not available")
98
+
99
  # 1. Verify JWT authentication
100
  if not authorization or not authorization.startswith("Bearer "):
101
  raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
 
131
  )
132
 
133
  # 4. Check database cache (if enabled)
134
+ db = None
135
+ if DB_ENABLED:
136
+ try:
137
+ db = SessionLocal()
138
+ cached_translation = db.query(Translation).filter(
139
+ Translation.chapter_id == request.chapter_id,
140
+ Translation.content_hash == request.content_hash,
141
+ Translation.target_language == "urdu"
142
+ ).first()
143
+
144
+ if cached_translation:
145
+ latency_ms = (time.time() - start_time) * 1000
146
+ translation_metrics["cache_hits"] += 1
147
+ translation_metrics["total_latency_ms"] += latency_ms
148
+ logger.info(f"Cache HIT for chapter {request.chapter_id} | Latency: {latency_ms:.0f}ms")
149
+ log_metrics()
150
+ return UrduTranslationResponse(
151
+ translated_content=cached_translation.translated_content,
152
+ cached=True,
153
+ translation_id=str(cached_translation.id)
154
+ )
155
+ except Exception as db_err:
156
+ logger.warning(f"Database cache check failed: {str(db_err)}")
157
+ finally:
158
+ if db:
159
+ db.close()
160
+ db = None
161
 
162
  # 5. Cache MISS - Translate using OpenRouter
163
  translation_metrics["cache_misses"] += 1
164
  translation_metrics["api_calls"] += 1
165
+ logger.info(f"Cache MISS for chapter {request.chapter_id} - calling OpenRouter API")
166
 
167
  api_start_time = time.time()
168
  try:
169
  translated_text = translation_service.translate_to_urdu(request.content)
170
  api_latency_ms = (time.time() - api_start_time) * 1000
171
+ logger.info(f"OpenRouter API call successful | API Latency: {api_latency_ms:.0f}ms")
172
  except Exception as api_error:
173
  translation_metrics["api_failures"] += 1
174
+ logger.error(f"OpenRouter API error: {str(api_error)}")
175
  # Retry once with exponential backoff
176
  time.sleep(2)
177
  try:
178
  translated_text = translation_service.translate_to_urdu(request.content)
179
  api_latency_ms = (time.time() - api_start_time) * 1000
180
+ logger.info(f"Retry successful after {api_latency_ms:.0f}ms")
181
  except Exception as retry_error:
182
  translation_metrics["api_failures"] += 1
183
+ logger.error(f"Retry failed: {str(retry_error)}")
184
  raise HTTPException(
185
  status_code=503,
186
  detail="Translation service temporarily unavailable. Please try again."
 
188
 
189
  # 6. Save to database (if enabled)
190
  translation_id = None
191
+ if DB_ENABLED:
192
  try:
 
193
  import uuid
194
+ db = SessionLocal()
195
 
196
+ db_translation = Translation(
197
  id=uuid.uuid4(),
198
  chapter_id=request.chapter_id,
199
  content_hash=request.content_hash,
 
211
  translation_id = str(db_translation.id)
212
  logger.info(f"Saved translation to database: {translation_id}")
213
 
214
+ except Exception as save_err:
215
+ logger.warning(f"Failed to save translation: {str(save_err)}")
216
+ if db:
217
+ db.rollback()
218
+ finally:
219
+ if db:
220
+ db.close()
 
 
 
 
221
 
222
  # Log final metrics
223
  total_latency_ms = (time.time() - start_time) * 1000
224
  translation_metrics["total_latency_ms"] += total_latency_ms
225
+ logger.info(f"Translation complete | Total Latency: {total_latency_ms:.0f}ms | Cached: False")
226
  log_metrics()
227
 
228
  return UrduTranslationResponse(
 
243
  async def translate_text(request: TranslationRequest):
244
  """
245
  Legacy translation endpoint - translate text between languages
 
246
  """
247
  try:
248
+ if not SERVICES_ENABLED:
249
+ raise HTTPException(status_code=503, detail="Translation service not available")
250
+
251
  if request.source_lang == "en" and request.target_lang == "ur":
252
  translated_text = translation_service.translate_to_urdu(request.text)
253
  elif request.source_lang == "ur" and request.target_lang == "en":
 
265
  target_lang=request.target_lang
266
  )
267
 
268
+ except HTTPException:
269
+ raise
270
  except Exception as e:
271
  logger.error(f"Translation error: {str(e)}")
272
  raise HTTPException(status_code=500, detail=f"Error during translation: {str(e)}")
 
277
  """Health check for translation service"""
278
  return {
279
  "status": "translation service is running",
280
+ "services_enabled": SERVICES_ENABLED,
281
  "database_enabled": DB_ENABLED,
282
+ "rate_limiter_enabled": rate_limiter is not None
283
  }
284
 
285
 
 
289
  ):
290
  """Get translation statistics for current user"""
291
  try:
292
+ if not SERVICES_ENABLED:
293
+ raise HTTPException(status_code=503, detail="Service not available")
294
+
295
  if not authorization or not authorization.startswith("Bearer "):
296
  raise HTTPException(status_code=401, detail="Missing authorization header")
297
 
requirements.txt CHANGED
@@ -1,18 +1,15 @@
1
- fastapi>=0.104.1
2
- uvicorn[standard]>=0.24.0
3
- sqlalchemy[asyncio]>=2.0.23
4
- asyncpg>=0.29.0
5
- qdrant-client>=1.7.1
6
- google-generativeai>=0.4.0
7
- python-multipart>=0.0.6
8
- python-jose[cryptography]>=3.3.0
9
- passlib[bcrypt]>=1.7.4
10
- better-exceptions>=0.3.3
11
- python-dotenv>=1.0.0
12
- pydantic>=2.5.0
13
- pydantic-settings>=2.1.0
14
- httpx>=0.25.2
15
- pytest>=7.4.3
16
- pytest-asyncio>=0.21.1
17
- alembic>=1.13.1
18
- openai>=1.10.0
 
1
+ ο»Ώfastapi==0.109.0
2
+ uvicorn==0.27.0
3
+ python-dotenv==1.0.0
4
+ openai==1.12.0
5
+ qdrant-client==1.7.0
6
+ pydantic==2.5.3
7
+ pydantic[email]
8
+ python-multipart==0.0.6
9
+ python-jose[cryptography]==3.3.0
10
+ passlib[bcrypt]==1.7.4
11
+ sqlalchemy==2.0.25
12
+ asyncpg==0.29.0
13
+ httpx==0.26.0
14
+ alembic==1.13.1
15
+ psycopg2-binary