Spaces:
Build error
Build error
Hetansh Waghela commited on
Commit ·
2d21c4c
1
Parent(s): 3ed7a65
Enhance Docker and SQLite support for Hugging Face Spaces
Browse files- Updated Dockerfile to create persistent data directories for SQLite and uploads.
- Modified README.md to provide deployment instructions for Hugging Face Spaces.
- Adjusted Alembic migration logic to skip for SQLite and use create_all() instead.
- Updated database initialization to support SQLite in HF Spaces.
- Refactored upload directory handling in services to use settings for compatibility with HF Spaces.
- Added dynamic CORS origin handling for Hugging Face Spaces.
- Implemented secure cookie settings for production environments.
- Dockerfile +16 -3
- README.md +26 -3
- backend/alembic/env.py +14 -4
- backend/app/db.py +20 -2
- backend/app/main.py +13 -0
- backend/app/routes/auth.py +10 -6
- backend/app/routes/reports.py +4 -3
- backend/app/services/document_processing.py +3 -1
- backend/app/services/enhanced_report_service.py +4 -2
- backend/app/services/profile_service.py +0 -1
- backend/app/services/report_service.py +2 -1
- backend/app/settings.py +21 -2
Dockerfile
CHANGED
|
@@ -39,9 +39,14 @@ RUN set -eux; \
|
|
| 39 |
libxext6 \
|
| 40 |
libgomp1 \
|
| 41 |
curl \
|
|
|
|
| 42 |
; \
|
| 43 |
rm -rf /var/lib/apt/lists/*
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
# Python deps
|
| 46 |
COPY backend/requirements.txt ./requirements.txt
|
| 47 |
RUN python -m pip install --no-cache-dir -r requirements.txt
|
|
@@ -51,12 +56,20 @@ COPY backend/ ./
|
|
| 51 |
|
| 52 |
# Frontend production build served by FastAPI
|
| 53 |
COPY --from=frontend-build /frontend/dist /app/frontend_dist
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
EXPOSE 7860
|
| 57 |
|
| 58 |
-
HEALTHCHECK --interval=30s --timeout=10s --start-period=
|
| 59 |
CMD curl -f http://localhost:7860/api || exit 1
|
| 60 |
|
| 61 |
-
CMD ["
|
| 62 |
|
|
|
|
| 39 |
libxext6 \
|
| 40 |
libgomp1 \
|
| 41 |
curl \
|
| 42 |
+
poppler-utils \
|
| 43 |
; \
|
| 44 |
rm -rf /var/lib/apt/lists/*
|
| 45 |
|
| 46 |
+
# Create persistent data directory for SQLite and uploads
|
| 47 |
+
# Hugging Face Spaces mounts /data as persistent storage
|
| 48 |
+
RUN mkdir -p /data /data/uploads /data/chroma_db && chmod -R 777 /data
|
| 49 |
+
|
| 50 |
# Python deps
|
| 51 |
COPY backend/requirements.txt ./requirements.txt
|
| 52 |
RUN python -m pip install --no-cache-dir -r requirements.txt
|
|
|
|
| 56 |
|
| 57 |
# Frontend production build served by FastAPI
|
| 58 |
COPY --from=frontend-build /frontend/dist /app/frontend_dist
|
| 59 |
+
|
| 60 |
+
# Environment variables for HF Spaces
|
| 61 |
+
ENV FRONTEND_DIST_DIR=/app/frontend_dist \
|
| 62 |
+
DATABASE_URL="sqlite+aiosqlite:////data/lumea.db" \
|
| 63 |
+
CHROMA_PERSIST_DIR="/data/chroma_db" \
|
| 64 |
+
UPLOAD_DIR="/data/uploads"
|
| 65 |
+
|
| 66 |
+
# Create a startup script that ensures directories exist (using sh for slim image)
|
| 67 |
+
RUN printf '#!/bin/sh\nmkdir -p /data /data/uploads /data/chroma_db 2>/dev/null || true\nexec uvicorn app.main:app --host 0.0.0.0 --port 7860\n' > /app/start.sh && chmod +x /app/start.sh
|
| 68 |
|
| 69 |
EXPOSE 7860
|
| 70 |
|
| 71 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
| 72 |
CMD curl -f http://localhost:7860/api || exit 1
|
| 73 |
|
| 74 |
+
CMD ["/app/start.sh"]
|
| 75 |
|
README.md
CHANGED
|
@@ -1043,7 +1043,28 @@ pytest tests/test_routes_medicines.py
|
|
| 1043 |
|
| 1044 |
## Deployment
|
| 1045 |
|
| 1046 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
|
| 1048 |
```bash
|
| 1049 |
docker-compose up -d
|
|
@@ -1056,11 +1077,13 @@ Includes:
|
|
| 1056 |
|
| 1057 |
### Production Considerations
|
| 1058 |
|
| 1059 |
-
1. **Database**:
|
|
|
|
|
|
|
| 1060 |
2. **Backend**: Deploy to Railway, Render, or AWS ECS
|
| 1061 |
3. **Frontend**: Deploy to Vercel, Netlify, or Cloudflare Pages
|
| 1062 |
4. **Secrets**: Use environment variables, never commit `.env`
|
| 1063 |
-
5. **HTTPS**: Enable secure cookies in production
|
| 1064 |
6. **CORS**: Update `FRONTEND_ORIGIN` for production domain
|
| 1065 |
|
| 1066 |
---
|
|
|
|
| 1043 |
|
| 1044 |
## Deployment
|
| 1045 |
|
| 1046 |
+
### Hugging Face Spaces (Recommended for Demo)
|
| 1047 |
+
|
| 1048 |
+
This app is optimized for Hugging Face Spaces with Docker SDK:
|
| 1049 |
+
|
| 1050 |
+
1. **Create a new Space** on [huggingface.co/new-space](https://huggingface.co/new-space)
|
| 1051 |
+
2. **Select Docker SDK** and set port to `7860`
|
| 1052 |
+
3. **Push this repository** to the Space
|
| 1053 |
+
4. **Configure Secrets** (Settings → Secrets):
|
| 1054 |
+
- `JWT_SECRET`: Strong random secret for auth tokens
|
| 1055 |
+
- `GROQ_API_KEY`: For AI features (get free key at [console.groq.com](https://console.groq.com))
|
| 1056 |
+
- `GEMINI_API_KEY`: Optional fallback for AI features
|
| 1057 |
+
- `GOOGLE_PLACES_API_KEY`: Optional for pharmacy locator
|
| 1058 |
+
|
| 1059 |
+
The app automatically:
|
| 1060 |
+
- Uses SQLite stored in `/data` (persistent storage)
|
| 1061 |
+
- Serves frontend from same FastAPI server (port 7860)
|
| 1062 |
+
- Configures HTTPS-compatible secure cookies
|
| 1063 |
+
- Creates database tables on first startup
|
| 1064 |
+
|
| 1065 |
+
**No PostgreSQL required** - SQLite works out of the box!
|
| 1066 |
+
|
| 1067 |
+
### Docker Compose (Local Development)
|
| 1068 |
|
| 1069 |
```bash
|
| 1070 |
docker-compose up -d
|
|
|
|
| 1077 |
|
| 1078 |
### Production Considerations
|
| 1079 |
|
| 1080 |
+
1. **Database**:
|
| 1081 |
+
- **HF Spaces**: SQLite (automatic, stored in /data)
|
| 1082 |
+
- **Self-hosted**: Use Neon, Supabase, or managed PostgreSQL
|
| 1083 |
2. **Backend**: Deploy to Railway, Render, or AWS ECS
|
| 1084 |
3. **Frontend**: Deploy to Vercel, Netlify, or Cloudflare Pages
|
| 1085 |
4. **Secrets**: Use environment variables, never commit `.env`
|
| 1086 |
+
5. **HTTPS**: Enable secure cookies in production (automatic on HF Spaces)
|
| 1087 |
6. **CORS**: Update `FRONTEND_ORIGIN` for production domain
|
| 1088 |
|
| 1089 |
---
|
backend/alembic/env.py
CHANGED
|
@@ -73,13 +73,23 @@ def run_migrations_online() -> None:
|
|
| 73 |
In this scenario we need to create an Engine
|
| 74 |
and associate a connection with the context.
|
| 75 |
|
|
|
|
| 76 |
"""
|
| 77 |
-
|
| 78 |
-
# Parse the URL and convert SSL parameters
|
| 79 |
-
import re
|
| 80 |
-
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
| 81 |
|
| 82 |
async_url = settings.database_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
sync_url = async_url.replace("postgresql+asyncpg://", "postgresql://")
|
| 84 |
|
| 85 |
# Parse URL to handle SSL parameter
|
|
|
|
| 73 |
In this scenario we need to create an Engine
|
| 74 |
and associate a connection with the context.
|
| 75 |
|
| 76 |
+
NOTE: Alembic migrations are PostgreSQL-specific. For SQLite, use create_all() instead.
|
| 77 |
"""
|
| 78 |
+
from urllib.parse import urlparse, urlunparse
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
async_url = settings.database_url
|
| 81 |
+
|
| 82 |
+
# Skip migrations for SQLite - they use PostgreSQL-specific types
|
| 83 |
+
if async_url.startswith("sqlite"):
|
| 84 |
+
import logging
|
| 85 |
+
logging.getLogger(__name__).warning(
|
| 86 |
+
"Skipping Alembic migrations for SQLite. "
|
| 87 |
+
"Migrations use PostgreSQL-specific types. "
|
| 88 |
+
"Use SQLAlchemy create_all() instead."
|
| 89 |
+
)
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
# Use synchronous postgres driver for migrations instead of asyncpg
|
| 93 |
sync_url = async_url.replace("postgresql+asyncpg://", "postgresql://")
|
| 94 |
|
| 95 |
# Parse URL to handle SSL parameter
|
backend/app/db.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import asyncio
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 4 |
from sqlalchemy.orm import declarative_base
|
|
@@ -59,9 +60,18 @@ async def get_db():
|
|
| 59 |
|
| 60 |
async def run_migrations() -> None:
|
| 61 |
"""
|
| 62 |
-
Run Alembic migrations to `head` (best for dev/local).
|
| 63 |
Uses the sync driver path configured in `backend/alembic/env.py`.
|
|
|
|
|
|
|
| 64 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
from alembic import command
|
| 66 |
from alembic.config import Config
|
| 67 |
|
|
@@ -72,8 +82,16 @@ async def run_migrations() -> None:
|
|
| 72 |
await asyncio.to_thread(command.upgrade, cfg, "head")
|
| 73 |
|
| 74 |
async def init_db():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
if settings.AUTO_MIGRATE:
|
| 76 |
-
# Prefer migrations; create_all() does not add columns to existing tables.
|
| 77 |
await run_migrations()
|
| 78 |
return
|
| 79 |
|
|
|
|
| 1 |
import asyncio
|
| 2 |
+
import logging
|
| 3 |
from pathlib import Path
|
| 4 |
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 5 |
from sqlalchemy.orm import declarative_base
|
|
|
|
| 60 |
|
| 61 |
async def run_migrations() -> None:
|
| 62 |
"""
|
| 63 |
+
Run Alembic migrations to `head` (best for dev/local with PostgreSQL).
|
| 64 |
Uses the sync driver path configured in `backend/alembic/env.py`.
|
| 65 |
+
|
| 66 |
+
NOTE: Migrations are PostgreSQL-specific. For SQLite, use create_all() instead.
|
| 67 |
"""
|
| 68 |
+
# Skip migrations for SQLite - they use PostgreSQL-specific types
|
| 69 |
+
if db_url.startswith("sqlite"):
|
| 70 |
+
logging.getLogger(__name__).info("Skipping Alembic migrations for SQLite - using create_all() instead")
|
| 71 |
+
async with engine.begin() as conn:
|
| 72 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 73 |
+
return
|
| 74 |
+
|
| 75 |
from alembic import command
|
| 76 |
from alembic.config import Config
|
| 77 |
|
|
|
|
| 82 |
await asyncio.to_thread(command.upgrade, cfg, "head")
|
| 83 |
|
| 84 |
async def init_db():
|
| 85 |
+
"""Initialize database - uses create_all() for SQLite, optionally migrations for PostgreSQL."""
|
| 86 |
+
if db_url.startswith("sqlite"):
|
| 87 |
+
# SQLite: always use create_all() since migrations are PostgreSQL-specific
|
| 88 |
+
logging.getLogger(__name__).info("SQLite detected - creating tables with create_all()")
|
| 89 |
+
async with engine.begin() as conn:
|
| 90 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
if settings.AUTO_MIGRATE:
|
| 94 |
+
# PostgreSQL: Prefer migrations; create_all() does not add columns to existing tables.
|
| 95 |
await run_migrations()
|
| 96 |
return
|
| 97 |
|
backend/app/main.py
CHANGED
|
@@ -67,6 +67,7 @@ async def lifespan(app: FastAPI):
|
|
| 67 |
app = FastAPI(title="Lumea Health Platform API", lifespan=lifespan)
|
| 68 |
|
| 69 |
# CORS configuration - allow both localhost ports and Docker
|
|
|
|
| 70 |
origins = [
|
| 71 |
"http://localhost:5173",
|
| 72 |
"http://localhost:5174",
|
|
@@ -80,6 +81,18 @@ origins = [
|
|
| 80 |
settings.frontend_origin,
|
| 81 |
]
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
app.add_middleware(
|
| 84 |
CORSMiddleware,
|
| 85 |
allow_origins=origins,
|
|
|
|
| 67 |
app = FastAPI(title="Lumea Health Platform API", lifespan=lifespan)
|
| 68 |
|
| 69 |
# CORS configuration - allow both localhost ports and Docker
|
| 70 |
+
# Build origins list dynamically for HF Spaces support
|
| 71 |
origins = [
|
| 72 |
"http://localhost:5173",
|
| 73 |
"http://localhost:5174",
|
|
|
|
| 81 |
settings.frontend_origin,
|
| 82 |
]
|
| 83 |
|
| 84 |
+
# Add Hugging Face Spaces origin if running in HF Space
|
| 85 |
+
# SPACE_ID format: username/space-name
|
| 86 |
+
space_id = os.getenv("SPACE_ID")
|
| 87 |
+
if space_id:
|
| 88 |
+
# HF Spaces URL patterns
|
| 89 |
+
space_name = space_id.replace("/", "-").lower()
|
| 90 |
+
origins.extend([
|
| 91 |
+
f"https://{space_name}.hf.space",
|
| 92 |
+
f"https://huggingface.co/spaces/{space_id}",
|
| 93 |
+
])
|
| 94 |
+
logger.info(f"Added HF Space origins for {space_id}")
|
| 95 |
+
|
| 96 |
app.add_middleware(
|
| 97 |
CORSMiddleware,
|
| 98 |
allow_origins=origins,
|
backend/app/routes/auth.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
| 2 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
from sqlalchemy import select
|
|
@@ -9,6 +10,9 @@ from app.security import hash_password, verify_password, create_access_token, ge
|
|
| 9 |
|
| 10 |
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
@router.post("/signup", response_model=TokenResponse)
|
| 13 |
@router.post("/register", response_model=TokenResponse)
|
| 14 |
async def signup(user_data: UserCreate, response: Response, db: AsyncSession = Depends(get_db)):
|
|
@@ -30,13 +34,13 @@ async def signup(user_data: UserCreate, response: Response, db: AsyncSession = D
|
|
| 30 |
|
| 31 |
access_token = create_access_token({"sub": str(new_user.id)})
|
| 32 |
|
| 33 |
-
# Set httpOnly cookie
|
| 34 |
response.set_cookie(
|
| 35 |
key="auth_token",
|
| 36 |
value=access_token,
|
| 37 |
httponly=True,
|
| 38 |
-
secure=
|
| 39 |
-
samesite="lax",
|
| 40 |
max_age=7 * 24 * 60 * 60 # 7 days
|
| 41 |
)
|
| 42 |
|
|
@@ -72,13 +76,13 @@ async def login(
|
|
| 72 |
|
| 73 |
access_token = create_access_token({"sub": str(user.id)})
|
| 74 |
|
| 75 |
-
# Set httpOnly cookie
|
| 76 |
response.set_cookie(
|
| 77 |
key="auth_token",
|
| 78 |
value=access_token,
|
| 79 |
httponly=True,
|
| 80 |
-
secure=
|
| 81 |
-
samesite="lax",
|
| 82 |
max_age=7 * 24 * 60 * 60 # 7 days
|
| 83 |
)
|
| 84 |
|
|
|
|
| 1 |
+
import os
|
| 2 |
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
| 3 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
from sqlalchemy import select
|
|
|
|
| 10 |
|
| 11 |
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
| 12 |
|
| 13 |
+
# Detect if running in production (HF Spaces uses HTTPS)
|
| 14 |
+
IS_PRODUCTION = bool(os.getenv("SPACE_ID")) or os.getenv("ENVIRONMENT") == "production"
|
| 15 |
+
|
| 16 |
@router.post("/signup", response_model=TokenResponse)
|
| 17 |
@router.post("/register", response_model=TokenResponse)
|
| 18 |
async def signup(user_data: UserCreate, response: Response, db: AsyncSession = Depends(get_db)):
|
|
|
|
| 34 |
|
| 35 |
access_token = create_access_token({"sub": str(new_user.id)})
|
| 36 |
|
| 37 |
+
# Set httpOnly cookie (secure=True for HTTPS in production)
|
| 38 |
response.set_cookie(
|
| 39 |
key="auth_token",
|
| 40 |
value=access_token,
|
| 41 |
httponly=True,
|
| 42 |
+
secure=IS_PRODUCTION,
|
| 43 |
+
samesite="lax" if IS_PRODUCTION else "lax",
|
| 44 |
max_age=7 * 24 * 60 * 60 # 7 days
|
| 45 |
)
|
| 46 |
|
|
|
|
| 76 |
|
| 77 |
access_token = create_access_token({"sub": str(user.id)})
|
| 78 |
|
| 79 |
+
# Set httpOnly cookie (secure=True for HTTPS in production)
|
| 80 |
response.set_cookie(
|
| 81 |
key="auth_token",
|
| 82 |
value=access_token,
|
| 83 |
httponly=True,
|
| 84 |
+
secure=IS_PRODUCTION,
|
| 85 |
+
samesite="lax" if IS_PRODUCTION else "lax",
|
| 86 |
max_age=7 * 24 * 60 * 60 # 7 days
|
| 87 |
)
|
| 88 |
|
backend/app/routes/reports.py
CHANGED
|
@@ -24,15 +24,16 @@ from app.schemas import (
|
|
| 24 |
)
|
| 25 |
from app.services.report_service import ReportService
|
| 26 |
from app.services.enhanced_report_service import EnhancedReportService
|
|
|
|
| 27 |
|
| 28 |
logger = logging.getLogger(__name__)
|
| 29 |
logging.basicConfig(level=logging.INFO)
|
| 30 |
|
| 31 |
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
| 32 |
|
| 33 |
-
# Upload directory
|
| 34 |
-
UPLOAD_DIR = Path(
|
| 35 |
-
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 36 |
|
| 37 |
# Allowed file types
|
| 38 |
ALLOWED_EXTENSIONS = {'.pdf', '.png', '.jpg', '.jpeg', '.tiff'}
|
|
|
|
| 24 |
)
|
| 25 |
from app.services.report_service import ReportService
|
| 26 |
from app.services.enhanced_report_service import EnhancedReportService
|
| 27 |
+
from app.settings import settings
|
| 28 |
|
| 29 |
logger = logging.getLogger(__name__)
|
| 30 |
logging.basicConfig(level=logging.INFO)
|
| 31 |
|
| 32 |
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
| 33 |
|
| 34 |
+
# Upload directory from settings (supports HF Spaces /data path)
|
| 35 |
+
UPLOAD_DIR = Path(settings.UPLOAD_DIR)
|
| 36 |
+
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
| 37 |
|
| 38 |
# Allowed file types
|
| 39 |
ALLOWED_EXTENSIONS = {'.pdf', '.png', '.jpg', '.jpeg', '.tiff'}
|
backend/app/services/document_processing.py
CHANGED
|
@@ -41,6 +41,7 @@ from app.services.grok_extractor import extract_document_metrics
|
|
| 41 |
from app.services.memory_service import get_memory_service
|
| 42 |
from app.services.graph_service import get_graph_service
|
| 43 |
from app.services.rag_service import get_rag_service
|
|
|
|
| 44 |
|
| 45 |
logger = logging.getLogger(__name__)
|
| 46 |
|
|
@@ -52,7 +53,6 @@ class DocumentProcessingService:
|
|
| 52 |
Handles the entire flow from file upload to extracted observations.
|
| 53 |
"""
|
| 54 |
|
| 55 |
-
UPLOAD_DIR = Path("./uploads")
|
| 56 |
ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
| 57 |
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20MB
|
| 58 |
|
|
@@ -60,6 +60,8 @@ class DocumentProcessingService:
|
|
| 60 |
self.db = db
|
| 61 |
self.user = user
|
| 62 |
self.pdf_extractor = PDFExtractor()
|
|
|
|
|
|
|
| 63 |
|
| 64 |
def _get_user_upload_dir(self) -> Path:
|
| 65 |
"""Get user-specific upload directory"""
|
|
|
|
| 41 |
from app.services.memory_service import get_memory_service
|
| 42 |
from app.services.graph_service import get_graph_service
|
| 43 |
from app.services.rag_service import get_rag_service
|
| 44 |
+
from app.settings import settings
|
| 45 |
|
| 46 |
logger = logging.getLogger(__name__)
|
| 47 |
|
|
|
|
| 53 |
Handles the entire flow from file upload to extracted observations.
|
| 54 |
"""
|
| 55 |
|
|
|
|
| 56 |
ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
| 57 |
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20MB
|
| 58 |
|
|
|
|
| 60 |
self.db = db
|
| 61 |
self.user = user
|
| 62 |
self.pdf_extractor = PDFExtractor()
|
| 63 |
+
# Use settings for upload directory (supports HF Spaces /data path)
|
| 64 |
+
self.UPLOAD_DIR = Path(settings.UPLOAD_DIR)
|
| 65 |
|
| 66 |
def _get_user_upload_dir(self) -> Path:
|
| 67 |
"""Get user-specific upload directory"""
|
backend/app/services/enhanced_report_service.py
CHANGED
|
@@ -20,6 +20,7 @@ from sqlalchemy import select
|
|
| 20 |
from app.models import Report, Observation, ReportStatus, ObservationType
|
| 21 |
from app.services.pdf_extractor import PDFExtractor
|
| 22 |
from app.services.lab_parser import LabParser
|
|
|
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -34,8 +35,9 @@ class EnhancedReportService:
|
|
| 34 |
self.db = db
|
| 35 |
self.extractor = PDFExtractor()
|
| 36 |
self.parser = LabParser()
|
| 37 |
-
|
| 38 |
-
self.upload_dir
|
|
|
|
| 39 |
|
| 40 |
async def process_report(self, report_id: UUID, user_id: UUID):
|
| 41 |
"""
|
|
|
|
| 20 |
from app.models import Report, Observation, ReportStatus, ObservationType
|
| 21 |
from app.services.pdf_extractor import PDFExtractor
|
| 22 |
from app.services.lab_parser import LabParser
|
| 23 |
+
from app.settings import settings
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 35 |
self.db = db
|
| 36 |
self.extractor = PDFExtractor()
|
| 37 |
self.parser = LabParser()
|
| 38 |
+
# Use settings for upload directory (supports HF Spaces /data path)
|
| 39 |
+
self.upload_dir = Path(settings.UPLOAD_DIR)
|
| 40 |
+
self.upload_dir.mkdir(parents=True, exist_ok=True)
|
| 41 |
|
| 42 |
async def process_report(self, report_id: UUID, user_id: UUID):
|
| 43 |
"""
|
backend/app/services/profile_service.py
CHANGED
|
@@ -10,7 +10,6 @@ from datetime import datetime, date
|
|
| 10 |
from typing import List, Dict, Any, Optional, Tuple
|
| 11 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 12 |
from sqlalchemy import select, delete
|
| 13 |
-
from sqlalchemy.dialects.postgresql import insert
|
| 14 |
|
| 15 |
from app.models import (
|
| 16 |
User, UserProfile, ProfileAnswer, ProfileCondition, ProfileSymptom,
|
|
|
|
| 10 |
from typing import List, Dict, Any, Optional, Tuple
|
| 11 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 12 |
from sqlalchemy import select, delete
|
|
|
|
| 13 |
|
| 14 |
from app.models import (
|
| 15 |
User, UserProfile, ProfileAnswer, ProfileCondition, ProfileSymptom,
|
backend/app/services/report_service.py
CHANGED
|
@@ -18,12 +18,13 @@ from app.settings import settings
|
|
| 18 |
class ReportService:
|
| 19 |
"""Service for managing medical reports"""
|
| 20 |
|
| 21 |
-
UPLOAD_DIR = Path("./uploads")
|
| 22 |
ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
| 23 |
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
| 24 |
|
| 25 |
def __init__(self, db: Session):
|
| 26 |
self.db = db
|
|
|
|
|
|
|
| 27 |
self._ensure_upload_dir()
|
| 28 |
|
| 29 |
def _ensure_upload_dir(self):
|
|
|
|
| 18 |
class ReportService:
|
| 19 |
"""Service for managing medical reports"""
|
| 20 |
|
|
|
|
| 21 |
ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
| 22 |
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
| 23 |
|
| 24 |
def __init__(self, db: Session):
|
| 25 |
self.db = db
|
| 26 |
+
# Use settings for upload directory (supports HF Spaces /data path)
|
| 27 |
+
self.UPLOAD_DIR = Path(settings.UPLOAD_DIR)
|
| 28 |
self._ensure_upload_dir()
|
| 29 |
|
| 30 |
def _ensure_upload_dir(self):
|
backend/app/settings.py
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
|
|
| 1 |
from pydantic_settings import BaseSettings
|
| 2 |
from typing import Optional
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
class Settings(BaseSettings):
|
| 5 |
# Defaults are set so the app can boot in environments like Hugging Face
|
| 6 |
# Spaces without requiring secrets to be configured up front.
|
| 7 |
#
|
| 8 |
# IMPORTANT: Override JWT_SECRET + DATABASE_URL with Space Secrets for any
|
| 9 |
# real deployment.
|
| 10 |
-
database_url: str =
|
| 11 |
jwt_secret: str = "dev-secret-change-me"
|
| 12 |
frontend_origin: str = "http://localhost:5173"
|
| 13 |
jwt_algorithm: str = "HS256"
|
|
@@ -26,8 +40,13 @@ class Settings(BaseSettings):
|
|
| 26 |
OLLAMA_MODEL: str = "hf.co/unsloth/medgemma-4b-it-GGUF:Q6_K_XL"
|
| 27 |
OLLAMA_TIMEOUT: int = 120 # seconds
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# ChromaDB Configuration
|
| 30 |
-
CHROMA_PERSIST_DIR: str = "/
|
| 31 |
EMBEDDING_MODEL: str = "all-MiniLM-L6-v2"
|
| 32 |
RAG_TOP_K: int = 5
|
| 33 |
|
|
|
|
| 1 |
+
import os
|
| 2 |
from pydantic_settings import BaseSettings
|
| 3 |
from typing import Optional
|
| 4 |
|
| 5 |
+
|
| 6 |
+
def _get_default_database_url() -> str:
|
| 7 |
+
"""
|
| 8 |
+
Determine the default database URL based on the environment.
|
| 9 |
+
- In Docker/HF Spaces: Use /data for persistent storage
|
| 10 |
+
- Locally: Use ./lumea.db in current directory
|
| 11 |
+
"""
|
| 12 |
+
# Check if we're in a Docker/HF Spaces environment
|
| 13 |
+
if os.path.exists("/data") or os.getenv("SPACE_ID"):
|
| 14 |
+
return "sqlite+aiosqlite:////data/lumea.db"
|
| 15 |
+
return "sqlite+aiosqlite:///./lumea.db"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
class Settings(BaseSettings):
|
| 19 |
# Defaults are set so the app can boot in environments like Hugging Face
|
| 20 |
# Spaces without requiring secrets to be configured up front.
|
| 21 |
#
|
| 22 |
# IMPORTANT: Override JWT_SECRET + DATABASE_URL with Space Secrets for any
|
| 23 |
# real deployment.
|
| 24 |
+
database_url: str = _get_default_database_url()
|
| 25 |
jwt_secret: str = "dev-secret-change-me"
|
| 26 |
frontend_origin: str = "http://localhost:5173"
|
| 27 |
jwt_algorithm: str = "HS256"
|
|
|
|
| 40 |
OLLAMA_MODEL: str = "hf.co/unsloth/medgemma-4b-it-GGUF:Q6_K_XL"
|
| 41 |
OLLAMA_TIMEOUT: int = 120 # seconds
|
| 42 |
|
| 43 |
+
# Upload directory for reports
|
| 44 |
+
# In Docker/HF Spaces: /data/uploads (persistent)
|
| 45 |
+
# Locally: ./uploads
|
| 46 |
+
UPLOAD_DIR: str = "/data/uploads" if os.path.exists("/data") or os.getenv("SPACE_ID") else "./uploads"
|
| 47 |
+
|
| 48 |
# ChromaDB Configuration
|
| 49 |
+
CHROMA_PERSIST_DIR: str = "/data/chroma_db" if os.path.exists("/data") or os.getenv("SPACE_ID") else "./chroma_db"
|
| 50 |
EMBEDDING_MODEL: str = "all-MiniLM-L6-v2"
|
| 51 |
RAG_TOP_K: int = 5
|
| 52 |
|