feat(03-04): retire flat-file settings; wire per-user AI config via DB lookup

- config.py: Remove SETTINGS_FILE, DEFAULT_SYSTEM_PROMPT, DEFAULT_SETTINGS
  constants; add system_prompt, default_ai_provider, default_ai_model to Settings
- services/classifier.py: Add _DEFAULT_SYSTEM_PROMPT module constant; classify_document
  and suggest_topics_for_document accept ai_provider/ai_model kwargs; no longer calls
  storage.load_settings() — uses app_settings defaults with DB-supplied overrides (D-14, D-15)
- services/storage.py: Delete load_settings, save_settings, mask_api_key, settings_masked;
  remove from __all__; remove import copy, json, DEFAULT_SETTINGS, SETTINGS_FILE (D-12)
- tasks/document_tasks.py: _run resolves user.ai_provider/ai_model via session.get(User,
  doc.user_id) and passes through to classifier; task signature unchanged (T-03-19)
- api/settings.py: Deleted — /api/settings endpoint removed (D-12)
- main.py: Remove settings_router import and include_router call
- tests/test_settings.py: Replace all tests with test_settings_endpoint_removed (404, green)
- tests/test_classifier.py: Implement test_per_user_provider, test_celery_task_uses_user_provider,
  test_default_provider_fallback; remove xfail markers (DOC-03, DOC-05)
This commit is contained in:
curo1305
2026-05-23 20:32:55 +02:00
parent aadc69fea0
commit 6849ebd1e6
8 changed files with 193 additions and 316 deletions
-86
View File
@@ -1,86 +0,0 @@
import time
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import storage
from config import DEFAULT_SYSTEM_PROMPT
from ai import get_provider
router = APIRouter(prefix="/api/settings", tags=["settings"])
class SettingsPatch(BaseModel):
system_prompt: Optional[str] = None
active_provider: Optional[str] = None
providers: Optional[dict] = None
class TestProviderRequest(BaseModel):
provider: str
@router.get("")
async def get_settings():
settings = storage.load_settings()
return storage.settings_masked(settings)
@router.patch("")
async def patch_settings(body: SettingsPatch):
settings = storage.load_settings()
if body.system_prompt is not None:
settings["system_prompt"] = body.system_prompt
if body.active_provider is not None:
valid = {"anthropic", "openai", "ollama", "lmstudio"}
if body.active_provider not in valid:
raise HTTPException(400, f"Invalid provider. Must be one of: {valid}")
settings["active_provider"] = body.active_provider
if body.providers is not None:
# Deep merge per-provider config
for prov_name, prov_cfg in body.providers.items():
if prov_name not in settings.get("providers", {}):
settings.setdefault("providers", {})[prov_name] = {}
existing = settings["providers"][prov_name]
for key, val in prov_cfg.items():
# Don't overwrite api_key if it comes in masked (contains ****)
if key == "api_key" and val and "****" in str(val):
continue
existing[key] = val
storage.save_settings(settings)
return storage.settings_masked(settings)
@router.post("/test-provider")
async def test_provider(body: TestProviderRequest):
settings = storage.load_settings()
# Temporarily switch active provider for the test
test_settings = dict(settings)
test_settings["active_provider"] = body.provider
try:
provider = get_provider(test_settings)
except ValueError as e:
raise HTTPException(400, str(e))
start = time.monotonic()
try:
ok = await provider.health_check()
except Exception as e:
return {"ok": False, "message": str(e), "latency_ms": 0}
latency_ms = int((time.monotonic() - start) * 1000)
return {
"ok": ok,
"message": "Connection successful" if ok else "Health check failed",
"latency_ms": latency_ms,
}
@router.get("/default-prompt")
async def get_default_prompt():
return {"system_prompt": DEFAULT_SYSTEM_PROMPT}