Spaces:
Build error
Build error
| """ | |
| Endpoints de gestion des providers et modèles IA (R10 — préfixe /api/v1/). | |
| GET /api/v1/providers → providers détectés (disponibles ou non) | |
| GET /api/v1/providers/{provider_type}/models → modèles d'un provider | |
| POST /api/v1/models/refresh → liste agrégée de tous les modèles | |
| PUT /api/v1/corpora/{id}/model → associe un modèle à un corpus | |
| GET /api/v1/corpora/{id}/model → modèle actif d'un corpus | |
| Les clés API vivent exclusivement dans les secrets HuggingFace (variables | |
| d'environnement). Le backend détecte automatiquement quels providers sont | |
| disponibles au démarrage. L'interface ne demande jamais de clé (R06). | |
| """ | |
| # 1. stdlib | |
| import logging | |
| from datetime import datetime, timezone | |
| # 2. third-party | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from pydantic import BaseModel, ConfigDict, Field | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| # 3. local | |
| from app.models.corpus import CorpusModel | |
| from app.models.database import get_db | |
| from app.models.model_config_db import ModelConfigDB | |
| from app.schemas.model_config import ProviderType | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(tags=["models"]) | |
| # ── Schémas ─────────────────────────────────────────────────────────────────── | |
| class ProviderInfo(BaseModel): | |
| """Informations sur un provider IA détecté au démarrage.""" | |
| provider_type: str | |
| display_name: str | |
| available: bool | |
| model_count: int | |
| class ModelSelectRequest(BaseModel): | |
| model_id: str = Field(..., min_length=1, max_length=256) | |
| provider_type: str = Field(..., min_length=1, max_length=64) | |
| display_name: str = Field("", max_length=256) | |
| supports_vision: bool = Field(True) | |
| class ModelConfigResponse(BaseModel): | |
| model_config = ConfigDict(from_attributes=True) | |
| corpus_id: str | |
| provider_type: str | |
| selected_model_id: str | |
| selected_model_display_name: str | |
| supports_vision: bool | |
| updated_at: datetime | |
| class ModelsRefreshResponse(BaseModel): | |
| models: list[dict] | |
| count: int | |
| refreshed_at: datetime | |
| # ── Endpoints ───────────────────────────────────────────────────────────────── | |
| async def list_providers() -> list[dict]: | |
| """Liste tous les providers IA avec leur disponibilité. | |
| Un provider est disponible si la variable d'environnement correspondante | |
| est présente dans les secrets HuggingFace. Aucune clé n'est exposée. | |
| """ | |
| from app.services.ai.model_registry import get_available_providers | |
| return get_available_providers() | |
| async def get_provider_models(provider_type: str) -> list[dict]: | |
| """Liste les modèles disponibles pour un provider spécifique.""" | |
| try: | |
| ptype = ProviderType(provider_type) | |
| except ValueError: | |
| raise HTTPException( | |
| status_code=404, | |
| detail=f"Provider inconnu : {provider_type}. " | |
| f"Valeurs acceptées : {[p.value for p in ProviderType]}", | |
| ) | |
| from app.services.ai.model_registry import list_models_for_provider | |
| try: | |
| models = list_models_for_provider(ptype) | |
| except RuntimeError as exc: | |
| raise HTTPException(status_code=503, detail=str(exc)) | |
| except Exception as exc: | |
| logger.warning("Erreur listing models", extra={"provider": provider_type, "error": str(exc)}) | |
| raise HTTPException(status_code=502, detail="Erreur temporaire du provider IA. Réessayez ultérieurement.") | |
| return [m.model_dump() for m in models] | |
| async def refresh_models() -> ModelsRefreshResponse: | |
| """Force la mise à jour de la liste agrégée de tous les modèles disponibles.""" | |
| from app.services.ai.model_registry import list_all_models | |
| models = list_all_models() | |
| return ModelsRefreshResponse( | |
| models=[m.model_dump() for m in models], | |
| count=len(models), | |
| refreshed_at=datetime.now(timezone.utc), | |
| ) | |
| async def set_corpus_model( | |
| corpus_id: str, | |
| body: ModelSelectRequest, | |
| db: AsyncSession = Depends(get_db), | |
| ) -> ModelConfigResponse: | |
| """Associe un modèle IA à un corpus. Crée ou met à jour la configuration.""" | |
| corpus = await db.get(CorpusModel, corpus_id) | |
| if corpus is None: | |
| raise HTTPException(status_code=404, detail="Corpus introuvable") | |
| display_name = body.display_name or body.model_id | |
| config = await db.get(ModelConfigDB, corpus_id) | |
| if config is None: | |
| config = ModelConfigDB( | |
| corpus_id=corpus_id, | |
| provider_type=body.provider_type, | |
| selected_model_id=body.model_id, | |
| selected_model_display_name=display_name, | |
| supports_vision=body.supports_vision, | |
| updated_at=datetime.now(timezone.utc), | |
| ) | |
| db.add(config) | |
| else: | |
| config.provider_type = body.provider_type | |
| config.selected_model_id = body.model_id | |
| config.selected_model_display_name = display_name | |
| config.supports_vision = body.supports_vision | |
| config.updated_at = datetime.now(timezone.utc) | |
| await db.commit() | |
| await db.refresh(config) | |
| return config | |
| async def get_corpus_model( | |
| corpus_id: str, db: AsyncSession = Depends(get_db) | |
| ) -> ModelConfigResponse: | |
| """Retourne la configuration du modèle IA actif pour un corpus.""" | |
| corpus = await db.get(CorpusModel, corpus_id) | |
| if corpus is None: | |
| raise HTTPException(status_code=404, detail="Corpus introuvable") | |
| config = await db.get(ModelConfigDB, corpus_id) | |
| if config is None: | |
| raise HTTPException( | |
| status_code=404, | |
| detail="Aucun modèle configuré pour ce corpus", | |
| ) | |
| return config | |