khadijaaao commited on
Commit
0f07bde
·
verified ·
1 Parent(s): 0afc9b2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -0
app.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # BACKEND API POUR COACH PÉDAGOGIQUE IA (VERSION FINALE)
3
+ # =============================================================================
4
+ # Ce script utilise FastAPI et implémente la méthode de téléchargement dynamique
5
+ # pour le modèle LLM, évitant ainsi d'avoir à le stocker dans le dépôt Git.
6
+ # =============================================================================
7
+
8
+ # --- Imports ---
9
+ import os
10
+ import torch
11
+ from fastapi import FastAPI, HTTPException
12
+ from pydantic import BaseModel
13
+ from llama_cpp import Llama
14
+ from langchain_community.vectorstores import FAISS
15
+ from langchain_community.embeddings import HuggingFaceEmbeddings
16
+ from huggingface_hub import hf_hub_download
17
+ import logging
18
+
19
+ # Configuration du logging
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # --- Initialisation de l'API ---
24
+ app = FastAPI()
25
+
26
+ # --- Modèles Pydantic pour la validation des données ---
27
+ class QuestionRequest(BaseModel):
28
+ question: str
29
+
30
+ class AnswerResponse(BaseModel):
31
+ answer: str
32
+
33
+ # --- Chargement des modèles au démarrage de l'API ---
34
+ # On utilise un "singleton" pour s'assurer que les modèles ne sont chargés qu'une seule fois.
35
+ class ModelSingleton:
36
+ llm = None
37
+ vectorstore = None
38
+ embeddings = None
39
+
40
+ def load_models(self):
41
+ if self.llm is None:
42
+ try:
43
+ # --- Étape 1 : Configuration des chemins vers les artefacts LOCAUX ---
44
+ # Ces dossiers (embeddings, faiss) DOIVENT être dans votre dépôt Git.
45
+ base_dir = os.path.dirname(__file__)
46
+ faiss_index_path = os.path.join(base_dir, "faiss_index_wize")
47
+ embedding_model_path = os.path.join(base_dir, "embedding_model_saved")
48
+
49
+ logger.info("Chargement du modèle d'embeddings local...")
50
+ self.embeddings = HuggingFaceEmbeddings(
51
+ model_name=embedding_model_path,
52
+ model_kwargs={'device': 'cpu'} # Sur un Space gratuit, c'est CPU uniquement
53
+ )
54
+ logger.info("Modèle d'embeddings chargé.")
55
+
56
+ logger.info("Chargement de la base de connaissances FAISS locale...")
57
+ self.vectorstore = FAISS.load_local(
58
+ faiss_index_path,
59
+ self.embeddings,
60
+ allow_dangerous_deserialization=True
61
+ )
62
+ logger.info("Base de connaissances FAISS chargée.")
63
+
64
+ # --- Étape 2 : Téléchargement dynamique du gros modèle GGUF ---
65
+ # Le fichier n'est PAS dans le dépôt, il est téléchargé depuis le Hub.
66
+ model_repo_id = "QuantFactory/Meta-Llama-3-8B-Instruct-GGUF"
67
+ model_filename = "Meta-Llama-3-8B-Instruct.Q4_K_M.gguf"
68
+
69
+ logger.info(f"Téléchargement du modèle LLM '{model_filename}' depuis le Hub... (peut être long)")
70
+
71
+ model_path = hf_hub_download(
72
+ repo_id=model_repo_id,
73
+ filename=model_filename
74
+ )
75
+ logger.info(f"Modèle téléchargé dans : {model_path}")
76
+
77
+ # --- Étape 3 : Chargement du LLM depuis le fichier téléchargé ---
78
+ logger.info("Chargement du modèle LLM en mémoire (peut échouer par manque de RAM)...")
79
+ self.llm = Llama(
80
+ model_path=model_path,
81
+ n_gpu_layers=0, # 0 car nous sommes sur un CPU
82
+ n_ctx=4096,
83
+ verbose=False,
84
+ chat_format="llama-3"
85
+ )
86
+ logger.info("✅ Modèle LLM chargé avec succès.")
87
+
88
+ except Exception as e:
89
+ logger.error(f"❌ Erreur critique lors du chargement des modèles: {e}")
90
+ # Si le chargement échoue, on lève une exception pour que l'API ne démarre pas incorrectement.
91
+ raise RuntimeError(f"Impossible de charger les modèles: {e}")
92
+
93
+ # Instancier et charger les modèles au démarrage de l'application
94
+ # L'événement "startup" de FastAPI est le meilleur endroit pour faire ça.
95
+ @app.on_event("startup")
96
+ def startup_event():
97
+ global models
98
+ models = ModelSingleton()
99
+ try:
100
+ models.load_models()
101
+ except Exception as e:
102
+ # On log l'erreur, l'API répondra avec des erreurs 503 si les modèles ne sont pas chargés.
103
+ logger.error(f"DÉMARRAGE ÉCHOUÉ : Les modèles n'ont pas pu être initialisés. {e}")
104
+ # On met les modèles à None pour pouvoir gérer l'erreur proprement dans les endpoints.
105
+ models.llm = None
106
+ models.vectorstore = None
107
+
108
+ # --- Définition du point de terminaison de l'API ---
109
+ @app.post("/ask", response_model=AnswerResponse)
110
+ def ask_question(request: QuestionRequest):
111
+ """
112
+ Ce point de terminaison reçoit une question, utilise le RAG pour trouver
113
+ le contexte et génère une réponse avec le LLM.
114
+ """
115
+ if models.llm is None or models.vectorstore is None:
116
+ raise HTTPException(status_code=503, detail="Service non disponible : les modèles n'ont pas pu être chargés au démarrage.")
117
+
118
+ user_question = request.question
119
+ logger.info(f"Requête reçue pour la question : '{user_question}'")
120
+
121
+ try:
122
+ # 1. RAG : Récupérer le contexte
123
+ retriever = models.vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
124
+ docs = retriever.invoke(user_question)
125
+ context = "\n".join([doc.page_content for doc in docs])
126
+
127
+ # 2. Prompt Engineering (votre logique exacte)
128
+ system_message = (
129
+ "Tu es un coach pédagogique expert, travaillant avec un système RAG basé sur des documents fournis. "
130
+ "Tu réponds uniquement à partir des informations extraites de ces documents. "
131
+ "Tu ne réponds qu’en français. Tu ne dois jamais inventer de réponse. "
132
+ "Tes réponses doivent être en 1 à 2 phrases maximum, claires et compactes."
133
+ )
134
+
135
+ prompt = f"""
136
+ <|begin_of_text|><|start_header_id|>system<|end_header_id|>
137
+ {system_message}
138
+ <|eot_id|><|start_header_id|>user<|end_header_id|>
139
+ Contexte :
140
+ {context}
141
+
142
+ Question : {user_question}
143
+ <|eot_id|><|start_header_id|>assistant<|end_header_id|>
144
+ """
145
+ # 3. Génération de la réponse
146
+ logger.info("Génération de la réponse...")
147
+ response = models.llm(
148
+ prompt,
149
+ max_tokens=512, # On réduit pour une réponse plus rapide
150
+ temperature=0.3,
151
+ stop=["<|eot_id|>"],
152
+ echo=False
153
+ )
154
+ answer = response['choices'][0]['text'].strip()
155
+ logger.info("Réponse générée avec succès.")
156
+
157
+ return AnswerResponse(answer=answer)
158
+
159
+ except Exception as e:
160
+ logger.error(f"Erreur lors de la génération de la réponse : {e}")
161
+ raise HTTPException(status_code=500, detail=str(e))
162
+
163
+ @app.get("/")
164
+ def read_root():
165
+ """Point de terminaison racine pour vérifier que le serveur est en marche."""
166
+ return {"status": "Backend du Coach IA est en ligne. Utilisez le point de terminaison /ask pour poser une question."}