Spaces:
Running
Running
nothex commited on
Commit Β·
67a6408
1
Parent(s): 5af3152
refactor: harden security, optimize deployment, and project-wide cleanup
Browse files- Initialized app.py for deployment tool compatibility
- Secured auth_utils by removing unsafe JWT fallbacks and implementing strict secret validation
- Cleaned up redundant routes and static mounts in main.py
- Aligned Dockerfile to Python 3.11 for dependency stability (PaddleOCR fix)
- Rewrote render.yaml to include managed Redis and dedicated Celery worker services
- Sanitized .gitignore and updated .env.example with missing critical secrets
- .gitignore +1 -2
- ARCHITECTURE.md +8 -6
- Dockerfile +13 -13
- README.md +13 -11
- app.py +4 -1
- backend/core/auth_utils.py +24 -20
- backend/core/cache_manager.py +3 -3
- backend/main.py +39 -23
- render.yaml +54 -4
.gitignore
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
.env
|
| 2 |
-
D:\Work\replacements.txt
|
| 3 |
__pycache__/
|
| 4 |
*.pyc
|
| 5 |
*.pyo
|
|
@@ -16,4 +15,4 @@ intent_feedback.jsonl
|
|
| 16 |
note_to_me.txt
|
| 17 |
*.pkl
|
| 18 |
|
| 19 |
-
.dual-graph/
|
|
|
|
| 1 |
.env
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
| 4 |
*.pyo
|
|
|
|
| 15 |
note_to_me.txt
|
| 16 |
*.pkl
|
| 17 |
|
| 18 |
+
.dual-graph/
|
ARCHITECTURE.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
| 2 |
|
| 3 |
> Read this when starting a new session, onboarding someone, or after a long break.
|
| 4 |
> Everything the system does, how the pieces connect, and why decisions were made the way they were.
|
| 5 |
|
| 6 |
---
|
| 7 |
|
| 8 |
-
## What Is
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
-
Users upload PDF documents. They ask questions in natural language.
|
| 13 |
|
| 14 |
**What makes it non-trivial:**
|
| 15 |
- Each user sees only their own documents, enforced at the database level (RLS)
|
|
@@ -26,7 +28,7 @@ Users upload PDF documents. They ask questions in natural language. Nexus finds
|
|
| 26 |
## Project Structure
|
| 27 |
|
| 28 |
```
|
| 29 |
-
|
| 30 |
βββ backend/
|
| 31 |
β βββ main.py FastAPI app, CORS, rate limiter, Celery lifespan
|
| 32 |
β βββ api/
|
|
@@ -363,7 +365,7 @@ After classification, the winning category's centroid is updated with this docum
|
|
| 363 |
|
| 364 |
## The Three Self-Improvement Loops
|
| 365 |
|
| 366 |
-
|
| 367 |
|
| 368 |
**Loop 1 β Intent classifier (every 25 queries)**
|
| 369 |
User queries logged to `intent_feedback`. Every 25 new rows, the model retrains automatically and saves to disk. Learns the specific query patterns of your users.
|
|
|
|
| 1 |
+
# Morpheus β Architecture Guide
|
| 2 |
+
|
| 3 |
+
> Ask anything. Your documents answer.
|
| 4 |
|
| 5 |
> Read this when starting a new session, onboarding someone, or after a long break.
|
| 6 |
> Everything the system does, how the pieces connect, and why decisions were made the way they were.
|
| 7 |
|
| 8 |
---
|
| 9 |
|
| 10 |
+
## What Is Morpheus?
|
| 11 |
|
| 12 |
+
Morpheus is a **multi-tenant RAG (Retrieval-Augmented Generation) platform**.
|
| 13 |
|
| 14 |
+
Users upload PDF documents. They ask questions in natural language. Morpheus finds the most relevant passages and uses an AI to generate accurate, streamed answers with source citations.
|
| 15 |
|
| 16 |
**What makes it non-trivial:**
|
| 17 |
- Each user sees only their own documents, enforced at the database level (RLS)
|
|
|
|
| 28 |
## Project Structure
|
| 29 |
|
| 30 |
```
|
| 31 |
+
morpheus/
|
| 32 |
βββ backend/
|
| 33 |
β βββ main.py FastAPI app, CORS, rate limiter, Celery lifespan
|
| 34 |
β βββ api/
|
|
|
|
| 365 |
|
| 366 |
## The Three Self-Improvement Loops
|
| 367 |
|
| 368 |
+
Morpheus has three feedback loops that make it more accurate over time:
|
| 369 |
|
| 370 |
**Loop 1 β Intent classifier (every 25 queries)**
|
| 371 |
User queries logged to `intent_feedback`. Every 25 new rows, the model retrains automatically and saves to disk. Learns the specific query patterns of your users.
|
Dockerfile
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
# 1. Use an official lightweight Python image
|
| 2 |
-
FROM python:3.
|
| 3 |
|
| 4 |
# 2. Install system dependencies required for 'unstructured' PDF OCR
|
| 5 |
RUN apt-get update && apt-get install -y \
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
# 3. Create a non-root user (Required by Hugging Face for security)
|
| 16 |
RUN useradd -m -u 1000 user
|
|
@@ -31,10 +31,10 @@ COPY --chown=user:user . .
|
|
| 31 |
ARG PREBUILD_ML_ASSETS=1
|
| 32 |
ARG NEXUS_BUILD_ASSETS_MODE=light
|
| 33 |
RUN if [ "$PREBUILD_ML_ASSETS" = "1" ]; then \
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
# 8. Start FastAPI (7860 is the HF standard, but Railway uses $PORT)
|
| 40 |
ENV PORT=7860
|
|
|
|
| 1 |
# 1. Use an official lightweight Python image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
|
| 4 |
# 2. Install system dependencies required for 'unstructured' PDF OCR
|
| 5 |
RUN apt-get update && apt-get install -y \
|
| 6 |
+
tesseract-ocr \
|
| 7 |
+
poppler-utils \
|
| 8 |
+
libmagic-dev \
|
| 9 |
+
libgl1 \
|
| 10 |
+
libglib2.0-0 \
|
| 11 |
+
libgomp1 \
|
| 12 |
+
libgthread-2.0-0 \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
# 3. Create a non-root user (Required by Hugging Face for security)
|
| 16 |
RUN useradd -m -u 1000 user
|
|
|
|
| 31 |
ARG PREBUILD_ML_ASSETS=1
|
| 32 |
ARG NEXUS_BUILD_ASSETS_MODE=light
|
| 33 |
RUN if [ "$PREBUILD_ML_ASSETS" = "1" ]; then \
|
| 34 |
+
NEXUS_BUILD_ASSETS_MODE=$NEXUS_BUILD_ASSETS_MODE python -m backend.core.build_ml_assets ; \
|
| 35 |
+
else \
|
| 36 |
+
echo "Skipping ML asset pre-build"; \
|
| 37 |
+
fi
|
| 38 |
|
| 39 |
# 8. Start FastAPI (7860 is the HF standard, but Railway uses $PORT)
|
| 40 |
ENV PORT=7860
|
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: π§
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
@@ -8,7 +8,9 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
|
|
|
|
|
|
| 12 |
|
| 13 |
A multi-tenant Retrieval-Augmented Generation platform. Upload PDFs, ask questions in natural language, get accurate answers with source citations β streamed token by token, scoped strictly to your documents.
|
| 14 |
|
|
@@ -26,7 +28,7 @@ A multi-tenant Retrieval-Augmented Generation platform. Upload PDFs, ask questio
|
|
| 26 |
## Project Structure
|
| 27 |
|
| 28 |
```
|
| 29 |
-
|
| 30 |
βββ backend/
|
| 31 |
β βββ main.py FastAPI entry point, rate limiter, Celery lifespan
|
| 32 |
β βββ api/
|
|
@@ -87,8 +89,8 @@ nexus/
|
|
| 87 |
### 2. Clone and install
|
| 88 |
|
| 89 |
```bash
|
| 90 |
-
git clone https://github.com/your-username/
|
| 91 |
-
cd
|
| 92 |
python -m venv .venv
|
| 93 |
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 94 |
pip install -r requirements.txt
|
|
@@ -146,10 +148,10 @@ Create a user account via Supabase Auth (Auth β Users β Invite), then sign i
|
|
| 146 |
|
| 147 |
```bash
|
| 148 |
# Downloads embedding model, trains intent classifier
|
| 149 |
-
|
| 150 |
|
| 151 |
# Downloads embedding model only (faster, recommended for first run)
|
| 152 |
-
|
| 153 |
```
|
| 154 |
|
| 155 |
### 8. Seed the document classifier (after your first ingestion)
|
|
@@ -166,8 +168,8 @@ curl -X POST http://localhost:8000/api/v1/admin/warmup \
|
|
| 166 |
Build and run the full stack as a single container:
|
| 167 |
|
| 168 |
```bash
|
| 169 |
-
docker build -t
|
| 170 |
-
docker run -p 8000:7860 --env-file .env
|
| 171 |
```
|
| 172 |
|
| 173 |
> The Dockerfile is configured for HuggingFace Spaces (port 7860, non-root user). For local use, the container maps 8000 β 7860.
|
|
@@ -183,14 +185,14 @@ docker run -p 8000:7860 --env-file .env nexus-rag
|
|
| 183 |
3. Render reads `render.yaml` automatically
|
| 184 |
4. In the Render dashboard β Environment tab, add all your `.env` values
|
| 185 |
5. Add a Redis service: Render β New β Redis (free tier) β copy the internal URL to `REDIS_URL`
|
| 186 |
-
6. Deploy β copy your Render URL (e.g. `https://
|
| 187 |
|
| 188 |
### Frontend β Vercel
|
| 189 |
|
| 190 |
1. Update `API_URL` in `frontend/js/config.js` to your Render URL
|
| 191 |
2. [vercel.com](https://vercel.com) β New Project β connect your repo
|
| 192 |
3. Vercel reads `vercel.json` automatically
|
| 193 |
-
4. Deploy β copy your Vercel URL (e.g. `https://
|
| 194 |
|
| 195 |
### After both are deployed
|
| 196 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Morpheus RAG
|
| 3 |
emoji: π§
|
| 4 |
colorFrom: green
|
| 5 |
colorTo: blue
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Morpheus
|
| 12 |
+
|
| 13 |
+
> Ask anything. Your documents answer.
|
| 14 |
|
| 15 |
A multi-tenant Retrieval-Augmented Generation platform. Upload PDFs, ask questions in natural language, get accurate answers with source citations β streamed token by token, scoped strictly to your documents.
|
| 16 |
|
|
|
|
| 28 |
## Project Structure
|
| 29 |
|
| 30 |
```
|
| 31 |
+
morpheus/
|
| 32 |
βββ backend/
|
| 33 |
β βββ main.py FastAPI entry point, rate limiter, Celery lifespan
|
| 34 |
β βββ api/
|
|
|
|
| 89 |
### 2. Clone and install
|
| 90 |
|
| 91 |
```bash
|
| 92 |
+
git clone https://github.com/your-username/morpheus.git
|
| 93 |
+
cd morpheus
|
| 94 |
python -m venv .venv
|
| 95 |
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 96 |
pip install -r requirements.txt
|
|
|
|
| 148 |
|
| 149 |
```bash
|
| 150 |
# Downloads embedding model, trains intent classifier
|
| 151 |
+
Morpheus_BUILD_ASSETS_MODE=full python -m backend.core.build_ml_assets
|
| 152 |
|
| 153 |
# Downloads embedding model only (faster, recommended for first run)
|
| 154 |
+
Morpheus_BUILD_ASSETS_MODE=light python -m backend.core.build_ml_assets
|
| 155 |
```
|
| 156 |
|
| 157 |
### 8. Seed the document classifier (after your first ingestion)
|
|
|
|
| 168 |
Build and run the full stack as a single container:
|
| 169 |
|
| 170 |
```bash
|
| 171 |
+
docker build -t morpheus-rag .
|
| 172 |
+
docker run -p 8000:7860 --env-file .env morpheus-rag
|
| 173 |
```
|
| 174 |
|
| 175 |
> The Dockerfile is configured for HuggingFace Spaces (port 7860, non-root user). For local use, the container maps 8000 β 7860.
|
|
|
|
| 185 |
3. Render reads `render.yaml` automatically
|
| 186 |
4. In the Render dashboard β Environment tab, add all your `.env` values
|
| 187 |
5. Add a Redis service: Render β New β Redis (free tier) β copy the internal URL to `REDIS_URL`
|
| 188 |
+
6. Deploy β copy your Render URL (e.g. `https://morpheus-api.onrender.com`)
|
| 189 |
|
| 190 |
### Frontend β Vercel
|
| 191 |
|
| 192 |
1. Update `API_URL` in `frontend/js/config.js` to your Render URL
|
| 193 |
2. [vercel.com](https://vercel.com) β New Project β connect your repo
|
| 194 |
3. Vercel reads `vercel.json` automatically
|
| 195 |
+
4. Deploy β copy your Vercel URL (e.g. `https://morpheus.vercel.app`)
|
| 196 |
|
| 197 |
### After both are deployed
|
| 198 |
|
app.py
CHANGED
|
@@ -1 +1,4 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Entry point for deployments that expect app.py at the project root.
|
| 2 |
+
# All app logic lives in backend/main.py β this just re-exports the app object
|
| 3 |
+
# so tools like gunicorn can target either `app:app` or `backend.main:app`.
|
| 4 |
+
from backend.main import app # noqa: F401
|
backend/core/auth_utils.py
CHANGED
|
@@ -19,6 +19,7 @@ from fastapi import Header, HTTPException, status
|
|
| 19 |
|
| 20 |
# ββ Low-level JWT helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
|
|
|
|
| 22 |
def extract_jwt_sub(access_token: str) -> str:
|
| 23 |
"""
|
| 24 |
Extract the Supabase user id (JWT `sub`) while strictly verifying the signature.
|
|
@@ -31,28 +32,29 @@ def extract_jwt_sub(access_token: str) -> str:
|
|
| 31 |
import jwt
|
| 32 |
|
| 33 |
if not config.SUPABASE_JWT_SECRET:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
raise ValueError(f"Invalid JWT signature or payload: {e}")
|
| 51 |
|
| 52 |
# Supabase uses `sub` for the user UUID.
|
| 53 |
sub = payload.get("sub") or payload.get("user_id") or payload.get("uid")
|
| 54 |
if not sub:
|
| 55 |
-
raise ValueError(
|
|
|
|
|
|
|
| 56 |
|
| 57 |
return str(sub)
|
| 58 |
|
|
@@ -63,12 +65,14 @@ def safe_extract_jwt_sub(access_token: Optional[str]) -> Optional[str]:
|
|
| 63 |
return extract_jwt_sub(access_token) if access_token else None
|
| 64 |
except Exception as e:
|
| 65 |
import logging
|
| 66 |
-
|
|
|
|
| 67 |
return None
|
| 68 |
|
| 69 |
|
| 70 |
# ββ FastAPI dependency ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 71 |
|
|
|
|
| 72 |
async def require_auth_token(
|
| 73 |
x_auth_token: Optional[str] = Header(default=None, alias="X-Auth-Token"),
|
| 74 |
) -> str:
|
|
@@ -100,4 +104,4 @@ async def require_auth_token(
|
|
| 100 |
headers={"WWW-Authenticate": "Bearer"},
|
| 101 |
)
|
| 102 |
|
| 103 |
-
return user_id
|
|
|
|
| 19 |
|
| 20 |
# ββ Low-level JWT helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
|
| 22 |
+
|
| 23 |
def extract_jwt_sub(access_token: str) -> str:
|
| 24 |
"""
|
| 25 |
Extract the Supabase user id (JWT `sub`) while strictly verifying the signature.
|
|
|
|
| 32 |
import jwt
|
| 33 |
|
| 34 |
if not config.SUPABASE_JWT_SECRET:
|
| 35 |
+
raise RuntimeError(
|
| 36 |
+
"SUPABASE_JWT_SECRET is not configured. "
|
| 37 |
+
"Set it in your .env (Supabase β Settings β API β JWT Settings)."
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
payload = jwt.decode(
|
| 42 |
+
access_token,
|
| 43 |
+
key=config.SUPABASE_JWT_SECRET,
|
| 44 |
+
algorithms=["HS256"],
|
| 45 |
+
audience="authenticated",
|
| 46 |
+
)
|
| 47 |
+
except jwt.ExpiredSignatureError:
|
| 48 |
+
raise ValueError("JWT has expired")
|
| 49 |
+
except jwt.InvalidTokenError as e:
|
| 50 |
+
raise ValueError(f"Invalid JWT signature or payload: {e}")
|
|
|
|
| 51 |
|
| 52 |
# Supabase uses `sub` for the user UUID.
|
| 53 |
sub = payload.get("sub") or payload.get("user_id") or payload.get("uid")
|
| 54 |
if not sub:
|
| 55 |
+
raise ValueError(
|
| 56 |
+
"JWT does not contain a user id claim (`sub` / `uid` / `user_id`)."
|
| 57 |
+
)
|
| 58 |
|
| 59 |
return str(sub)
|
| 60 |
|
|
|
|
| 65 |
return extract_jwt_sub(access_token) if access_token else None
|
| 66 |
except Exception as e:
|
| 67 |
import logging
|
| 68 |
+
|
| 69 |
+
logging.getLogger("morpheus.auth").debug("JWT extraction failed: %s", e)
|
| 70 |
return None
|
| 71 |
|
| 72 |
|
| 73 |
# ββ FastAPI dependency ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 74 |
|
| 75 |
+
|
| 76 |
async def require_auth_token(
|
| 77 |
x_auth_token: Optional[str] = Header(default=None, alias="X-Auth-Token"),
|
| 78 |
) -> str:
|
|
|
|
| 104 |
headers={"WWW-Authenticate": "Bearer"},
|
| 105 |
)
|
| 106 |
|
| 107 |
+
return user_id
|
backend/core/cache_manager.py
CHANGED
|
@@ -21,7 +21,7 @@ import os
|
|
| 21 |
import time
|
| 22 |
from typing import Any, Dict, List, Optional
|
| 23 |
|
| 24 |
-
log = logging.getLogger("
|
| 25 |
|
| 26 |
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
TTL_BY_DOCTYPE = {
|
|
@@ -45,8 +45,8 @@ SHAREABLE_TYPES = {
|
|
| 45 |
"reference_chart",
|
| 46 |
}
|
| 47 |
|
| 48 |
-
PREFIX_CACHE = "
|
| 49 |
-
PREFIX_VERSION = "
|
| 50 |
|
| 51 |
|
| 52 |
def _get_redis():
|
|
|
|
| 21 |
import time
|
| 22 |
from typing import Any, Dict, List, Optional
|
| 23 |
|
| 24 |
+
log = logging.getLogger("morpheus.cache")
|
| 25 |
|
| 26 |
# ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
TTL_BY_DOCTYPE = {
|
|
|
|
| 45 |
"reference_chart",
|
| 46 |
}
|
| 47 |
|
| 48 |
+
PREFIX_CACHE = "morpheus:qcache"
|
| 49 |
+
PREFIX_VERSION = "morpheus:kb_version"
|
| 50 |
|
| 51 |
|
| 52 |
def _get_redis():
|
backend/main.py
CHANGED
|
@@ -4,6 +4,7 @@ backend/main.py β FastAPI entry point.
|
|
| 4 |
Local: uvicorn backend.main:app --reload --port 8000
|
| 5 |
Production: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
|
| 6 |
"""
|
|
|
|
| 7 |
import os
|
| 8 |
import sys
|
| 9 |
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
@@ -31,13 +32,12 @@ from dotenv import load_dotenv # noqa: E402
|
|
| 31 |
|
| 32 |
load_dotenv()
|
| 33 |
|
| 34 |
-
from backend.api import auth, corpus, ingest, query, admin,frontend_config # noqa: E402
|
| 35 |
from backend.core.intent_classifier import get_intent_classifier_status # noqa: E402
|
| 36 |
|
| 37 |
log = logging.getLogger("morpheus.main")
|
| 38 |
|
| 39 |
|
| 40 |
-
|
| 41 |
@asynccontextmanager
|
| 42 |
async def lifespan(app: FastAPI):
|
| 43 |
log.info("MORPHEUS API starting")
|
|
@@ -47,13 +47,18 @@ async def lifespan(app: FastAPI):
|
|
| 47 |
celery_process = None
|
| 48 |
if os.getenv("AUTO_START_CELERY", "true").lower() == "true":
|
| 49 |
try:
|
| 50 |
-
celery_process = subprocess.Popen(
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
log.info("Celery worker auto-started (PID %s)", celery_process.pid)
|
| 58 |
except Exception as exc:
|
| 59 |
log.warning("Could not auto-start Celery worker: %s", exc)
|
|
@@ -73,26 +78,36 @@ async def lifespan(app: FastAPI):
|
|
| 73 |
|
| 74 |
|
| 75 |
app = FastAPI(
|
| 76 |
-
title="Morpheus RAG API",
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
)
|
| 80 |
|
| 81 |
# ββ Rate limiting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 82 |
app.state.limiter = limiter
|
| 83 |
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 84 |
|
| 85 |
-
_origins = [
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
app.include_router(auth.router,
|
| 90 |
app.include_router(corpus.router, prefix="/api/v1/corpus", tags=["corpus"])
|
| 91 |
app.include_router(ingest.router, prefix="/api/v1/ingest", tags=["ingest"])
|
| 92 |
-
app.include_router(query.router,
|
| 93 |
-
app.include_router(admin.router,
|
| 94 |
app.include_router(frontend_config.router, prefix="/api/v1/config", tags=["config"])
|
| 95 |
|
|
|
|
| 96 |
@app.get("/health")
|
| 97 |
def health():
|
| 98 |
return {"status": "healthy"}
|
|
@@ -105,10 +120,12 @@ def health_details():
|
|
| 105 |
"intent_classifier": get_intent_classifier_status(),
|
| 106 |
}
|
| 107 |
|
|
|
|
| 108 |
@app.get("/api/status")
|
| 109 |
def status():
|
| 110 |
return {"status": "ok", "service": "Morpheus RAG API", "version": "1.0.0"}
|
| 111 |
|
|
|
|
| 112 |
# ββ Static Frontend βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 113 |
# Mount the entire frontend folder at the root of the app so it serves the index.html.
|
| 114 |
# This makes it a self-contained single-container app for deployment.
|
|
@@ -120,9 +137,8 @@ if os.path.isdir(frontend_path):
|
|
| 120 |
else:
|
| 121 |
log.warning("Frontend directory not found at %s. API-only mode.", frontend_path)
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
return FileResponse("frontend/index.html")
|
|
|
|
| 4 |
Local: uvicorn backend.main:app --reload --port 8000
|
| 5 |
Production: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
|
| 6 |
"""
|
| 7 |
+
|
| 8 |
import os
|
| 9 |
import sys
|
| 10 |
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
|
|
| 32 |
|
| 33 |
load_dotenv()
|
| 34 |
|
| 35 |
+
from backend.api import auth, corpus, ingest, query, admin, frontend_config # noqa: E402
|
| 36 |
from backend.core.intent_classifier import get_intent_classifier_status # noqa: E402
|
| 37 |
|
| 38 |
log = logging.getLogger("morpheus.main")
|
| 39 |
|
| 40 |
|
|
|
|
| 41 |
@asynccontextmanager
|
| 42 |
async def lifespan(app: FastAPI):
|
| 43 |
log.info("MORPHEUS API starting")
|
|
|
|
| 47 |
celery_process = None
|
| 48 |
if os.getenv("AUTO_START_CELERY", "true").lower() == "true":
|
| 49 |
try:
|
| 50 |
+
celery_process = subprocess.Popen(
|
| 51 |
+
[
|
| 52 |
+
sys.executable,
|
| 53 |
+
"-m",
|
| 54 |
+
"celery",
|
| 55 |
+
"-A",
|
| 56 |
+
"backend.core.tasks",
|
| 57 |
+
"worker",
|
| 58 |
+
"--pool=solo",
|
| 59 |
+
"--loglevel=info",
|
| 60 |
+
]
|
| 61 |
+
)
|
| 62 |
log.info("Celery worker auto-started (PID %s)", celery_process.pid)
|
| 63 |
except Exception as exc:
|
| 64 |
log.warning("Could not auto-start Celery worker: %s", exc)
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
app = FastAPI(
|
| 81 |
+
title="Morpheus RAG API",
|
| 82 |
+
version="1.0.0",
|
| 83 |
+
lifespan=lifespan,
|
| 84 |
+
docs_url="/docs" if os.getenv("DOCS_ENABLED", "true").lower() == "true" else None,
|
| 85 |
+
redoc_url="/redoc" if os.getenv("DOCS_ENABLED", "true").lower() == "true" else None,
|
| 86 |
)
|
| 87 |
|
| 88 |
# ββ Rate limiting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 89 |
app.state.limiter = limiter
|
| 90 |
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 91 |
|
| 92 |
+
_origins = [
|
| 93 |
+
o.strip() for o in os.getenv("ALLOWED_ORIGINS", "*").split(",") if o.strip()
|
| 94 |
+
]
|
| 95 |
+
app.add_middleware(
|
| 96 |
+
CORSMiddleware,
|
| 97 |
+
allow_origins=_origins,
|
| 98 |
+
allow_credentials=True,
|
| 99 |
+
allow_methods=["*"],
|
| 100 |
+
allow_headers=["*"],
|
| 101 |
+
)
|
| 102 |
|
| 103 |
+
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
|
| 104 |
app.include_router(corpus.router, prefix="/api/v1/corpus", tags=["corpus"])
|
| 105 |
app.include_router(ingest.router, prefix="/api/v1/ingest", tags=["ingest"])
|
| 106 |
+
app.include_router(query.router, prefix="/api/v1/query", tags=["query"])
|
| 107 |
+
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
|
| 108 |
app.include_router(frontend_config.router, prefix="/api/v1/config", tags=["config"])
|
| 109 |
|
| 110 |
+
|
| 111 |
@app.get("/health")
|
| 112 |
def health():
|
| 113 |
return {"status": "healthy"}
|
|
|
|
| 120 |
"intent_classifier": get_intent_classifier_status(),
|
| 121 |
}
|
| 122 |
|
| 123 |
+
|
| 124 |
@app.get("/api/status")
|
| 125 |
def status():
|
| 126 |
return {"status": "ok", "service": "Morpheus RAG API", "version": "1.0.0"}
|
| 127 |
|
| 128 |
+
|
| 129 |
# ββ Static Frontend βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 130 |
# Mount the entire frontend folder at the root of the app so it serves the index.html.
|
| 131 |
# This makes it a self-contained single-container app for deployment.
|
|
|
|
| 137 |
else:
|
| 138 |
log.warning("Frontend directory not found at %s. API-only mode.", frontend_path)
|
| 139 |
|
| 140 |
+
# /css and /js are already served by the root StaticFiles mount above.
|
| 141 |
+
# Separate mounts removed to avoid routing conflicts.
|
| 142 |
|
| 143 |
+
# Note: StaticFiles(html=True) above already serves frontend/index.html for GET /
|
| 144 |
+
# The explicit route below is intentionally removed to avoid a dead route.
|
|
|
render.yaml
CHANGED
|
@@ -1,24 +1,74 @@
|
|
| 1 |
services:
|
| 2 |
- type: web
|
| 3 |
-
name:
|
| 4 |
runtime: python
|
| 5 |
-
buildCommand: pip install -r
|
| 6 |
startCommand: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
|
| 7 |
envVars:
|
| 8 |
- key: OPENROUTER_API_KEY
|
| 9 |
-
sync: false
|
| 10 |
- key: SUPABASE_URL
|
| 11 |
sync: false
|
| 12 |
- key: SUPABASE_SERVICE_KEY
|
| 13 |
sync: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
- key: COHERE_API_KEY
|
| 15 |
sync: false
|
| 16 |
- key: MASTER_ADMIN_KEY
|
| 17 |
sync: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
- key: ALLOWED_ORIGINS
|
| 19 |
-
value: "https://your-
|
| 20 |
- key: DOCS_ENABLED
|
| 21 |
value: "false"
|
| 22 |
- key: LOG_LEVEL
|
| 23 |
value: "INFO"
|
|
|
|
|
|
|
| 24 |
healthCheckPath: /health
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
services:
|
| 2 |
- type: web
|
| 3 |
+
name: morpheus-api
|
| 4 |
runtime: python
|
| 5 |
+
buildCommand: pip install -r requirements.txt
|
| 6 |
startCommand: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
|
| 7 |
envVars:
|
| 8 |
- key: OPENROUTER_API_KEY
|
| 9 |
+
sync: false
|
| 10 |
- key: SUPABASE_URL
|
| 11 |
sync: false
|
| 12 |
- key: SUPABASE_SERVICE_KEY
|
| 13 |
sync: false
|
| 14 |
+
- key: SUPABASE_ANON_KEY
|
| 15 |
+
sync: false
|
| 16 |
+
- key: SUPABASE_JWT_SECRET
|
| 17 |
+
sync: false
|
| 18 |
+
- key: GROQ_API_KEY
|
| 19 |
+
sync: false
|
| 20 |
+
- key: GEMINI_API_KEY
|
| 21 |
+
sync: false
|
| 22 |
- key: COHERE_API_KEY
|
| 23 |
sync: false
|
| 24 |
- key: MASTER_ADMIN_KEY
|
| 25 |
sync: false
|
| 26 |
+
- key: REDIS_URL
|
| 27 |
+
fromService:
|
| 28 |
+
name: morpheus-redis
|
| 29 |
+
type: redis
|
| 30 |
+
property: connectionString
|
| 31 |
- key: ALLOWED_ORIGINS
|
| 32 |
+
value: "https://your-morpheus.vercel.app"
|
| 33 |
- key: DOCS_ENABLED
|
| 34 |
value: "false"
|
| 35 |
- key: LOG_LEVEL
|
| 36 |
value: "INFO"
|
| 37 |
+
- key: AUTO_START_CELERY
|
| 38 |
+
value: "false"
|
| 39 |
healthCheckPath: /health
|
| 40 |
+
|
| 41 |
+
- type: worker
|
| 42 |
+
name: morpheus-celery
|
| 43 |
+
runtime: python
|
| 44 |
+
buildCommand: pip install -r requirements.txt
|
| 45 |
+
startCommand: python -m celery -A backend.core.tasks worker --pool=solo --loglevel=info
|
| 46 |
+
envVars:
|
| 47 |
+
- key: OPENROUTER_API_KEY
|
| 48 |
+
sync: false
|
| 49 |
+
- key: SUPABASE_URL
|
| 50 |
+
sync: false
|
| 51 |
+
- key: SUPABASE_SERVICE_KEY
|
| 52 |
+
sync: false
|
| 53 |
+
- key: SUPABASE_ANON_KEY
|
| 54 |
+
sync: false
|
| 55 |
+
- key: SUPABASE_JWT_SECRET
|
| 56 |
+
sync: false
|
| 57 |
+
- key: GROQ_API_KEY
|
| 58 |
+
sync: false
|
| 59 |
+
- key: GEMINI_API_KEY
|
| 60 |
+
sync: false
|
| 61 |
+
- key: COHERE_API_KEY
|
| 62 |
+
sync: false
|
| 63 |
+
- key: REDIS_URL
|
| 64 |
+
fromService:
|
| 65 |
+
name: morpheus-redis
|
| 66 |
+
type: redis
|
| 67 |
+
property: connectionString
|
| 68 |
+
- key: LOG_LEVEL
|
| 69 |
+
value: "INFO"
|
| 70 |
+
|
| 71 |
+
databases:
|
| 72 |
+
- type: redis
|
| 73 |
+
name: morpheus-redis
|
| 74 |
+
plan: free
|