Skip to content
Closed
17 changes: 9 additions & 8 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import time
import uuid
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
Expand All @@ -26,7 +25,12 @@
from routes.sessions import router as sessions_router
from routes.settings import router as settings_router
from routes.upload import router as upload_router
from services.db_service import dedupe_clear_orphaned_processing, get_db, init_db
from services.db_service import (
dedupe_clear_orphaned_processing,
get_db,
init_db,
)
from utils.config import settings


# --- Issue #284 Engine Stability: Contextual Thread-Safe Log Formatter ---
Expand All @@ -53,7 +57,7 @@ def format(self, record):
root_logger.handlers = [stream_handler]

logger = logging.getLogger(__name__)
FRONTEND_DIST = Path(os.getenv("FRONTEND_DIST", "/app/frontend/dist"))
FRONTEND_DIST = settings.frontend_dist


def run_preflight_checks():
Expand Down Expand Up @@ -102,7 +106,6 @@ async def lifespan(app: FastAPI):
# server run. They will never be resolved to 'done', so leaving them
# would produce false-409 responses for legitimate first retries.
dedupe_clear_orphaned_processing()

# Start stream cleanup task
from routes.chat import clean_expired_streams

Expand Down Expand Up @@ -141,11 +144,9 @@ async def add_request_correlation_id(request: Request, call_next):
return response


default_cors_origins = "http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://localhost:8000"

cors_origins = [
origin.strip()
for origin in os.getenv("CORS_ORIGINS", default_cors_origins).split(",")
if origin.strip()
origin.strip() for origin in settings.cors_origins.split(",") if origin.strip()
]

app.add_middleware(GZipMiddleware, minimum_size=1000)
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ python-docx==1.1.2
unstructured==0.18.31
python-multipart==0.0.9
pydantic==2.9.0
pydantic-settings==2.5.2
python-dotenv==1.0.1
httpx==0.27.0
pytest==8.3.0
Expand Down
71 changes: 38 additions & 33 deletions backend/routes/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import asyncio
import logging
import os
import time
from collections.abc import Callable
from typing import Any

from fastapi import APIRouter, HTTPException, status
from models.schemas import AppSettings
from services.db_service import get_settings, save_setting, save_settings
from utils.config import settings

router = APIRouter()
logger = logging.getLogger(__name__)
Expand All @@ -18,16 +18,7 @@


def _resolve_settings_timeout_seconds() -> float:
raw_timeout = os.getenv("SETTINGS_API_TIMEOUT_SECONDS", str(DEFAULT_SETTINGS_API_TIMEOUT_SECONDS))
try:
parsed_timeout = float(raw_timeout)
except ValueError:
logger.warning(
"settings_timeout_invalid raw_value=%s fallback_timeout_s=%s",
raw_timeout,
DEFAULT_SETTINGS_API_TIMEOUT_SECONDS,
)
return DEFAULT_SETTINGS_API_TIMEOUT_SECONDS
parsed_timeout = float(settings.settings_api_timeout_seconds)
if parsed_timeout <= 0:
logger.warning(
"settings_timeout_non_positive timeout_s=%s fallback_timeout_s=%s",
Expand All @@ -41,7 +32,9 @@ def _resolve_settings_timeout_seconds() -> float:
SETTINGS_API_TIMEOUT_SECONDS = _resolve_settings_timeout_seconds()


async def _run_with_timeout(operation: str, function: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
async def _run_with_timeout(
operation: str, function: Callable[..., Any], *args: Any, **kwargs: Any
) -> Any:
start_time = time.perf_counter()
try:
return await asyncio.wait_for(
Expand All @@ -60,54 +53,65 @@ async def _run_with_timeout(operation: str, function: Callable[..., Any], *args:
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail=f"Settings {operation} operation timed out after {SETTINGS_API_TIMEOUT_SECONDS} seconds.",
) from exc


@router.get("/")
async def get_all():
return await _run_with_timeout("read", get_settings)


@router.put("/")
async def update_settings(body: AppSettings):
# 1. Enforce safety validation boundary limits on Temperature
if body.temperature < 0.0 or body.temperature > 2.0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{
"loc": ["body", "temperature"],
"msg": "Temperature must scale cleanly between 0.0 and 2.0.",
"type": "value_error"
}]
detail=[
{
"loc": ["body", "temperature"],
"msg": "Temperature must scale cleanly between 0.0 and 2.0.",
"type": "value_error",
}
],
)

# 2. Enforce safety validation boundary limits on RAG Context Chunks
if body.rag_top_k < 1 or body.rag_top_k > 10:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{
"loc": ["body", "rag_top_k"],
"msg": "RAG Context chunks selection must stay between 1 and 10.",
"type": "value_error"
}]
detail=[
{
"loc": ["body", "rag_top_k"],
"msg": "RAG Context chunks selection must stay between 1 and 10.",
"type": "value_error",
}
],
)

# 3. Enforce safety validation boundary limits on RAG Chunk Size
if body.rag_chunk_size < 100 or body.rag_chunk_size > 2000:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{
"loc": ["body", "rag_chunk_size"],
"msg": "RAG chunk size must be between 100 and 2000 characters.",
"type": "value_error"
}]
detail=[
{
"loc": ["body", "rag_chunk_size"],
"msg": "RAG chunk size must be between 100 and 2000 characters.",
"type": "value_error",
}
],
)

# 4. Enforce safety validation boundary limits on RAG Chunk Overlap
if body.rag_chunk_overlap < 0 or body.rag_chunk_overlap > 200:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{
"loc": ["body", "rag_chunk_overlap"],
"msg": "RAG chunk overlap must be between 0 and 200 characters.",
"type": "value_error"
}]
detail=[
{
"loc": ["body", "rag_chunk_overlap"],
"msg": "RAG chunk overlap must be between 0 and 200 characters.",
"type": "value_error",
}
],
)

current_settings = get_settings()
Expand All @@ -126,6 +130,7 @@ async def update_settings(body: AppSettings):
await _run_with_timeout("save", save_settings, payload)
return await _run_with_timeout("read", get_settings)


@router.put("/{key}")
async def update_one(key: str, value: dict):
new_value = value.get("value")
Expand All @@ -140,4 +145,4 @@ async def update_one(key: str, value: dict):
)

await _run_with_timeout("save", save_setting, key, new_value)
return {"key": key, "updated": True}
return {"key": key, "updated": True}
Loading
Loading