.env.example ADDED
File without changes
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .env
2
+ notebooks
3
+ venv
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "remote.autoForwardPortsFallback": 0
3
+ }
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Utiliser une image Python légère
2
+ FROM python:3.10-slim
3
+
4
+ # 2. Définir le répertoire de travail
5
+ WORKDIR /app
6
+
7
+ # 3. Installer les dépendances système (nécessaires pour psycopg2 et l'audio)
8
+ RUN apt-get update && apt-get install -y \
9
+ build-essential \
10
+ libpq-dev \
11
+ ffmpeg \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # 4. Créer un utilisateur non-root (sécurité exigée par Hugging Face)
15
+ RUN useradd -m -u 1000 user
16
+ USER user
17
+ ENV HOME=/home/user \
18
+ PATH=/home/user/.local/bin:$PATH
19
+
20
+ WORKDIR $HOME/app
21
+
22
+ # 5. Copier et installer les dépendances Python
23
+ COPY --chown=user requirements.txt .
24
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
25
+
26
+ # 6. Copier tout le reste du code
27
+ COPY --chown=user . .
28
+
29
+ # 7. Exposer le port 7860 (Port par défaut de Hugging Face Spaces)
30
+ EXPOSE 7860
31
+
32
+ # 8. Lancer l'application avec Uvicorn
33
+ CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
__pycache__/main.cpython-312.pyc ADDED
Binary file (6.8 kB). View file
 
api.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from typing import Any
5
+ from tools.transcription import transcribe_audio
6
+ from fastapi import File, UploadFile, Form
7
+ import os
8
+ import shutil
9
+ import uvicorn
10
+
11
+ # On importe graphe LangGraph depuis main.py
12
+ from main import app as langgraph_app
13
+
14
+ # Initialisation de l'API
15
+ app = FastAPI(
16
+ title="Nova Health API",
17
+ description="API d'orientation médicale propulsée par l'IA"
18
+ )
19
+
20
+ # --- CONFIGURATION CORS ---
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ @app.get("/")
30
+ async def accueil():
31
+ """Route d'accueil simple pour vérifier que l'API tourne."""
32
+ return {
33
+ "nom_projet": "Nova Health API",
34
+ "statut": "En ligne ",
35
+ "message": "Bienvenue ! Le système d'orientation médicale IA est opérationnel.",
36
+ "documentation": "Allez sur /docs pour tester l'API avec Swagger UI."
37
+ }
38
+
39
+ # --- MODÈLES DE DONNÉES (JSON) ---
40
+
41
+ # Ce que le frontend doit t'envoyer
42
+ class ChatRequest(BaseModel):
43
+ user_message: str
44
+ user_lat: float
45
+ user_lon: float
46
+
47
+ # front
48
+ class ChatResponse(BaseModel):
49
+ intention: str
50
+ mot_cle: str
51
+ reponse_texte: Any
52
+
53
+ # --- LA ROUTE DE L'API ---
54
+
55
+ @app.post("/api/chat", response_model=ChatResponse)
56
+ async def chat_endpoint(request: ChatRequest):
57
+ """
58
+ Reçoit le message et les coordonnées GPS du patient,
59
+ et renvoie l'orientation médicale formatée en JSON.
60
+
61
+ """
62
+ try:
63
+ # On prépare les données pour ton routeur LangGraph
64
+ inputs = {
65
+ "user_message": request.user_message,
66
+ "user_lat": request.user_lat,
67
+ "user_lon": request.user_lon
68
+ }
69
+
70
+ # On fait réfléchir l'IA
71
+ result = langgraph_app.invoke(inputs)
72
+
73
+ # On renvoie le résultat au format JSON propre
74
+ return ChatResponse(
75
+ intention=result.get("intention", "INCONNU"),
76
+ mot_cle=result.get("mot_cle", ""),
77
+ reponse_texte=result.get("final_response", "Désolé, une erreur est survenue lors de la recherche.")
78
+ )
79
+
80
+ except Exception as e:
81
+ # S'il y a un crash, on renvoie une erreur 500 propre au frontend
82
+ raise HTTPException(status_code=500, detail=str(e))
83
+
84
+
85
+ @app.post("/api/audio", response_model=ChatResponse)
86
+ async def audio_endpoint(
87
+ user_lat: float = Form(...),
88
+ user_lon: float = Form(...),
89
+ file: UploadFile = File(...)
90
+ ):
91
+ temp_file_path = f"temp_{file.filename}"
92
+
93
+ try:
94
+ # 1. Sauvegarde
95
+ with open(temp_file_path, "wb") as buffer:
96
+ shutil.copyfileobj(file.file, buffer)
97
+
98
+ # 2. Transcription
99
+ texte_transcrit = transcribe_audio(temp_file_path)
100
+ print(f"[VOCAL] Message transcrit : {texte_transcrit}")
101
+
102
+ # 3. Appel LangGraph
103
+ print("[INFO] : Lancement du Graph")
104
+ inputs = {
105
+ "user_message": str(texte_transcrit),
106
+ "user_lat": float(user_lat),
107
+ "user_lon": float(user_lon)
108
+ }
109
+
110
+ result = langgraph_app.invoke(inputs)
111
+
112
+ # 4. RETOUR UNIFIÉ
113
+ return ChatResponse(
114
+ intention=result.get("intention", "INCONNU"),
115
+ mot_cle=result.get("mot_cle", ""),
116
+ reponse_texte=result.get("final_response", "Désolé, une erreur est survenue lors de la recherche.")
117
+ )
118
+
119
+ except Exception as e:
120
+ print(f"Erreur Audio : {e}")
121
+ raise HTTPException(status_code=500, detail=f"Erreur : {str(e)}")
122
+
123
+ finally:
124
+ if os.path.exists(temp_file_path):
125
+ os.remove(temp_file_path)
data/__init__.py ADDED
File without changes
data/finetuning_dataset.jsonl ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"timestamp": "2026-03-25T17:35:11.044811", "messages": [{"role": "user", "content": "Je recherche une pharmacie ?"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
2
+ {"timestamp": "2026-03-25T17:36:22.184748", "messages": [{"role": "user", "content": "j'ai tres mal au pieds "}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"orthopédiste\"}"}]}
3
+ {"timestamp": "2026-03-25T17:40:15.984261", "messages": [{"role": "user", "content": "j'ai tres mal au pieds "}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"orthopédiste\"}"}]}
4
+ {"timestamp": "2026-03-25T17:53:53.209742", "messages": [{"role": "user", "content": "Salut"}, {"role": "assistant", "content": "{\"categorie\": \"INCONNU\", \"mot_cle\": \"\"}"}]}
5
+ {"timestamp": "2026-03-25T17:54:17.423431", "messages": [{"role": "user", "content": "Comment tu vas !?"}, {"role": "assistant", "content": "{\"categorie\": \"INCONNU\", \"mot_cle\": \"\"}"}]}
6
+ {"timestamp": "2026-03-25T17:57:21.096039", "messages": [{"role": "user", "content": "J'ai mal a la tête !?"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"neurologue\"}"}]}
7
+ {"timestamp": "2026-03-25T18:12:28.671667", "messages": [{"role": "user", "content": "je recherche un pédiatre !?"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatre\"}"}]}
8
+ {"timestamp": "2026-03-25T18:19:47.991063", "messages": [{"role": "user", "content": "je recherche un pédiatre !?"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatre\"}"}]}
9
+ {"timestamp": "2026-03-25T18:20:21.362024", "messages": [{"role": "user", "content": "je recherche une pédiatrie !?"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatrie\"}"}]}
10
+ {"timestamp": "2026-03-25T18:20:48.677926", "messages": [{"role": "user", "content": "je recherche une pédiatre !?"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatre\"}"}]}
11
+ {"timestamp": "2026-03-25T18:24:15.915410", "messages": [{"role": "user", "content": "où est ce que je peux trouver un pédiatre"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatrie\"}"}]}
12
+ {"timestamp": "2026-03-25T18:24:35.214389", "messages": [{"role": "user", "content": "où est ce que je peux trouver un pédiatre"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatrie\"}"}]}
13
+ {"timestamp": "2026-03-25T23:29:07.900516", "messages": [{"role": "user", "content": "j'ai tres mal au pieds "}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"podiatrie\"}"}]}
14
+ {"timestamp": "2026-03-25T23:29:25.543983", "messages": [{"role": "user", "content": "j'ai tres mal au pieds "}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"podiatrie\"}"}]}
15
+ {"timestamp": "2026-03-26T01:18:38.962149", "messages": [{"role": "user", "content": "je recherche un pediatre"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatrie\"}"}]}
16
+ {"timestamp": "2026-03-26T01:19:26.148374", "messages": [{"role": "user", "content": "je recherche un spécialiste pour soigner mes enfants"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"pédiatrie\"}"}]}
17
+ {"timestamp": "2026-03-26T01:22:32.351642", "messages": [{"role": "user", "content": "je recherche un medecin generaliste"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
18
+ {"timestamp": "2026-03-26T01:29:28.086462", "messages": [{"role": "user", "content": "je recherche un cardiologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
19
+ {"timestamp": "2026-03-26T01:39:39.098727", "messages": [{"role": "user", "content": "je recherche un cardiologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
20
+ {"timestamp": "2026-03-26T01:40:24.371453", "messages": [{"role": "user", "content": "je recherche un cardiologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
21
+ {"timestamp": "2026-03-26T01:42:07.794359", "messages": [{"role": "user", "content": "je recherche un cardiologu"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
22
+ {"timestamp": "2026-03-26T01:43:41.254338", "messages": [{"role": "user", "content": "je recherche un cardiologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
23
+ {"timestamp": "2026-03-26T01:45:16.333679", "messages": [{"role": "user", "content": "je recherche un cardiologu"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
24
+ {"timestamp": "2026-03-26T01:49:07.889329", "messages": [{"role": "user", "content": "je recherche un cardiologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
25
+ {"timestamp": "2026-03-26T01:59:01.586599", "messages": [{"role": "user", "content": "je recherche un cardiologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"cardiologie\"}"}]}
26
+ {"timestamp": "2026-03-26T01:59:43.935602", "messages": [{"role": "user", "content": "je recherche une pharmacies"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
27
+ {"timestamp": "2026-03-26T02:02:15.051402", "messages": [{"role": "user", "content": "je recherche pharmacie de garde"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
28
+ {"timestamp": "2026-03-26T02:03:27.105631", "messages": [{"role": "user", "content": "Saluut comment tu vas !?"}, {"role": "assistant", "content": "{\"categorie\": \"INCONNU\", \"mot_cle\": \"\"}"}]}
29
+ {"timestamp": "2026-03-26T02:08:59.793440", "messages": [{"role": "user", "content": "Je recherches les pharmacie de garde"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
30
+ {"timestamp": "2026-03-26T02:09:49.780263", "messages": [{"role": "user", "content": "Je recherches les pharmacie de garde"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
31
+ {"timestamp": "2026-03-26T02:14:04.479515", "messages": [{"role": "user", "content": "Je recherches les pharmacie de garde"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
32
+ {"timestamp": "2026-03-26T02:19:51.996416", "messages": [{"role": "user", "content": "je fais de la fievre et j 'ai besoin de voir un medecin"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
33
+ {"timestamp": "2026-03-26T02:21:04.691359", "messages": [{"role": "user", "content": "je fais de la fievre et j ai besoin de voir un medecin"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
34
+ {"timestamp": "2026-03-26T02:24:50.052640", "messages": [{"role": "user", "content": "je fais de la fievre et j ai besoin de voir un medecin"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
35
+ {"timestamp": "2026-03-26T02:35:29.689165", "messages": [{"role": "user", "content": "je fais de la fievre et j ai besoin de voir un medecin"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
36
+ {"timestamp": "2026-03-26T02:37:30.868528", "messages": [{"role": "user", "content": "je fais de la fievre et j ai besoin de voir un medecin"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
37
+ {"timestamp": "2026-03-26T03:01:56.188466", "messages": [{"role": "user", "content": "je fais de la fievre et j ai besoin de voir un medecin"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
38
+ {"timestamp": "2026-03-26T03:03:05.646253", "messages": [{"role": "user", "content": "je veux aller a la pharmacie"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
39
+ {"timestamp": "2026-03-26T03:03:43.558212", "messages": [{"role": "user", "content": "je cheche un gynecologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"maternité\"}"}]}
40
+ {"timestamp": "2026-03-26T03:04:14.888236", "messages": [{"role": "user", "content": "je fais de la fievre"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
41
+ {"timestamp": "2026-03-26T03:11:03.225954", "messages": [{"role": "user", "content": "je fais de la fievre"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
42
+ {"timestamp": "2026-03-26T03:14:57.801055", "messages": [{"role": "user", "content": "je fais de la fievre"}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
43
+ {"timestamp": "2026-03-26T03:15:35.628768", "messages": [{"role": "user", "content": "je recherche un sexologue"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"sexologie\"}"}]}
44
+ {"timestamp": "2026-03-26T03:19:13.296489", "messages": [{"role": "user", "content": "ma femme enceinte de 8 mois a des douleurs"}, {"role": "assistant", "content": "{\"categorie\": \"SPECIALISTE\", \"mot_cle\": \"maternité\"}"}]}
45
+ {"timestamp": "2026-03-26T03:38:52.846270", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
46
+ {"timestamp": "2026-03-26T03:39:18.871398", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
47
+ {"timestamp": "2026-03-26T03:42:03.520544", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
48
+ {"timestamp": "2026-03-26T03:42:23.167446", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
49
+ {"timestamp": "2026-03-26T03:51:15.969948", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci"}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
50
+ {"timestamp": "2026-03-26T03:52:03.999730", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
51
+ {"timestamp": "2026-03-26T03:55:55.213240", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
52
+ {"timestamp": "2026-03-26T03:56:13.138699", "messages": [{"role": "user", "content": " Bonsoir, je fais de la fièvre et j'ai besoin d'aide."}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
53
+ {"timestamp": "2026-03-26T04:01:10.919057", "messages": [{"role": "user", "content": " Bonsoir, je fais de la fièvre et j'ai besoin d'aide."}, {"role": "assistant", "content": "{\"categorie\": \"ORIENTATION\", \"mot_cle\": \"\"}"}]}
54
+ {"timestamp": "2026-03-26T04:01:39.094206", "messages": [{"role": "user", "content": " Bonsoir, je recherche une pharmacie de garde proche de moi. Merci."}, {"role": "assistant", "content": "{\"categorie\": \"PHARMACIE\", \"mot_cle\": \"\"}"}]}
data/pharmacies_cache.json ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "nom": "PHARMACIE LA NOUVELLE TULIPE",
4
+ "quartier": "LOME",
5
+ "contacts": "99 47 00 70",
6
+ "latitude": "6.2608333",
7
+ "longitude": "1.16075",
8
+ "map_link": "https://maps.google.com/?q=6.2608333,1.16075",
9
+ "adresse": "Rte de Mission-Tové, près de la Station CAP Agoè-légbassito"
10
+ },
11
+ {
12
+ "nom": "PHARMACIE LA BARAKA",
13
+ "quartier": "AGOE",
14
+ "contacts": "90 17 49 28 / 70 41 44 13",
15
+ "latitude": "6.2421387",
16
+ "longitude": "1.1664511",
17
+ "map_link": "https://maps.google.com/?q=6.2421387,1.1664511",
18
+ "adresse": "Agoe Logopé, non loin de l'Ecoke la BRUYERE à proximité du camp GP"
19
+ },
20
+ {
21
+ "nom": "PHARMACIE DIVINA GRACIA",
22
+ "quartier": "AGOE",
23
+ "contacts": "93 83 91 00 / 96 80 10 21",
24
+ "latitude": "6.2327797",
25
+ "longitude": "1.1868497",
26
+ "map_link": "https://maps.google.com/?q=6.2327797,1.1868497",
27
+ "adresse": "Agoè fiovi, rond point Cool Catche"
28
+ },
29
+ {
30
+ "nom": "PHARMACIE TCHEP'SON",
31
+ "quartier": "AGOE",
32
+ "contacts": "96 90 04 64 / 70 42 94 41",
33
+ "latitude": "6.2590019",
34
+ "longitude": "1.1991617",
35
+ "map_link": "https://maps.google.com/?q=6.2590019,1.1991617",
36
+ "adresse": "Face terminal du Sahel, Agoè zongo"
37
+ },
38
+ {
39
+ "nom": "PHARMACIE GRATITUDE",
40
+ "quartier": "Agoè Légbassito",
41
+ "contacts": "92 18 94 85",
42
+ "latitude": "6.2646808",
43
+ "longitude": "1.133599",
44
+ "map_link": "https://maps.google.com/?q=6.2646808,1.133599",
45
+ "adresse": "Grand contournement non loin du carrefour avinato à 100m de la clinique OASIS "
46
+ },
47
+ {
48
+ "nom": "PHARMACIE LA MAIN DE DIEU",
49
+ "quartier": "AGOE",
50
+ "contacts": "22 52 77 55",
51
+ "latitude": "6.2349799",
52
+ "longitude": "1.2003657",
53
+ "map_link": "https://maps.google.com/?q=6.2349799,1.2003657",
54
+ "adresse": "Agoè nouveau marché, vers le camp de police"
55
+ },
56
+ {
57
+ "nom": "PHARMACIE YESHUA",
58
+ "quartier": "Agoè-Nyivé",
59
+ "contacts": "98772153 / 98772150",
60
+ "latitude": "6.2197903",
61
+ "longitude": "1.1567935",
62
+ "map_link": "https://maps.google.com/?q=6.2197903,1.1567935",
63
+ "adresse": "Agoè - Vakpossito vers l'école NDE, entre le CMS Mur et le CMS Maranatha"
64
+ },
65
+ {
66
+ "nom": "PHARMACIE ZOSSIME",
67
+ "quartier": "AGOE",
68
+ "contacts": "99 99 80 75 / 70 46 26 64",
69
+ "latitude": "6.2333748",
70
+ "longitude": "1.1391774",
71
+ "map_link": "https://maps.google.com/?q=6.2333748,1.1391774",
72
+ "adresse": "Zossimé, route de Sanguéra près du marché de Zossimé"
73
+ },
74
+ {
75
+ "nom": "PHARMACIE LE ROCHER",
76
+ "quartier": "AGOE",
77
+ "contacts": "92 30 06 56 / 99 08 05 01",
78
+ "latitude": "6.2461716",
79
+ "longitude": "1.2071869",
80
+ "map_link": "https://maps.google.com/?q=6.2461716,1.2071869",
81
+ "adresse": "Agoè Zongo, sur la nationale N°1, près du terrain de jeu de Golf"
82
+ },
83
+ {
84
+ "nom": "PHARMACIE CLEMENCE",
85
+ "quartier": "AGOE",
86
+ "contacts": "70 19 35 35 / 70 21 26 26",
87
+ "latitude": "6.2199286",
88
+ "longitude": "1.1895335",
89
+ "map_link": "https://maps.google.com/?q=6.2199286,1.1895335",
90
+ "adresse": "Route de la Cour d'appel, entre la CEET et l'Ecole privée LA SOURCE"
91
+ },
92
+ {
93
+ "nom": "PHARMACIE DIEUDONNE",
94
+ "quartier": "AGOE NYIVE",
95
+ "contacts": "23 38 07 44",
96
+ "latitude": "6.2157404",
97
+ "longitude": "1.1374364",
98
+ "map_link": "https://maps.google.com/?q=6.2157404,1.1374364",
99
+ "adresse": "Route LEO 2000, non loin de FUCEC, Agoè-Téléssou, BP 7951"
100
+ },
101
+ {
102
+ "nom": "PHARMACIE MILLENAIRE",
103
+ "quartier": "LOME",
104
+ "contacts": "22 51 64 31",
105
+ "latitude": "6.206914",
106
+ "longitude": "1.1873226",
107
+ "map_link": "https://maps.google.com/?q=6.206914,1.1873226",
108
+ "adresse": "Adidoadin, 05 BP 64"
109
+ },
110
+ {
111
+ "nom": "PHARMACIE APOLLON",
112
+ "quartier": "LOME",
113
+ "contacts": "22 31 01 07",
114
+ "latitude": "6.202853",
115
+ "longitude": "1.1644948",
116
+ "map_link": "https://maps.google.com/?q=6.202853,1.1644948",
117
+ "adresse": "Hétsiavi, avédji, 08 BP 80507"
118
+ },
119
+ {
120
+ "nom": "PHARMACIE EVA",
121
+ "quartier": "AGOE",
122
+ "contacts": "92163232",
123
+ "latitude": "6.24591",
124
+ "longitude": "1.1150278",
125
+ "map_link": "https://maps.google.com/?q=6.24591,1.1150278",
126
+ "adresse": "Sanguéra, non loin de T'OIL"
127
+ },
128
+ {
129
+ "nom": "PHARMACIE OSSAN",
130
+ "quartier": "LOME",
131
+ "contacts": "23 38 44 25 / 22 51 13 04",
132
+ "latitude": "6.201132",
133
+ "longitude": "1.1785749",
134
+ "map_link": "https://maps.google.com/?q=6.201132,1.1785749",
135
+ "adresse": "Carrefour d’Avédji, BP 869"
136
+ },
137
+ {
138
+ "nom": "PHARMACIE SAINT MICHEL",
139
+ "quartier": "AGOE NYIVE",
140
+ "contacts": "22 51 70 22",
141
+ "latitude": "6.2152161",
142
+ "longitude": "1.2062228",
143
+ "map_link": "https://maps.google.com/?q=6.2152161,1.2062228",
144
+ "adresse": "Près de la Brasserie BB, 05 BP 201"
145
+ },
146
+ {
147
+ "nom": "PHARMACIE SILOE",
148
+ "quartier": "LOME",
149
+ "contacts": "22 33 82 87",
150
+ "latitude": "6.2068567",
151
+ "longitude": "1.1387278",
152
+ "map_link": "https://maps.google.com/?q=6.2068567,1.1387278",
153
+ "adresse": "Carrefour Atigangomé, Adidogomé A pédokoe, 07 BP 7189"
154
+ },
155
+ {
156
+ "nom": "PHARMACIE SAINT ESPRIT",
157
+ "quartier": "AGOE NYIVE",
158
+ "contacts": "22 40 29 06",
159
+ "latitude": "6.2218567",
160
+ "longitude": "1.219288",
161
+ "map_link": "https://maps.google.com/?q=6.2218567,1.219288",
162
+ "adresse": "Route Agoè-Kégué, CEG Agoè Est, BP 1246"
163
+ },
164
+ {
165
+ "nom": "PHARMACIE ACTUELLE",
166
+ "quartier": "LOME",
167
+ "contacts": "22 51 11 72 / 22 50 63 30",
168
+ "latitude": "6.1926065",
169
+ "longitude": "1.1418284",
170
+ "map_link": "https://maps.google.com/?q=6.1926065,1.1418284",
171
+ "adresse": "Route ségbé, sagbado, BP 3622"
172
+ },
173
+ {
174
+ "nom": "PHARMACIE VIGUEUR",
175
+ "quartier": "LOME",
176
+ "contacts": "22 51 63 30 / 90 03 29 71",
177
+ "latitude": "6.1925619",
178
+ "longitude": "1.1986329",
179
+ "map_link": "https://maps.google.com/?q=6.1925619,1.1986329",
180
+ "adresse": "267, rue Agbalépédo, 05 BP 02"
181
+ },
182
+ {
183
+ "nom": "PHARMACIE ADIDOGOME",
184
+ "quartier": "LOME",
185
+ "contacts": "22 25 54 85 / 22 50 54 85",
186
+ "latitude": "6.1850613",
187
+ "longitude": "1.1640512",
188
+ "map_link": "https://maps.google.com/?q=6.1850613,1.1640512",
189
+ "adresse": "Avenue du 30 Août, adidogomé, 04 BP 224 Lomé 04"
190
+ },
191
+ {
192
+ "nom": "PHARMACIE ASSURANCE",
193
+ "quartier": "AGOE",
194
+ "contacts": "93 08 76 76 / 96 82 76 76",
195
+ "latitude": "6.3208762",
196
+ "longitude": "1.2088685",
197
+ "map_link": "https://maps.google.com/?q=6.3208762,1.2088685",
198
+ "adresse": "Adétikopé, Nationale N° 1, non loin du marché"
199
+ },
200
+ {
201
+ "nom": "PHARMACIE DJIDJOLE",
202
+ "quartier": "LOME",
203
+ "contacts": "22 25 65 12 / 23 20 95 55",
204
+ "latitude": "6.1804899",
205
+ "longitude": "1.1930898",
206
+ "map_link": "https://maps.google.com/?q=6.1804899,1.1930898",
207
+ "adresse": "93 rue Pya BP 8539"
208
+ },
209
+ {
210
+ "nom": "PHARMACIE DE L'AEROPORT",
211
+ "quartier": "LOME",
212
+ "contacts": "22 26 21 22",
213
+ "latitude": "6.18244",
214
+ "longitude": "1.2055796",
215
+ "map_link": "https://maps.google.com/?q=6.18244,1.2055796",
216
+ "adresse": "631, bd ; du haho, imm ; SITO, hédzranawoé, BP 3717"
217
+ },
218
+ {
219
+ "nom": "PHARMACIE UNIVERS SANTE",
220
+ "quartier": "LOMÉ",
221
+ "contacts": "22 61 81 43 / 93 88 83 31",
222
+ "latitude": "6.1827033",
223
+ "longitude": "1.2124053",
224
+ "map_link": "https://maps.google.com/?q=6.1827033,1.2124053",
225
+ "adresse": "Bd. GNASSIMGBE Eyadéma, Cité OUA face a l'entrée du CHU-CAMPUS"
226
+ },
227
+ {
228
+ "nom": "PHARMACIE SAINT JOSEPH",
229
+ "quartier": "LOME",
230
+ "contacts": "22 25 74 65",
231
+ "latitude": "6.1772853",
232
+ "longitude": "1.2029426",
233
+ "map_link": "https://maps.google.com/?q=6.1772853,1.2029426",
234
+ "adresse": "Rue Bretelle, Avenue de la RP, Bè Klikamé, BP 13000"
235
+ },
236
+ {
237
+ "nom": "PHARMACIE DES LILAS",
238
+ "quartier": "LOME",
239
+ "contacts": "22 42 29 16",
240
+ "latitude": "6.1830855",
241
+ "longitude": "1.2332334",
242
+ "map_link": "https://maps.google.com/?q=6.1830855,1.2332334",
243
+ "adresse": "7 rue de kégué, en face du camp, BP 31000"
244
+ },
245
+ {
246
+ "nom": "PHARMACIE INTERNATIONALE",
247
+ "quartier": "LOME",
248
+ "contacts": "22 26 89 94 / 99 90 89 66",
249
+ "latitude": "6.1832166",
250
+ "longitude": "1.2389391",
251
+ "map_link": "https://maps.google.com/?q=6.1832166,1.2389391",
252
+ "adresse": "1180, boulevard du Haho, Angle Nord, Assi yéyé Hédzranawoé, BP 1344"
253
+ },
254
+ {
255
+ "nom": "PHARMACIE DU CAMPUS",
256
+ "quartier": "LOME",
257
+ "contacts": "22 21 56 32",
258
+ "latitude": "6.1629236",
259
+ "longitude": "1.2111689",
260
+ "map_link": "https://maps.google.com/?q=6.1629236,1.2111689",
261
+ "adresse": "Bd. De la Kara, adéwui, BP 20217"
262
+ },
263
+ {
264
+ "nom": "PHARMACIE DE LA LIBERATION",
265
+ "quartier": "LOME",
266
+ "contacts": "22 22 25 25",
267
+ "latitude": "6.1533788",
268
+ "longitude": "1.2173758",
269
+ "map_link": "https://maps.google.com/?q=6.1533788,1.2173758",
270
+ "adresse": "199 avenue de la libération, tokoin trésor, BP 12045"
271
+ },
272
+ {
273
+ "nom": "PHARMACIE PROVIDENCE",
274
+ "quartier": "LOMÉ",
275
+ "contacts": "22 26 66 48",
276
+ "latitude": "6.1606858",
277
+ "longitude": "1.2316459",
278
+ "map_link": "https://maps.google.com/?q=6.1606858,1.2316459",
279
+ "adresse": "Boulevard Jean-Paul II"
280
+ },
281
+ {
282
+ "nom": "PHARMACIE SOURCE DE VIE",
283
+ "quartier": "LOME",
284
+ "contacts": "22 22 45 71",
285
+ "latitude": "6.1471155",
286
+ "longitude": "1.2012943",
287
+ "map_link": "https://maps.google.com/?q=6.1471155,1.2012943",
288
+ "adresse": "Bd. De la victoire, Face Collège protestant, BP 147"
289
+ },
290
+ {
291
+ "nom": "PHARMACIE SAINTE RITA",
292
+ "quartier": "LOME",
293
+ "contacts": "22 20 90 16",
294
+ "latitude": "6.1383352",
295
+ "longitude": "1.2281291",
296
+ "map_link": "https://maps.google.com/?q=6.1383352,1.2281291",
297
+ "adresse": "Rue de France, face Hôtel Sana, Doulassamé, BP 10046"
298
+ },
299
+ {
300
+ "nom": "PHARMACIE DE L’UNION",
301
+ "quartier": "LOME",
302
+ "contacts": "22 27 71 64",
303
+ "latitude": "6.1602452",
304
+ "longitude": "1.2610987",
305
+ "map_link": "https://maps.google.com/?q=6.1602452,1.2610987",
306
+ "adresse": "Bd ; mafakassa Bè KPOTA BP 2735"
307
+ },
308
+ {
309
+ "nom": "PHARMACIE DES ETOILES",
310
+ "quartier": "LOME",
311
+ "contacts": "22 22 45 04",
312
+ "latitude": "6.1320067",
313
+ "longitude": "1.2182941",
314
+ "map_link": "https://maps.google.com/?q=6.1320067,1.2182941",
315
+ "adresse": "10, avenue de la nouvelle marché, BP 3484"
316
+ },
317
+ {
318
+ "nom": "PHARMACIE HORIZON",
319
+ "quartier": "LOME",
320
+ "contacts": "90 56 52 56 / 22 20 42 42",
321
+ "latitude": "6.1289741",
322
+ "longitude": "1.210742",
323
+ "map_link": "https://maps.google.com/?q=6.1289741,1.210742",
324
+ "adresse": "95, boulevard du 13 Janvier, Nyekonakpoè, 08 BP 81726"
325
+ },
326
+ {
327
+ "nom": "PHARMACIE AMESSIAME",
328
+ "quartier": "LOME",
329
+ "contacts": "22 21 49 74 / 22 40 66 69",
330
+ "latitude": "6.1361035",
331
+ "longitude": "1.2320088",
332
+ "map_link": "https://maps.google.com/?q=6.1361035,1.2320088",
333
+ "adresse": "Bd. F H Boigny près du marché de Bè, BP 61720"
334
+ },
335
+ {
336
+ "nom": "PHARMACIE SANTE",
337
+ "quartier": "LOME",
338
+ "contacts": "22 21 58 41",
339
+ "latitude": "6.1308958",
340
+ "longitude": "1.2251543",
341
+ "map_link": "https://maps.google.com/?q=6.1308958,1.2251543",
342
+ "adresse": "674, rue du chemin de fer, 01 BP 1236"
343
+ },
344
+ {
345
+ "nom": "PHARMACIE DE SEGBE",
346
+ "quartier": "LOME",
347
+ "contacts": "90 59 49 35 / 79 30 07 29",
348
+ "latitude": "6.1939325",
349
+ "longitude": "1.0440659",
350
+ "map_link": "https://maps.google.com/?q=6.1939325,1.0440659",
351
+ "adresse": "Ségbé, quartier Zanvi,, près de l'EPP et du CEG Ségbé"
352
+ },
353
+ {
354
+ "nom": "PHARMACIE ECLAIR",
355
+ "quartier": "LOME",
356
+ "contacts": "22 22 75 11",
357
+ "latitude": "6.1425481",
358
+ "longitude": "1.2548651",
359
+ "map_link": "https://maps.google.com/?q=6.1425481,1.2548651",
360
+ "adresse": "Rue avénou, bè ahligo, BP 81297"
361
+ },
362
+ {
363
+ "nom": "PHARMACIE LE PROGRES",
364
+ "quartier": "LOME",
365
+ "contacts": "22 35 86 55 / 22 27 14 63",
366
+ "latitude": "6.166686",
367
+ "longitude": "1.2810575",
368
+ "map_link": "https://maps.google.com/?q=6.166686,1.2810575",
369
+ "adresse": "Akodésséwa Kponou (Zorro-bar), 06 BP 60986"
370
+ },
371
+ {
372
+ "nom": "PHARMACIE SAINT PHILIPPE",
373
+ "quartier": "AGOE",
374
+ "contacts": "90 67 33 24 / 99 99 80 04",
375
+ "latitude": "6.2280254",
376
+ "longitude": "1.0259728",
377
+ "map_link": "https://maps.google.com/?q=6.2280254,1.0259728",
378
+ "adresse": "Zanguéra, Route Lomé-Kpalimé, près de la station de service OANDO"
379
+ },
380
+ {
381
+ "nom": "PHARMACIE DE L'EDEN",
382
+ "quartier": "LOME",
383
+ "contacts": "70 42 13 98",
384
+ "latitude": "6.1611581",
385
+ "longitude": "1.306967",
386
+ "map_link": "https://maps.google.com/?q=6.1611581,1.306967",
387
+ "adresse": "Route d'Anèo, face à la cité Baguida"
388
+ },
389
+ {
390
+ "nom": "PHARMACIE SAINT PIO",
391
+ "quartier": "KLOBATEME",
392
+ "contacts": "93404040 / 96868686",
393
+ "latitude": "0.0",
394
+ "longitude": "0.0",
395
+ "map_link": "",
396
+ "adresse": "Klobatèmé, non loin du CMS Klobatèmé à côté du Complexe Scolaire Bon Samaritain"
397
+ },
398
+ {
399
+ "nom": "PHARMACIE WASTINE",
400
+ "quartier": "Lomé",
401
+ "contacts": "92611700 / 22518119",
402
+ "latitude": "0.0",
403
+ "longitude": "0.0",
404
+ "map_link": "",
405
+ "adresse": "Adidogomé Logoté, à 400m du carrefour Logoté sur la route menant au quartier Lankouvi"
406
+ }
407
+ ]
main.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import TypedDict, Literal
3
+ from dotenv import load_dotenv
4
+
5
+ from langchain_groq import ChatGroq
6
+ from langchain_core.messages import HumanMessage, SystemMessage
7
+ from langgraph.graph import StateGraph, END
8
+ from pydantic import BaseModel, Field
9
+
10
+ from tools.pharma_tools import get_nearly
11
+ from tools.hospitals_tools import find_hospitals
12
+
13
+ load_dotenv()
14
+
15
+ import json
16
+ from datetime import datetime
17
+ import os
18
+
19
+ def save_for_finetuning(user_message: str, intention: str, mot_cle: str):
20
+ """Sauvegarde l'interaction au format JSONL pour un futur fine-tuning."""
21
+
22
+ log_entry = {
23
+ "timestamp": datetime.now().isoformat(),
24
+ "messages": [
25
+ {"role": "user", "content": user_message},
26
+ {"role": "assistant", "content": f'{{"categorie": "{intention}", "mot_cle": "{mot_cle}"}}'}
27
+ ]
28
+ }
29
+
30
+ # Chemin vers le fichier de sauvegarde (dans ton dossier data)
31
+ os.makedirs("data", exist_ok=True)
32
+ log_file = "data/finetuning_dataset.jsonl"
33
+
34
+ with open(log_file, "a", encoding="utf-8") as f:
35
+ f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
36
+
37
+ # Definition de l'Etat
38
+ class GraphState(TypedDict):
39
+ user_message: str
40
+ user_lat: float
41
+ user_lon: float
42
+ intention: str
43
+ mot_cle: str
44
+ final_response: str
45
+
46
+ # 2. Définition du format de sortie
47
+ class RouterOutput(BaseModel):
48
+ categorie: Literal["ORIENTATION", "SPECIALISTE", "PHARMACIE", "INCONNU"] = Field(
49
+ description="Classer la demande dans l'une de ces catégories."
50
+ )
51
+ mot_cle: str = Field(
52
+ description="Le service (ex: maternité), la spécialité (ex: cardiologue) ou vide si pharmacie.",
53
+ default=""
54
+ )
55
+
56
+ # Initialisation du LLM
57
+ llm = ChatGroq(model="openai/gpt-oss-120b", temperature=0)
58
+ # On force le LLM à répondre avec notre structure Pydantic
59
+ router_llm = llm.with_structured_output(RouterOutput)
60
+
61
+ # --- LES NOEUDS DU GRAPHE (NODES) ---
62
+
63
+ def analyze_intent(state: GraphState):
64
+ """Analyse le message de l'utilisateur et détermine la route à prendre."""
65
+ prompt = f"""Tu es le routeur principal d'un assistant médical au Togo.
66
+ Analyse ce message et extrais l'intention et le mot clé utile.
67
+
68
+ RÈGLE ABSOLUE POUR LE MOT_CLE :
69
+ Si l'utilisateur cherche un spécialiste, le mot clé DOIT TOUJOURS être le nom du service médical exact (souvent en "ie" ou "logie"), car c'est ce format qui est dans la base de données.
70
+ Exemples stricts :
71
+ - "pédiatre" ou "enfant" -> "pédiatrie"
72
+ - "cardiologue" -> "cardiologie"
73
+ - "ophtalmo" -> "ophtalmologie"
74
+ - "chirurgien" -> "chirurgie"
75
+ - "gynéco" ou "accoucher" -> "maternité"
76
+
77
+ Message : {state["user_message"]}"""
78
+
79
+ result = router_llm.invoke([SystemMessage(content=prompt)])
80
+
81
+ save_for_finetuning(state["user_message"], result.categorie, result.mot_cle)
82
+
83
+ return {"intention": result.categorie, "mot_cle": result.mot_cle}
84
+
85
+ def route_to_pharmacy(state: GraphState):
86
+ """Gère la recherche de pharmacies."""
87
+ response = get_nearly.invoke({
88
+ "user_lat": state["user_lat"],
89
+ "user_lon": state["user_lon"]
90
+ })
91
+
92
+ return {"final_response": response}
93
+
94
+ def route_to_orientation(state: GraphState):
95
+ """Gère l'orientation (rayon 20km)."""
96
+ response = find_hospitals.invoke({
97
+ "user_lat": state["user_lat"],
98
+ "user_lon": state["user_lon"],
99
+ "service_requis": state["mot_cle"],
100
+ "is_specialist": False
101
+ })
102
+ return {"final_response": response}
103
+
104
+ def route_to_specialist(state: GraphState):
105
+ """Gère la recherche de spécialiste."""
106
+ response = find_hospitals.invoke({
107
+ "user_lat": state["user_lat"],
108
+ "user_lon": state["user_lon"],
109
+ "service_requis": state["mot_cle"],
110
+ "is_specialist": True
111
+ })
112
+ return {"final_response": response}
113
+
114
+ # --- LA LOGIQUE DE ROUTAGE CONDITIONNEL (EDGES) ---
115
+
116
+ def decide_next_node(state: GraphState):
117
+ """Détermine le prochain nœud en fonction de l'intention."""
118
+ if state["intention"] == "PHARMACIE":
119
+ return "pharmacy_node"
120
+ elif state["intention"] == "SPECIALISTE":
121
+ return "specialist_node"
122
+ elif state["intention"] == "ORIENTATION":
123
+ return "orientation_node"
124
+ else:
125
+ return END
126
+
127
+ # --- CONSTRUCTION DU GRAPHE ---
128
+
129
+ workflow = StateGraph(GraphState)
130
+
131
+ # Ajout des nœuds
132
+ workflow.add_node("router_node", analyze_intent)
133
+ workflow.add_node("pharmacy_node", route_to_pharmacy)
134
+ workflow.add_node("orientation_node", route_to_orientation)
135
+ workflow.add_node("specialist_node", route_to_specialist)
136
+
137
+ # Le point d'entrée est toujours le routeur
138
+ workflow.set_entry_point("router_node")
139
+
140
+ # Ajout des conditions (Depuis le routeur, vers où aller ?)
141
+ workflow.add_conditional_edges(
142
+ "router_node",
143
+ decide_next_node,
144
+ {
145
+ "pharmacy_node": "pharmacy_node",
146
+ "specialist_node": "specialist_node",
147
+ "orientation_node": "orientation_node",
148
+ END: END
149
+ }
150
+ )
151
+
152
+ # Tous les nœuds d'action mènent à la fin
153
+ workflow.add_edge("pharmacy_node", END)
154
+ workflow.add_edge("orientation_node", END)
155
+ workflow.add_edge("specialist_node", END)
156
+
157
+ # Compilation du graphe
158
+ app = workflow.compile()
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ dotenv
4
+ fastapi
5
+ langchain
6
+ langchain_core
7
+ langchain_groq
8
+ langchain_ollama
9
+ requests
10
+ pyshorteners
11
+ psycopg2-binary
12
+ uvicorn
13
+ pydantic
14
+ python-multipart
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (145 Bytes). View file
 
src/__pycache__/prompts.cpython-312.pyc ADDED
Binary file (526 Bytes). View file
 
src/prompts.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pharmacy_prompt = """
2
+ TU ES : NOVA HEALTH PharmaGuide.
3
+ ROLE : Assistant serviable pour trouver des pharmacies.
4
+
5
+ MISSION :
6
+ 1. Accueille la demande poliment si besoin.
7
+ 2. Utilise l'outil pour trouver les pharmacies.
8
+ 3. Présente les résultats de façon claire et lisible.
9
+
10
+ FORMAT DE SORTIE OBLIGATOIRE :
11
+ "Voici les pharmacies de garde les plus proches de votre position :"
12
+
13
+ """
tools/__init__.py ADDED
File without changes
tools/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (147 Bytes). View file
 
tools/__pycache__/hospitals_tools.cpython-312.pyc ADDED
Binary file (3.35 kB). View file
 
tools/__pycache__/pharma_tools.cpython-312.pyc ADDED
Binary file (5.97 kB). View file
 
tools/__pycache__/transcription.cpython-312.pyc ADDED
Binary file (950 Bytes). View file
 
tools/hospitals_tools.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import psycopg2
3
+ from dotenv import load_dotenv
4
+ from langchain.tools import tool
5
+
6
+ # Connexion a la db
7
+ load_dotenv()
8
+
9
+ def get_db_connection():
10
+ db_url = os.getenv("DB_URL")
11
+ if not db_url:
12
+ raise ValueError("L'URL de la base de données RENDER_DB_URL est introuvable dans le .env")
13
+ return psycopg2.connect(db_url)
14
+
15
+ @tool
16
+ def find_hospitals(user_lat: float, user_lon: float, service_requis: str, is_specialist: bool = False):
17
+ """
18
+ Cherche les 5 hôpitaux les plus proches avec lits disponibles, sans limite de distance.
19
+ """
20
+ conn = get_db_connection()
21
+ cursor = conn.cursor()
22
+
23
+ # Requête unique et propre : on calcule la distance, on filtre par service/lits,
24
+ # on trie par proximité et on limite à 5.
25
+ query = """
26
+ SELECT h.name, h.address, h.phone,
27
+ (6371 * acos(cos(radians(%s)) * cos(radians(h.latitude)) * cos(radians(h.longitude) - radians(%s)) +
28
+ sin(radians(%s)) * sin(radians(h.latitude)))) AS distance_reelle,
29
+ h.latitude,
30
+ h.longitude
31
+ FROM structures h
32
+ WHERE EXISTS (
33
+ SELECT 1 FROM services s
34
+ WHERE s.structure_id = h.id
35
+ AND s.name ILIKE %s
36
+ AND s.total_beds >= 2
37
+ )
38
+ ORDER BY distance_reelle ASC
39
+ LIMIT 5;
40
+ """
41
+
42
+ def executer_recherche(mot_cle):
43
+ # On passe systématiquement les 4 paramètres requis par la requête ci-dessus
44
+ cursor.execute(query, (user_lat, user_lon, user_lat, f"%{mot_cle}%"))
45
+ return cursor.fetchall()
46
+
47
+ try:
48
+ # 3. On tente d'abord de chercher le service demandé
49
+ resultats_recherche = executer_recherche(service_requis)
50
+
51
+ # 4. GESTION DU REPLI
52
+ if not resultats_recherche:
53
+ resultats_fallback = executer_recherche("général")
54
+
55
+ if not resultats_fallback:
56
+ return "Aucun médecin généraliste trouvé à proximité."
57
+
58
+ resultats_finaux = resultats_fallback
59
+
60
+ else:
61
+ # Si on a trouvé le spécialiste
62
+ resultats_finaux = resultats_recherche
63
+
64
+ # 5. Boucle d'affichage
65
+ resultats_json = []
66
+
67
+ for row in resultats_finaux:
68
+ resultats_json.append({
69
+ "name": row[0],
70
+ "address": row[1],
71
+ "phone": row[2],
72
+ "distance": round(row[3], 2),
73
+ "map_link": f"https://www.google.com/maps/dir/?api=1&origin={user_lat},{user_lon}&destination={row[4]},{row[5]}"
74
+ })
75
+ return resultats_json
76
+
77
+ except Exception as e:
78
+ return f"[ERROR] Erreur base de données : {e}"
79
+ finally:
80
+ cursor.close()
81
+ conn.close()
tools/pharma_tools.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+ import math
4
+ import json
5
+ from dotenv import load_dotenv
6
+ from pathlib import Path
7
+ from langchain.tools import tool
8
+
9
+ load_dotenv()
10
+
11
+ # --- CONFIGURATION DES CHEMINS ---
12
+ BASE_DIR = Path(__file__).resolve().parent.parent
13
+ DB_DIR = BASE_DIR / 'data'
14
+ CACHE_FILE_PATH = DB_DIR / 'pharmacies_cache.json'
15
+
16
+
17
+ def fetch_and_save_pharmacies():
18
+ url = os.environ.get('url')
19
+ payload = {
20
+ 'access_token': os.environ.get('access_token'),
21
+ 'c_identifiant': os.environ.get('c_identifiant'),
22
+ 'u_identifiant': os.environ.get('u_identifiant'),
23
+ 'latitude': '6.2545014',
24
+ 'longitude': '1.1690376',
25
+ 'client': '{"fullname": "soulemane bilali","phone": "+22891016567"}'
26
+ }
27
+
28
+ headers = {'apiKey': os.environ.get('apiKey')}
29
+
30
+ try:
31
+ response = requests.post(url, headers=headers, data=payload, timeout=15)
32
+ response.raise_for_status()
33
+ data = response.json()
34
+
35
+ filtered_pharmacies = []
36
+
37
+ print("[INFO] : Loading pharmacy")
38
+
39
+ for phar in data.get('pharmacies', []):
40
+
41
+ clean_phar = {
42
+ "nom": phar.get("nom"),
43
+ "quartier": phar.get("ville"),
44
+ "contacts": f"{phar.get('contact_1') or ''} / {phar.get('contact_2') or ''}".strip(" / "),
45
+ "latitude": phar.get("latitude"),
46
+ "longitude": phar.get("longitude"),
47
+ "map_link": phar.get('map_link'),
48
+ "adresse": phar.get("adresse")
49
+ }
50
+ filtered_pharmacies.append(clean_phar)
51
+
52
+ # Sauvegarde
53
+ os.makedirs(DB_DIR, exist_ok=True)
54
+ with open(CACHE_FILE_PATH, 'w', encoding='utf-8') as f:
55
+ json.dump(filtered_pharmacies, f, ensure_ascii=False, indent=4)
56
+
57
+ print("[DEBUG] : Sucess update")
58
+
59
+ except Exception as e:
60
+ print(f"[ERROR] Update failed: {e}")
61
+
62
+
63
+ ######################################## CALCULATE DISTANCE ######################################################33
64
+
65
+ import math
66
+ def calculat_distance(lat1, lon1, lat2, lon2):
67
+ R = 6371
68
+
69
+ dlat = math.radians(float(lat2) - float(lat1))
70
+ dlon = math.radians(float(lon2) - float(lon1))
71
+
72
+ a = math.sin(dlat / 2)**2 + math.cos(math.radians(float(lat1))) * \
73
+ math.cos(math.radians(float(lat2))) * math.sin(dlon / 2)**2
74
+
75
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
76
+ return R * c
77
+
78
+ ############################################# NEARLY PHARMMACY #####################################################################
79
+ @tool
80
+ def get_nearly(user_lat, user_lon, fichier_json=CACHE_FILE_PATH):
81
+ """ Get nearly pharmacy """
82
+ try:
83
+ print('[INFOS] : fetch and save save pharmacy')
84
+ fetch_and_save_pharmacies()
85
+
86
+ with open(fichier_json, 'r', encoding='utf-8') as f:
87
+ pharmacies = json.load(f)
88
+
89
+ for phar in pharmacies:
90
+
91
+ if float(phar['latitude']) != 0:
92
+ dist = calculat_distance(user_lat, user_lon, phar['latitude'], phar['longitude'])
93
+ phar['distance_reelle'] = round(dist, 2)
94
+ else:
95
+ phar['distance_reelle'] = float('inf') # Trop loin si pas de GPS
96
+
97
+ top_5 = sorted(pharmacies, key=lambda x: x['distance_reelle'])[:5]
98
+
99
+ resultats_json = []
100
+ for p in top_5:
101
+ resultats_json.append({
102
+ "name": p.get('nom', 'N/A'),
103
+ "address": f"{p.get('quartier', '')} - {p.get('adresse', '')}",
104
+ "phone": p.get('contacts', 'N/A'),
105
+ "distance": p['distance_reelle'],
106
+ "map_link": p.get('map_link', '')
107
+ })
108
+ return resultats_json
109
+
110
+ except FileNotFoundError:
111
+ return {"error": "Le fichier JSON des pharmacies est introuvable."}
112
+ except Exception as e:
113
+ return {"error": str(e)}
114
+
tools/prompts.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ router_prompt = """
2
+ TU ES : Le routeur principal de NOVA HEALTH.
3
+ TON RÔLE : Analyser la demande de l'utilisateur et la classer dans l'une des 3 catégories suivantes :
4
+ 1. "ORIENTATION" : L'utilisateur décrit une situation (femme enceinte, enfant malade, accident).
5
+ 2. "SPECIALISTE" : L'utilisateur cherche explicitement une spécialité médicale (cardiologue, ophtalmologue...).
6
+ 3. "PHARMACIE" : L'utilisateur cherche une pharmacie de garde ou des médicaments.
7
+
8
+ FORMAT DE SORTIE OBLIGATOIRE (JSON pur) :
9
+ {
10
+ "categorie": "ORIENTATION" | "SPECIALISTE" | "PHARMACIE",
11
+ "mot_cle": "maternité" | "pédiatrie" | "cardiologie" | null (selon le besoin extrait)
12
+ }
13
+ """
tools/transcription.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from groq import Groq
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ def transcribe_audio(audio_file_path):
8
+ client = Groq(api_key=os.getenv("GROQ_API_KEY"))
9
+
10
+ with open(audio_file_path, "rb") as file:
11
+ transcription = client.audio.transcriptions.create(
12
+ file=(audio_file_path, file.read()),
13
+ model="whisper-large-v3",
14
+ response_format="text", # "json"
15
+ language="fr"
16
+ )
17
+ return transcription