AxL95 commited on
Commit
92343a7
·
verified ·
1 Parent(s): 5bee94a

Ajout fichiers pour mieux structurer le code

Browse files
Files changed (7) hide show
  1. admin.py +130 -0
  2. auth.py +137 -0
  3. chat.py +367 -0
  4. conversations.py +142 -0
  5. database.py +34 -0
  6. init_documents.py +81 -0
  7. utils.py +33 -0
admin.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, File, UploadFile, HTTPException, Depends
2
+ from bson.objectid import ObjectId
3
+ import os
4
+ import PyPDF2
5
+ from io import BytesIO
6
+ from datetime import datetime
7
+
8
+ from auth import get_admin_user
9
+ from database import get_db
10
+ from config import SAVE_FOLDER
11
+ from chat import embedding_model
12
+
13
+ router = APIRouter(prefix="/api/admin", tags=["Administration"])
14
+ db=get_db()
15
+
16
+ @router.post("/knowledge/upload")
17
+ async def upload_pdf(
18
+ file: UploadFile = File(...),
19
+ title: str = None,
20
+ tags: str = None,
21
+ current_user: dict = Depends(get_admin_user)
22
+ ):
23
+ try:
24
+ if not file.filename.endswith('.pdf'):
25
+ raise HTTPException(status_code=400, detail="Le fichier doit être un PDF")
26
+
27
+ contents = await file.read()
28
+ pdf_file = BytesIO(contents)
29
+
30
+ pdf_reader = PyPDF2.PdfReader(pdf_file)
31
+ text_content = ""
32
+ for page_num in range(len(pdf_reader.pages)):
33
+ text_content += pdf_reader.pages[page_num].extract_text() + "\n"
34
+
35
+ embedding = None
36
+ if embedding_model:
37
+ try:
38
+ max_length = 5000
39
+ truncated_text = text_content[:max_length]
40
+ embedding = embedding_model.encode(truncated_text).tolist()
41
+ except Exception as e:
42
+ print(f"Erreur lors de la génération de l'embedding: {str(e)}")
43
+
44
+ doc_id = ObjectId()
45
+
46
+ pdf_path = f"files/{str(doc_id)}.pdf"
47
+ os.makedirs("files", exist_ok=True)
48
+ with open(pdf_path, "wb") as f:
49
+ pdf_file.seek(0)
50
+ f.write(contents)
51
+
52
+ document = {
53
+ "_id": doc_id,
54
+ "text": text_content,
55
+ "embedding": embedding,
56
+ "title": title or file.filename,
57
+ "tags": tags.split(",") if tags else [],
58
+ "uploaded_by": str(current_user["_id"]),
59
+ "upload_date": datetime.utcnow()
60
+ }
61
+
62
+ print(f"Tentative d'insertion du document avec ID: {doc_id}")
63
+ result = db.connaissances.insert_one(document)
64
+ print(f"Document inséré avec ID: {result.inserted_id}")
65
+
66
+ verification = db.connaissances.find_one({"_id": doc_id})
67
+ if verification:
68
+ print(f"Document vérifié et trouvé dans la base de données")
69
+ return {"success": True, "document_id": str(doc_id)}
70
+ else:
71
+ print(f"ERREUR: Document non trouvé après insertion")
72
+ return {"success": False, "error": "Document non trouvé après insertion"}
73
+
74
+ except Exception as e:
75
+ import traceback
76
+ print(f"Erreur lors de l'upload du PDF: {traceback.format_exc()}")
77
+ raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}")
78
+
79
+ @router.get("/knowledge")
80
+ async def list_documents(current_user: dict = Depends(get_admin_user)):
81
+ try:
82
+ documents = list(db.connaissances.find().sort("upload_date", -1))
83
+
84
+ result = []
85
+ for doc in documents:
86
+ doc_safe = {
87
+ "id": str(doc["_id"]),
88
+ "title": doc.get("title", "Sans titre"),
89
+ "tags": doc.get("tags", []),
90
+ "date": doc.get("upload_date").isoformat() if "upload_date" in doc else None,
91
+ "text_preview": doc.get("text", "")[:100] + "..." if len(doc.get("text", "")) > 100 else doc.get("text", "")
92
+ }
93
+ result.append(doc_safe)
94
+
95
+ return {"documents": result}
96
+ except Exception as e:
97
+ print(f"Erreur lors de la liste des documents: {str(e)}")
98
+ raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}")
99
+
100
+ @router.delete("/knowledge/{document_id}")
101
+ async def delete_document(document_id: str, current_user: dict = Depends(get_admin_user)):
102
+ try:
103
+ try:
104
+ doc_id = ObjectId(document_id)
105
+ except Exception:
106
+ raise HTTPException(status_code=400, detail="ID de document invalide")
107
+
108
+ document = db.connaissances.find_one({"_id": doc_id})
109
+ if not document:
110
+ raise HTTPException(status_code=404, detail="Document non trouvé")
111
+
112
+ result = db.connaissances.delete_one({"_id": doc_id})
113
+
114
+ if result.deleted_count == 0:
115
+ raise HTTPException(status_code=500, detail="Échec de la suppression du document")
116
+
117
+ pdf_path = f"files/{document_id}.pdf"
118
+ if os.path.exists(pdf_path):
119
+ try:
120
+ os.remove(pdf_path)
121
+ print(f"Fichier supprimé: {pdf_path}")
122
+ except Exception as e:
123
+ print(f"Erreur lors de la suppression du fichier: {str(e)}")
124
+
125
+ return {"success": True, "message": "Document supprimé avec succès"}
126
+
127
+ except HTTPException as he:
128
+ raise he
129
+ except Exception as e:
130
+ raise HTTPException(status_code=500, detail=f"Erreur lors de la suppression: {str(e)}")
auth.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, Response, HTTPException, Depends
2
+ from datetime import datetime, timedelta
3
+ from passlib.hash import bcrypt
4
+ import secrets
5
+ from bson.objectid import ObjectId
6
+
7
+ from database import get_db
8
+
9
+ router = APIRouter(prefix="/api", tags=["Authentification"])
10
+
11
+ @router.post("/register")
12
+ async def register(request: Request):
13
+ data = await request.json()
14
+ db = get_db()
15
+
16
+ required_fields = ["prenom", "nom", "email", "password"]
17
+ for field in required_fields:
18
+ if not data.get(field):
19
+ raise HTTPException(status_code=400, detail=f"Le champ {field} est requis")
20
+
21
+ existing_user = db.users.find_one({"email": data["email"]})
22
+ if existing_user:
23
+ raise HTTPException(status_code=409, detail="Cet email est déjà utilisé")
24
+
25
+ hashed_password = bcrypt.hash(data["password"])
26
+
27
+ user = {
28
+ "prenom": data["prenom"],
29
+ "nom": data["nom"],
30
+ "email": data["email"],
31
+ "password": hashed_password,
32
+ "createdAt": datetime.utcnow(),
33
+ "role": data.get("role", "user"),
34
+
35
+ }
36
+
37
+ result = db.users.insert_one(user)
38
+
39
+ return {"message": "Utilisateur créé avec succès", "userId": str(result.inserted_id)}
40
+
41
+ @router.post("/login")
42
+ async def login(request: Request, response: Response):
43
+ try:
44
+ data = await request.json()
45
+ db = get_db()
46
+ email = data.get("email")
47
+ password = data.get("password")
48
+
49
+ user = db.users.find_one({"email": email})
50
+ if not user or not bcrypt.verify(password, user["password"]):
51
+ raise HTTPException(status_code=401, detail="Email ou mot de passe incorrect")
52
+
53
+ session_id = secrets.token_hex(16)
54
+ user_id = str(user["_id"])
55
+ username = f"{user['prenom']} {user['nom']}"
56
+
57
+ db.sessions.insert_one({
58
+ "session_id": session_id,
59
+ "user_id": user_id,
60
+ "created_at": datetime.utcnow(),
61
+ "expires_at": datetime.utcnow() + timedelta(days=7)
62
+ })
63
+
64
+ response.set_cookie(
65
+ key="session_id",
66
+ value=session_id,
67
+ httponly=False,
68
+ max_age=7*24*60*60,
69
+ samesite="none",
70
+ secure=True,
71
+ path="/"
72
+ )
73
+
74
+ print(f"Session : {session_id} pour {user_id}")
75
+
76
+
77
+ return {
78
+ "success": True,
79
+ "username": username,
80
+ "user_id": user_id,
81
+ "session_id": session_id,
82
+ "role": user.get("role", "user")
83
+ }
84
+ except Exception as e:
85
+ print(f"Erreur login: {str(e)}")
86
+ raise HTTPException(status_code=500, detail=str(e))
87
+
88
+ @router.post("/logout")
89
+ async def logout(request: Request, response: Response):
90
+ db = get_db()
91
+ session_id = request.cookies.get("session_id")
92
+
93
+ if session_id:
94
+ db.sessions.delete_one({"session_id": session_id})
95
+
96
+ response.delete_cookie(key="session_id")
97
+ return {"success": True}
98
+
99
+ async def get_current_user(request: Request):
100
+ db = get_db()
101
+ session_id = request.cookies.get("session_id")
102
+
103
+ print(f"Cookie: {session_id[:5] if session_id else 'None'}")
104
+
105
+ if not session_id:
106
+ auth_header = request.headers.get("Authorization")
107
+ if auth_header and auth_header.startswith("Bearer "):
108
+ session_id = auth_header.replace("Bearer ", "")
109
+ print(f"Session reçue: {session_id[:5]}...")
110
+
111
+ if not session_id:
112
+ session_id = request.query_params.get("session_id")
113
+ if session_id:
114
+ print(f"Session des paramètres de requête: {session_id[:5]}...")
115
+
116
+ if not session_id:
117
+ raise HTTPException(status_code=401, detail="Non authentifié - Aucune session trouvée")
118
+
119
+ session = db.sessions.find_one({
120
+ "session_id": session_id,
121
+ "expires_at": {"$gt": datetime.utcnow()}
122
+ })
123
+
124
+ if not session:
125
+ raise HTTPException(status_code=401, detail="Session expirée ou invalide")
126
+
127
+ user = db.users.find_one({"_id": ObjectId(session["user_id"])})
128
+ if not user:
129
+ raise HTTPException(status_code=401, detail="Utilisateur non trouvé")
130
+
131
+ return user
132
+
133
+ async def get_admin_user(request: Request):
134
+ user = await get_current_user(request)
135
+ if user["role"] != "Administrateur":
136
+ raise HTTPException(status_code=403, detail="Droits d'administrateur requis")
137
+ return user
chat.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException, Depends
2
+ from fastapi.responses import JSONResponse
3
+ from datetime import datetime
4
+ from bson.objectid import ObjectId
5
+ from huggingface_hub import InferenceClient
6
+ from sentence_transformers import SentenceTransformer
7
+ from sklearn.metrics.pairwise import cosine_similarity
8
+ import re
9
+
10
+ from auth import get_current_user
11
+ from database import get_db
12
+ from config import HF_TOKEN, MAX_TOKENS, EMBEDDING_MODEL
13
+
14
+ router = APIRouter(prefix="/api", tags=["Chat"])
15
+ db=get_db()
16
+ conversation_history = {}
17
+ hf_client = InferenceClient(token=HF_TOKEN)
18
+
19
+ try:
20
+ from langchain_community.embeddings import HuggingFaceEmbeddings
21
+ embedding_model = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
22
+ print("✅ Modèle d'embedding médical chargé avec succès")
23
+ except Exception as e:
24
+ print(f"❌ Erreur chargement embedding: {str(e)}")
25
+ embedding_model = None
26
+
27
+ # Fonctions de RAG
28
+ def retrieve_relevant_context(query, embedding_model, mongo_collection, k=5):
29
+ query_embedding = embedding_model.embed_query(query)
30
+
31
+ docs = list(mongo_collection.find({}, {"text": 1, "embedding": 1}))
32
+
33
+ print(f"[DEBUG] Recherche de contexte pour: '{query}'")
34
+ print(f"[DEBUG] {len(docs)} documents trouvés dans la base de données")
35
+
36
+ if not docs:
37
+ print("[DEBUG] Aucun document dans la collection. RAG désactivé.")
38
+ return ""
39
+
40
+ similarities = []
41
+ for i, doc in enumerate(docs):
42
+ if "embedding" not in doc or not doc["embedding"]:
43
+ print(f"[DEBUG] Document {i} sans embedding")
44
+ continue
45
+
46
+ sim = cosine_similarity([query_embedding], [doc["embedding"]])[0][0]
47
+ similarities.append((sim, i, doc["text"]))
48
+
49
+ similarities.sort(reverse=True)
50
+
51
+ print("\n=== CONTEXTE SÉLECTIONNÉ ===")
52
+ top_k_docs = []
53
+ for i, (score, idx, text) in enumerate(similarities[:k]):
54
+ doc_preview = text[:100] + "..." if len(text) > 100 else text
55
+ print(f"Document #{i+1} (score: {score:.4f}): {doc_preview}")
56
+ top_k_docs.append(text)
57
+ print("==========================\n")
58
+
59
+ return "\n\n".join(top_k_docs)
60
+
61
+ @router.post("/chat")
62
+ async def chat(request: Request):
63
+ global conversation_history
64
+
65
+ data = await request.json()
66
+ user_message = data.get("message", "").strip()
67
+ conversation_id = data.get("conversation_id")
68
+ skip_save = data.get("skip_save", False)
69
+
70
+
71
+ if not skip_save and conversation_id and current_user:
72
+ db.messages.insert_one({
73
+ "conversation_id": conversation_id,
74
+ "user_id": str(current_user["_id"]),
75
+ "sender": "user",
76
+ "text": user_message,
77
+ "timestamp": datetime.utcnow()
78
+ })
79
+
80
+
81
+
82
+ if not user_message:
83
+ raise HTTPException(status_code=400, detail="Le champ 'message' est requis.")
84
+
85
+ current_user = None
86
+ try:
87
+ current_user = await get_current_user(request)
88
+ except HTTPException:
89
+ pass
90
+
91
+ current_tokens = 0
92
+ message_tokens = 0
93
+ if current_user and conversation_id:
94
+ conv = db.conversations.find_one({
95
+ "_id": ObjectId(conversation_id),
96
+ "user_id": str(current_user["_id"])
97
+ })
98
+ if conv:
99
+ current_tokens = conv.get("token_count", 0)
100
+ message_tokens = int(len(user_message.split()) * 1.3)
101
+ MAX_TOKENS = 2000
102
+ if current_tokens + message_tokens > MAX_TOKENS:
103
+ return JSONResponse({
104
+ "error": "token_limit_exceeded",
105
+ "message": "Cette conversation a atteint sa limite de taille. Veuillez en créer une nouvelle.",
106
+ "tokens_used": current_tokens,
107
+ "tokens_limit": MAX_TOKENS
108
+ }, status_code=403)
109
+
110
+
111
+
112
+ is_history_question = any(
113
+ phrase in user_message.lower()
114
+ for phrase in [
115
+ "ma première question", "ma précédente question", "ma dernière question",
116
+ "ce que j'ai demandé", "j'ai dit quoi", "quelles questions",
117
+ "c'était quoi ma", "quelle était ma", "mes questions", "questions précédentes"
118
+ ]
119
+ ) or re.search(r"(?:quelle|quelles|quoi).*?(\d+)[a-z]{2}.*?question", user_message.lower()) \
120
+ or re.search(r"derni[eè]re question", user_message.lower()) \
121
+ or re.search(r"premi[eè]re question", user_message.lower()) \
122
+ or re.search(r"question pr[eé]c[eé]dente", user_message.lower()) \
123
+ or re.search(r"(toutes|liste|quelles|quoi).*questions", user_message.lower())
124
+
125
+ if conversation_id not in conversation_history:
126
+ conversation_history[conversation_id] = []
127
+ if current_user and conversation_id:
128
+ previous_messages = list(db.messages.find(
129
+ {"conversation_id": conversation_id}
130
+ ).sort("timestamp", 1))
131
+
132
+ for msg in previous_messages:
133
+ if msg["sender"] == "user":
134
+ conversation_history[conversation_id].append(f"Question : {msg['text']}")
135
+ else:
136
+ conversation_history[conversation_id].append(f"Réponse : {msg['text']}")
137
+
138
+ if is_history_question:
139
+ actual_questions = []
140
+
141
+ if conversation_id in conversation_history:
142
+ for msg in conversation_history[conversation_id]:
143
+ if msg.startswith("Question : "):
144
+ q_text = msg.replace("Question : ", "")
145
+ is_meta = any(phrase in q_text.lower() for phrase in [
146
+ "ma première question", "ma précédente question", "ma dernière question",
147
+ "ce que j'ai demandé", "j'ai dit quoi", "quelles questions",
148
+ "c'était quoi ma", "quelle était ma", "mes questions"
149
+ ]) or re.search(r"(?:quelle|quelles|quoi).*?(\d+)[a-z]{2}.*?question", q_text.lower()) \
150
+ or re.search(r"derni[eè]re question", q_text.lower()) \
151
+ or re.search(r"premi[eè]re question", q_text.lower()) \
152
+ or re.search(r"question pr[eé]c[eé]dente", q_text.lower()) \
153
+ or re.search(r"(toutes|liste|quelles|quoi).*questions", q_text.lower())
154
+ if not is_meta:
155
+ actual_questions.append(q_text)
156
+
157
+ if not actual_questions:
158
+ return JSONResponse({
159
+ "response": "Vous n'avez pas encore posé de question dans cette conversation. C'est notre premier échange."
160
+ })
161
+
162
+ if re.search(r"derni[eè]re question", user_message.lower()):
163
+ return JSONResponse({
164
+ "response": f"Votre dernière question était : « {actual_questions[-1]} »"
165
+ })
166
+
167
+ if re.search(r"question pr[eé]c[eé]dente", user_message.lower()):
168
+ if len(actual_questions) >= 2:
169
+ return JSONResponse({
170
+ "response": f"Votre question précédente était : « {actual_questions[-2]} »"
171
+ })
172
+ else:
173
+ return JSONResponse({
174
+ "response": "Il n'y a pas encore de question précédente dans notre conversation."
175
+ })
176
+
177
+ if re.search(r"premi[eè]re question", user_message.lower()) or any(p in user_message.lower() for p in ["première question", "1ère question", "1ere question"]):
178
+ return JSONResponse({
179
+ "response": f"Votre première question était : « {actual_questions[0]} »"
180
+ })
181
+
182
+ match_nth = re.search(r"(?:quelle|quelles|quoi).*?(\d+)[a-z]{2}.*?question", user_message.lower())
183
+ if match_nth:
184
+ try:
185
+ question_number = int(match_nth.group(1))
186
+ if 0 < question_number <= len(actual_questions):
187
+ return JSONResponse({
188
+ "response": f"Votre {question_number}{'ère' if question_number == 1 else 'ème'} question était : « {actual_questions[question_number-1]} »"
189
+ })
190
+ else:
191
+ return JSONResponse({
192
+ "response": f"Vous n'avez pas encore posé {question_number} questions dans cette conversation."
193
+ })
194
+ except:
195
+ pass
196
+
197
+ question_number = None
198
+ if any(p in user_message.lower() for p in ["deuxième question", "2ème question", "2eme question", "seconde question"]):
199
+ question_number = 2
200
+ else:
201
+ match = re.search(r'(\d+)[eèiéê]*m*e* question', user_message.lower())
202
+ if match:
203
+ try:
204
+ question_number = int(match.group(1))
205
+ except:
206
+ pass
207
+
208
+ if question_number is not None:
209
+ if 0 < question_number <= len(actual_questions):
210
+ suffix = "ère" if question_number == 1 else "ème"
211
+ return JSONResponse({
212
+ "response": f"Votre {question_number}{suffix} question était : « {actual_questions[question_number-1]} »"
213
+ })
214
+ else:
215
+ return JSONResponse({
216
+ "response": f"Vous n'avez pas encore posé {question_number} questions dans cette conversation."
217
+ })
218
+
219
+ if len(actual_questions) == 1:
220
+ return JSONResponse({
221
+ "response": f"Vous avez posé une seule question jusqu'à présent : « {actual_questions[0]} »"
222
+ })
223
+ else:
224
+ question_list = "\n".join([f"{i+1}. {q}" for i, q in enumerate(actual_questions)])
225
+ return JSONResponse({
226
+ "response": f"Voici les questions que vous avez posées dans cette conversation :\n\n{question_list}"
227
+ })
228
+
229
+ context = None
230
+ if not is_history_question and embedding_model:
231
+ context = retrieve_relevant_context(user_message, embedding_model, db.connaissances, k=5)
232
+ if context and conversation_id:
233
+ conversation_history[conversation_id].append(f"Contexte : {context}")
234
+
235
+ if conversation_id:
236
+ conversation_history[conversation_id].append(f"Question : {user_message}")
237
+
238
+ system_prompt = (
239
+ "Tu es un chatbot spécialisé dans la santé mentale, et plus particulièrement la schizophrénie. "
240
+ "Tu réponds de façon fiable, claire et empathique, en t'appuyant uniquement sur des sources médicales et en français. "
241
+ "IMPORTANT: Fais particulièrement attention aux questions de suivi. Si l'utilisateur pose une question qui ne précise "
242
+ "pas clairement le sujet mais qui fait suite à votre échange précédent, comprends que cette question fait référence "
243
+ "au contexte de la conversation précédente. Par exemple, si l'utilisateur demande 'Comment les traite-t-on?' après "
244
+ "avoir parlé des symptômes positifs de la schizophrénie, ta réponse doit porter spécifiquement sur le traitement "
245
+ "des symptômes positifs, et non sur la schizophrénie en général.IMPORTANT: Vise tes réponses sous forme de Markdown."
246
+ )
247
+
248
+ enriched_context = ""
249
+
250
+ if conversation_id in conversation_history:
251
+ actual_questions = []
252
+ for msg in conversation_history[conversation_id]:
253
+ if msg.startswith("Question : "):
254
+ q_text = msg.replace("Question : ", "")
255
+ is_meta = any(phrase in q_text.lower() for phrase in [
256
+ "ma première question", "ma précédente question", "ma dernière question",
257
+ "ce que j'ai demandé", "j'ai dit quoi", "quelles questions",
258
+ "c'était quoi ma", "quelle était ma", "mes questions"
259
+ ]) or re.search(r"(?:quelle|quelles|quoi).*?(\d+)[a-z]{2}.*?question", q_text.lower()) \
260
+ or re.search(r"derni[eè]re question", q_text.lower()) \
261
+ or re.search(r"premi[eè]re question", q_text.lower()) \
262
+ or re.search(r"question pr[eé]c[eé]dente", q_text.lower()) \
263
+ or re.search(r"(toutes|liste|quelles|quoi).*questions", q_text.lower())
264
+ if not is_meta and q_text != user_message:
265
+ actual_questions.append(q_text)
266
+
267
+ if actual_questions:
268
+ recent_questions = actual_questions[-5:]
269
+ enriched_context += "Historique récent des questions:\n"
270
+ for i, q in enumerate(recent_questions):
271
+ enriched_context += f"- Question précédente {len(recent_questions)-i}: {q}\n"
272
+ enriched_context += "\n"
273
+
274
+ if context:
275
+ enriched_context += "Contexte médical pertinent:\n"
276
+ enriched_context += context
277
+ enriched_context += "\n\n"
278
+
279
+ if enriched_context:
280
+ system_prompt += (
281
+ f"\n\n{enriched_context}\n\n"
282
+ "Utilise ces informations pour répondre de manière plus précise et contextuelle. "
283
+ "Ne pas inventer d'informations. Si tu ne sais pas, redirige vers un professionnel de santé. "
284
+ "Tu dois donner une réponse complète, bien structurée et ne jamais couper ta réponse brutalement. "
285
+ "Si tu n'as pas assez de place pour finir, indique-le clairement à l'utilisateur."
286
+ )
287
+ else:
288
+ system_prompt += (
289
+ "Tu dois répondre uniquement à partir de connaissances médicales factuelles. "
290
+ "Si tu ne sais pas répondre, indique-le clairement et suggère de consulter un professionnel de santé. "
291
+ "Tu dois donner une réponse complète et bien structurée."
292
+ )
293
+
294
+ messages = [{"role": "system", "content": system_prompt}]
295
+
296
+ if conversation_id and len(conversation_history.get(conversation_id, [])) > 0:
297
+ history = conversation_history[conversation_id]
298
+ for i in range(0, min(20, len(history)-1), 2):
299
+ if i+1 < len(history):
300
+ if history[i].startswith("Question :"):
301
+ user_text = history[i].replace("Question : ", "")
302
+ messages.append({"role": "user", "content": user_text})
303
+
304
+ if history[i+1].startswith("Réponse :"):
305
+ assistant_text = history[i+1].replace("Réponse : ", "")
306
+ messages.append({"role": "assistant", "content": assistant_text})
307
+
308
+ messages.append({"role": "user", "content": user_message})
309
+
310
+ try:
311
+ completion = hf_client.chat.completions.create(
312
+ model="mistralai/Mistral-Small-24B-Instruct-2501",
313
+ messages=messages,
314
+ max_tokens=1024,
315
+ temperature=0.7,
316
+ timeout=15,
317
+ )
318
+ bot_response = completion.choices[0].message["content"].strip()
319
+ if bot_response.endswith((".", "!", "?")) == False and len(bot_response) > 500:
320
+ bot_response += "\n\n(Note: Ma réponse a été limitée par des contraintes de taille. N'hésitez pas à me demander de poursuivre si vous souhaitez plus d'informations.)"
321
+ except Exception:
322
+ try:
323
+ fallback = hf_client.text_generation(
324
+ model="mistralai/Mistral-7B-Instruct-v0.3",
325
+ prompt=f"<s>[INST] {system_prompt}\n\nQuestion: {user_message} [/INST]",
326
+ max_new_tokens=512,
327
+ temperature=0.7
328
+ )
329
+ bot_response = fallback
330
+ except Exception:
331
+ bot_response = "Je suis désolé, je rencontre actuellement des difficultés techniques. Pourriez-vous reformuler votre question ou réessayer dans quelques instants?"
332
+
333
+ if conversation_id:
334
+ conversation_history[conversation_id].append(f"Réponse : {bot_response}")
335
+
336
+ if len(conversation_history[conversation_id]) > 50:
337
+ conversation_history[conversation_id] = conversation_history[conversation_id][-50:]
338
+
339
+ if not skip_save and conversation_id and current_user:
340
+ db.messages.insert_one({
341
+ "conversation_id": conversation_id,
342
+ "user_id": str(current_user["_id"]),
343
+ "sender": "bot",
344
+ "text": bot_response,
345
+ "timestamp": datetime.utcnow()
346
+ })
347
+
348
+ if conversation_id and current_user:
349
+ db.messages.insert_one({
350
+ "conversation_id": conversation_id,
351
+ "user_id": str(current_user["_id"]),
352
+ "sender": "bot",
353
+ "text": bot_response,
354
+ "timestamp": datetime.utcnow()
355
+ })
356
+ response_tokens = int(len(bot_response.split()) * 1.3)
357
+ total_tokens = current_tokens + message_tokens + response_tokens
358
+ db.conversations.update_one(
359
+ {"_id": ObjectId(conversation_id)},
360
+ {"$set": {
361
+ "last_message": bot_response,
362
+ "updated_at": datetime.utcnow(),
363
+ "token_count": total_tokens
364
+ }}
365
+ )
366
+
367
+ return {"response": bot_response}
conversations.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException, Depends
2
+ from datetime import datetime
3
+ from bson.objectid import ObjectId
4
+
5
+ from auth import get_current_user
6
+ from database import get_db
7
+
8
+ router = APIRouter(prefix="/api", tags=["Conversations"])
9
+ db = get_db()
10
+
11
+ @router.get("/conversations")
12
+ async def get_conversations(current_user: dict = Depends(get_current_user)):
13
+ try:
14
+ user_id = str(current_user["_id"])
15
+
16
+ conversations = list(db.conversations.find(
17
+ {"user_id": user_id},
18
+ {"_id": 1, "title": 1, "date": 1, "time": 1, "last_message": 1, "created_at": 1}
19
+ ).sort("created_at", -1))
20
+
21
+ for conv in conversations:
22
+ conv["_id"] = str(conv["_id"])
23
+
24
+ return {"conversations": conversations}
25
+ except Exception as e:
26
+ raise HTTPException(status_code=500, detail=f"Erreur serveur: {str(e)}")
27
+
28
+ @router.post("/conversations")
29
+ async def create_conversation(request: Request, current_user: dict = Depends(get_current_user)):
30
+ try:
31
+ data = await request.json()
32
+ user_id = str(current_user["_id"])
33
+
34
+ conversation = {
35
+ "user_id": user_id,
36
+ "title": data.get("title", "Nouvelle conversation"),
37
+ "date": data.get("date"),
38
+ "time": data.get("time"),
39
+ "last_message": data.get("message", ""),
40
+ "created_at": datetime.utcnow()
41
+ }
42
+
43
+ result = db.conversations.insert_one(conversation)
44
+
45
+ return {"conversation_id": str(result.inserted_id)}
46
+ except Exception as e:
47
+ raise HTTPException(status_code=500, detail=f"Erreur serveur: {str(e)}")
48
+
49
+ @router.post("/conversations/{conversation_id}/messages")
50
+ async def add_message(conversation_id: str, request: Request, current_user: dict = Depends(get_current_user)):
51
+ try:
52
+ data = await request.json()
53
+ user_id = str(current_user["_id"])
54
+
55
+ print(f"Ajout message: conversation_id={conversation_id}, sender={data.get('sender')}, text={data.get('text')[:20]}...")
56
+
57
+ conversation = db.conversations.find_one({
58
+ "_id": ObjectId(conversation_id),
59
+ "user_id": user_id
60
+ })
61
+
62
+ if not conversation:
63
+ raise HTTPException(status_code=404, detail="Conversation non trouvée")
64
+
65
+ message = {
66
+ "conversation_id": conversation_id,
67
+ "user_id": user_id,
68
+ "sender": data.get("sender", "user"),
69
+ "text": data.get("text", ""),
70
+ "timestamp": datetime.utcnow()
71
+ }
72
+
73
+ db.messages.insert_one(message)
74
+
75
+ db.conversations.update_one(
76
+ {"_id": ObjectId(conversation_id)},
77
+ {"$set": {"last_message": data.get("text", ""), "updated_at": datetime.utcnow()}}
78
+ )
79
+
80
+ return {"success": True}
81
+ except Exception as e:
82
+ print(f"Erreur lors de l'ajout d'un message: {str(e)}")
83
+ raise HTTPException(status_code=500, detail=f"Erreur serveur: {str(e)}")
84
+ @router.get("/conversations/{conversation_id}/messages")
85
+ async def get_messages(conversation_id: str, current_user: dict = Depends(get_current_user)):
86
+ try:
87
+ user_id = str(current_user["_id"])
88
+
89
+ conversation = db.conversations.find_one({
90
+ "_id": ObjectId(conversation_id),
91
+ "user_id": user_id
92
+ })
93
+
94
+ if not conversation:
95
+ raise HTTPException(status_code=404, detail="Conversation non trouvée")
96
+
97
+ messages = list(db.messages.find(
98
+ {"conversation_id": conversation_id}
99
+ ).sort("timestamp", 1))
100
+
101
+ deduplicated_messages = []
102
+ seen_texts = set()
103
+
104
+ for msg in messages:
105
+ msg["_id"] = str(msg["_id"])
106
+
107
+ if "timestamp" in msg:
108
+ msg["timestamp"] = msg["timestamp"].isoformat()
109
+
110
+ timestamp_rounded = msg.get("timestamp", "")[:19]
111
+ dedup_key = f"{msg['sender']}:{msg['text'][:50]}:{timestamp_rounded}"
112
+
113
+ if dedup_key not in seen_texts:
114
+ seen_texts.add(dedup_key)
115
+ deduplicated_messages.append(msg)
116
+
117
+ if msg["sender"] == "assistant" and deduplicated_messages and deduplicated_messages[-1]["sender"] == "bot":
118
+ if deduplicated_messages[-1]["text"] == msg["text"]:
119
+ deduplicated_messages.pop()
120
+
121
+ return {"messages": deduplicated_messages}
122
+ except Exception as e:
123
+ raise HTTPException(status_code=500, detail=f"Erreur serveur: {str(e)}")
124
+
125
+ @router.delete("/conversations/{conversation_id}")
126
+ async def delete_conversation(conversation_id: str, current_user: dict = Depends(get_current_user)):
127
+ try:
128
+ user_id = str(current_user["_id"])
129
+
130
+ result = db.conversations.delete_one({
131
+ "_id": ObjectId(conversation_id),
132
+ "user_id": user_id
133
+ })
134
+
135
+ if result.deleted_count == 0:
136
+ raise HTTPException(status_code=404, detail="Conversation non trouvée")
137
+
138
+ db.messages.delete_many({"conversation_id": conversation_id})
139
+
140
+ return {"success": True}
141
+ except Exception as e:
142
+ raise HTTPException(status_code=500, detail=f"Erreur serveur: {str(e)}")
database.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import MongoClient
2
+ import os
3
+ from config import MONGODB_URI, DB_NAME, SAVE_FOLDER
4
+
5
+ mongo_client = None
6
+ db = None
7
+
8
+ def init_mongodb():
9
+ """Initialise la connexion à MongoDB."""
10
+ global mongo_client, db
11
+
12
+ mongo_client = MongoClient(MONGODB_URI)
13
+ db = mongo_client[DB_NAME]
14
+
15
+ os.makedirs(SAVE_FOLDER, exist_ok=True)
16
+
17
+ doc_count = db.connaissances.count_documents({})
18
+ print(f"\n[DIAGNOSTIC] Collection 'connaissances': {doc_count} documents trouvés")
19
+
20
+ if doc_count == 0:
21
+ print("[AVERTISSEMENT] La collection est vide. Le système RAG ne fonctionnera pas!")
22
+ else:
23
+ sample_doc = db.connaissances.find_one({})
24
+ has_embeddings = "embedding" in sample_doc and sample_doc["embedding"] is not None
25
+ print(f"[DIAGNOSTIC] Les documents ont des embeddings: {'✅ Oui' if has_embeddings else '❌ Non'}")
26
+
27
+ return db
28
+
29
+ def get_db():
30
+ """Récupère l'instance de la base de données."""
31
+ global db
32
+ if db is None:
33
+ return init_mongodb()
34
+ return db
init_documents.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ from urllib.request import Request, urlopen
4
+ from urllib.error import HTTPError, URLError
5
+ from pymongo import MongoClient
6
+ from langchain_community.document_loaders import PyPDFDirectoryLoader
7
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
8
+ from langchain_community.embeddings import HuggingFaceEmbeddings
9
+
10
+ from config import MONGODB_URI, DB_NAME, SAVE_FOLDER
11
+
12
+ PDF_URLS = [
13
+
14
+ ]
15
+
16
+ COLLECTION_NAME = "connaissances"
17
+
18
+ def download_pdf(url, save_path, retries=2, delay=3):
19
+ """Télécharge un PDF depuis une URL avec gestion des erreurs."""
20
+ for attempt in range(retries):
21
+ try:
22
+ req = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
23
+ with urlopen(req) as response, open(save_path, 'wb') as f:
24
+ f.write(response.read())
25
+ print(f"Téléchargé : {save_path}")
26
+ return
27
+ except (HTTPError, URLError) as e:
28
+ print(f"Erreur ({e}) pour {url}, tentative {attempt+1}/{retries}")
29
+ time.sleep(delay)
30
+ print(f"Échec du téléchargement : {url}")
31
+ '''
32
+ def init_documents():
33
+ """Initialise les documents dans la base de données avec leurs embeddings."""
34
+ os.makedirs(SAVE_FOLDER, exist_ok=True)
35
+
36
+
37
+ for url in PDF_URLS:
38
+ file_name = url.split("/")[-1]
39
+ file_path = os.path.join(SAVE_FOLDER, file_name)
40
+ if not os.path.exists(file_path):
41
+ download_pdf(url, file_path)
42
+
43
+ print("Chargement des PDFs...")
44
+ loader = PyPDFDirectoryLoader(SAVE_FOLDER)
45
+ docs = loader.load()
46
+
47
+ print("Découpage des documents...")
48
+ splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
49
+ chunks = splitter.split_documents(docs)
50
+ print(f"{len(chunks)} morceaux extraits.")
51
+
52
+ print("Initialisation du modèle d'embeddings...")
53
+ embedding_model = HuggingFaceEmbeddings(model_name="shtilev/medical_embedded_v2")
54
+
55
+ print("Connexion à MongoDB...")
56
+ client = MongoClient(MONGODB_URI)
57
+ collection = client[DB_NAME][COLLECTION_NAME]
58
+
59
+ confirm = input("Cette opération supprimera toutes les données existantes. Continuer? (o/n): ")
60
+ if confirm.lower() != 'o':
61
+ print("Opération annulée.")
62
+ return
63
+
64
+ print("Suppression des documents existants...")
65
+ collection.delete_many({})
66
+
67
+ print("Génération des embeddings et insertion dans la base de données...")
68
+ for i, chunk in enumerate(chunks):
69
+ text = chunk.page_content
70
+ print(f"Traitement du morceau {i+1}/{len(chunks)}")
71
+ embedding = embedding_model.embed_query(text)
72
+ collection.insert_one({
73
+ "text": text,
74
+ "embedding": embedding
75
+ })
76
+
77
+ print("Tous les embeddings ont été insérés dans la base MongoDB.")
78
+
79
+ if __name__ == "__main__":
80
+ init_documents()
81
+ '''
utils.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ def simulate_token_count(text):
4
+ """
5
+ simulation token
6
+ """
7
+ if not text:
8
+ return 0
9
+
10
+ text = text.replace('\n', ' \n ')
11
+
12
+ spaces_and_punct = sum(1 for c in text if c.isspace() or c in ',.;:!?()[]{}"\'`-_=+<>/@#$%^&*|\\')
13
+
14
+ digits = sum(1 for c in text if c.isdigit())
15
+
16
+ words = text.split()
17
+ short_words = sum(1 for w in words if len(w) <= 2)
18
+
19
+ code_blocks = len(re.findall(r'```[\s\S]*?```', text))
20
+ urls = len(re.findall(r'https?://\S+', text))
21
+
22
+ adjusted_length = len(text) - spaces_and_punct - digits - short_words
23
+
24
+ token_count = (
25
+ adjusted_length / 4 +
26
+ spaces_and_punct * 0.25 +
27
+ digits * 0.5 +
28
+ short_words * 0.5 +
29
+ code_blocks * 5 +
30
+ urls * 4
31
+ )
32
+
33
+ return int(token_count * 1.1) + 1