""" Admin-only settings API for per-service runtime configuration. All endpoints require the caller to be an admin (Depends(get_current_admin)). Config files live on the shared app_config volume (/config/). """ import asyncio import httpx from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from app.core.app_config import ( SYSTEM_PROMPT_SERVICES, _merge_api_key, load_ai_service_config, load_ai_service_config_masked, load_all_system_prompts, load_doc_service_config, load_doc_service_config_masked, save_ai_service_config, save_doc_service_config, save_service_system_prompts, ) from app.core.config import settings from app.deps import get_current_admin from app.models.user import User router = APIRouter() # ── Pydantic request bodies ──────────────────────────────────────────────────── class AIProviderUpdate(BaseModel): provider: str anthropic_api_key: str = "" anthropic_model: str = "" ollama_base_url: str = "" ollama_model: str = "" ollama_api_key: str = "" lmstudio_base_url: str = "" lmstudio_model: str = "" lmstudio_api_key: str = "" class LimitsUpdate(BaseModel): max_pdf_mb: int class SystemPromptUpdate(BaseModel): system: str user_template: str # ── AI settings ──────────────────────────────────────────────────────────────── @router.get("/ai") async def get_ai_settings( _: User = Depends(get_current_admin), ) -> dict: return load_ai_service_config_masked() @router.patch("/ai") async def update_ai_settings( body: AIProviderUpdate, _: User = Depends(get_current_admin), ) -> dict: valid_providers = ("anthropic", "ollama", "lmstudio") if body.provider not in valid_providers: raise HTTPException(status_code=422, detail=f"provider must be one of {valid_providers}") config = load_ai_service_config() config.provider = body.provider # Anthropic if body.anthropic_api_key: config.anthropic.api_key = _merge_api_key( body.anthropic_api_key, config.anthropic.api_key ) if body.anthropic_model: config.anthropic.model = body.anthropic_model # Ollama if body.ollama_base_url: config.ollama.base_url = body.ollama_base_url if body.ollama_model: config.ollama.model = body.ollama_model if body.ollama_api_key: config.ollama.api_key = _merge_api_key(body.ollama_api_key, config.ollama.api_key) # LM Studio if body.lmstudio_base_url: config.lmstudio.base_url = body.lmstudio_base_url if body.lmstudio_model: config.lmstudio.model = body.lmstudio_model if body.lmstudio_api_key: config.lmstudio.api_key = _merge_api_key( body.lmstudio_api_key, config.lmstudio.api_key ) await asyncio.to_thread(save_ai_service_config, config) return load_ai_service_config_masked() @router.post("/ai/test") async def test_ai_connection( _: User = Depends(get_current_admin), ) -> dict: """Proxy a minimal chat request to ai-service to verify the connection.""" try: async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post( f"{settings.AI_SERVICE_URL}/chat", json={ "messages": [{"role": "user", "content": "Reply with: ok"}], "max_tokens": 16, "temperature": 0, }, ) if resp.status_code == 200: data = resp.json() return {"ok": True, "provider": data.get("provider"), "response": data.get("content")} return {"ok": False, "error": f"ai-service returned {resp.status_code}: {resp.text[:200]}"} except Exception as exc: return {"ok": False, "error": str(exc)} # ── Document limits ──────────────────────────────────────────────────────────── @router.get("/documents/limits") async def get_documents_limits( _: User = Depends(get_current_admin), ) -> dict: return load_doc_service_config_masked() @router.patch("/documents/limits") async def update_documents_limits( body: LimitsUpdate, _: User = Depends(get_current_admin), ) -> dict: if body.max_pdf_mb < 1 or body.max_pdf_mb > 200: raise HTTPException(status_code=422, detail="max_pdf_mb must be between 1 and 200") config = load_doc_service_config() config.documents.max_pdf_bytes = body.max_pdf_mb * 1024 * 1024 await asyncio.to_thread(save_doc_service_config, config) return load_doc_service_config_masked() # ── System prompts ───────────────────────────────────────────────────────────── @router.get("/system-prompts") async def get_system_prompts( _: User = Depends(get_current_admin), ) -> dict: """Return all editable system prompts, keyed by service id.""" return await asyncio.to_thread(load_all_system_prompts) @router.patch("/system-prompts/{service_id}") async def update_system_prompt( service_id: str, body: SystemPromptUpdate, _: User = Depends(get_current_admin), ) -> dict: """Update the system prompts for a single service.""" if service_id not in SYSTEM_PROMPT_SERVICES: raise HTTPException(status_code=404, detail=f"No system prompts registered for {service_id!r}") await asyncio.to_thread( save_service_system_prompts, service_id, body.system, body.user_template ) return await asyncio.to_thread(load_all_system_prompts)