Spaces:
Runtime error
Runtime error
message
#1
by BilalBill - opened
- .env.example +0 -0
- .gitignore +3 -0
- .vscode/settings.json +3 -0
- Dockerfile +33 -0
- __pycache__/main.cpython-312.pyc +0 -0
- api.py +125 -0
- data/__init__.py +0 -0
- data/finetuning_dataset.jsonl +54 -0
- data/pharmacies_cache.json +407 -0
- main.py +158 -0
- requirements.txt +14 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-312.pyc +0 -0
- src/__pycache__/prompts.cpython-312.pyc +0 -0
- src/prompts.py +13 -0
- tools/__init__.py +0 -0
- tools/__pycache__/__init__.cpython-312.pyc +0 -0
- tools/__pycache__/hospitals_tools.cpython-312.pyc +0 -0
- tools/__pycache__/pharma_tools.cpython-312.pyc +0 -0
- tools/__pycache__/transcription.cpython-312.pyc +0 -0
- tools/hospitals_tools.py +81 -0
- tools/pharma_tools.py +114 -0
- tools/prompts.py +13 -0
- tools/transcription.py +17 -0
.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
|