Spaces:
Sleeping
Sleeping
| """Application FastAPI pour répondre aux questions FAQ via embeddings multilingues.""" | |
| import logging | |
| import sys | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.responses import HTMLResponse, Response | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from config import ( | |
| CSV_DELIMITER, | |
| FAQ_FORMULATIONS_PATH, | |
| FAQ_RESPONSES_PATH, | |
| ) | |
| from embeddings import EmbeddingManager | |
| from faq_loader import FAQEntry, load_faq_data | |
| from models import AnswerOutput, HealthResponse, QuestionInput | |
| # Configuration du logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Variables globales | |
| embedding_manager: EmbeddingManager | None = None | |
| faq_entries: list[FAQEntry] = [] | |
| async def lifespan(app: FastAPI): | |
| """ | |
| Gestion du cycle de vie de l'application FastAPI. | |
| Au démarrage: charger les FAQ et les embeddings dans ChromaDB | |
| À l'arrêt: nettoyer les ressources | |
| """ | |
| # --- Startup --- | |
| logger.info("🚀 Démarrage de l'application...") | |
| global embedding_manager, faq_entries | |
| try: | |
| # Initialiser le gestionnaire d'embeddings | |
| embedding_manager = EmbeddingManager() | |
| # Charger les données FAQ | |
| logger.info("📚 Chargement des données FAQ...") | |
| faq_entries = load_faq_data( | |
| FAQ_FORMULATIONS_PATH, | |
| FAQ_RESPONSES_PATH, | |
| delimiter=CSV_DELIMITER, | |
| ) | |
| # Peupler ChromaDB | |
| embedding_manager.populate_collection(faq_entries) | |
| logger.info(f"✓ Application prête - {embedding_manager.get_collection_size()} FAQs indexées") | |
| except Exception as e: | |
| logger.error(f"❌ Erreur au démarrage: {e}") | |
| # En cas d'erreur fatale au démarrage, quitter le process pour éviter | |
| # que la plateforme (ex: Hugging Face Spaces) reste en 'starting'. | |
| sys.exit(1) | |
| yield | |
| # --- Shutdown --- | |
| logger.info("🛑 Arrêt de l'application...") | |
| embedding_manager = None | |
| faq_entries = [] | |
| # Créer l'application FastAPI | |
| app = FastAPI( | |
| title="API FAQ - Embeddings multilingues", | |
| description="API pour répondre à des questions via recherche de similarité sur une base FAQ", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| # Configuration CORS (adapter selon vos besoins) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def health_check() -> HealthResponse: | |
| """ | |
| Endpoint de vérification de l'état du service. | |
| Returns: | |
| HealthResponse avec status et nombre de FAQs indexées | |
| """ | |
| if embedding_manager is None: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Service non initialisé - les embeddings ne sont pas chargés", | |
| ) | |
| faq_count: int = embedding_manager.get_collection_size() | |
| return HealthResponse( | |
| status="ok", | |
| faq_count=faq_count, | |
| ) | |
| async def ask_question(input_data: QuestionInput) -> AnswerOutput: | |
| """ | |
| Endpoint principal pour poser une question à la base FAQ. | |
| Reçoit une question en texte libre, génère son embedding multilingue, | |
| et retourne la réponse la plus similaire avec un score de confiance. | |
| Args: | |
| input_data: Requête contenant la question | |
| Returns: | |
| AnswerOutput avec la réponse trouvée, la formulation, le theme, et le score de similarité | |
| Raises: | |
| HTTPException: Si le service n'est pas initialisé ou aucun match trouvé au-dessus du seuil | |
| """ | |
| if embedding_manager is None: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Service non initialisé - les embeddings ne sont pas chargés", | |
| ) | |
| question: str = input_data.question | |
| logger.info(f"📝 Question reçue: {question}") | |
| # Rechercher la FAQ similaire (le seuil peut être fourni dans `input_data.threshold`) | |
| result = embedding_manager.search_similar_faq(input_data) | |
| # `search_similar_faq` renvoie déjà un `AnswerOutput` (avec `confidence=False` si pas de match) | |
| logger.info(f"✓ Réponse préparée pour: {question}") | |
| return result | |
| async def list_faq_entries() -> Response: | |
| """Retourner toutes les FAQ sous forme de chaîne Markdown.""" | |
| if not faq_entries: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Service non initialisé - les FAQ ne sont pas chargées", | |
| ) | |
| markdown_output: str = "\n\n".join( | |
| f"**{entry.formulation}**\n{entry.response}" for entry in faq_entries | |
| ) | |
| return Response(content=markdown_output, media_type="text/markdown") | |
| async def root() -> str: | |
| """Endpoint root avec information sur l'API.""" | |
| #return { | |
| # "message": "Bienvenue sur l'API FAQ - Embeddings multilingues", | |
| # "docs": "/docs", | |
| # "health": "/health", | |
| # "ask": "/ask", | |
| #} | |
| return """ | |
| <html> | |
| <head><title>Page d'accueil</title></head> | |
| <body> | |
| <h1>Welcome on the FAQ API !</h1> | |
| </body> | |
| </html> | |
| """ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run( | |
| "app:app", | |
| host="0.0.0.0", | |
| port=8000, | |
| reload=True, | |
| log_level="info", | |
| ) | |