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 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
- ENV FRONTEND_DIST_DIR=/app/frontend_dist
 
 
 
 
 
 
 
 
55
 
56
  EXPOSE 7860
57
 
58
- HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
59
  CMD curl -f http://localhost:7860/api || exit 1
60
 
61
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
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
- ### Docker Compose (Development/Staging)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1047
 
1048
  ```bash
1049
  docker-compose up -d
@@ -1056,11 +1077,13 @@ Includes:
1056
 
1057
  ### Production Considerations
1058
 
1059
- 1. **Database**: Use Neon, Supabase, or managed PostgreSQL
 
 
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
- # Use synchronous postgres driver for migrations instead of asyncpg
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=False, # Set to True in production with HTTPS
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=False, # Set to True in production with HTTPS
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("uploads")
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
- self.upload_dir = Path("uploads")
38
- self.upload_dir.mkdir(exist_ok=True)
 
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 = "sqlite+aiosqlite:///./lumea.db"
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 = "/app/chroma_db"
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