import asyncio import json import requests import os import random import pandas as pd from typing import List, Optional from sqlalchemy import text from config import groq_client, CHAT_MODEL, DATABRICKS_ENDPOINT_URL, DATABRICKS_TOKEN, engine # Chemin local vers ton dataset d'entrainement CSV_PATH = os.path.join(os.path.dirname(__file__), "medical_dataset_final.csv") print(f"[INFO] Chargement du dataset depuis le fichier local : {CSV_PATH}") try: if os.path.exists(CSV_PATH): hf_dataset = pd.read_csv(CSV_PATH) print("Dataset local chargé avec succès.") else: print(f"[ALERTE] Le fichier {CSV_PATH} est introuvable.") hf_dataset = None except Exception as e: print(f"[ALERTE] Échec du chargement du fichier CSV : {e}") hf_dataset = None # ───────────────────────────────────────────────────────────────────────────── # DATASET — symptômes réels (évite les hallucinations du LLM) # ───────────────────────────────────────────────────────────────────────────── def get_symptoms_from_dataset(disease_name: str) -> str: """Extrait les symptômes réels depuis le dataset pour éviter les hallucinations du LLM.""" global hf_dataset if hf_dataset is None: return "Données indisponibles." match = hf_dataset[hf_dataset['disease'].str.lower() == disease_name.lower()] if not match.empty: return str(match.iloc[0].get('symptoms', 'Non spécifié')) return "Aucun symptôme spécifique enregistré." # ───────────────────────────────────────────────────────────────────────────── # USER PROFILE # ───────────────────────────────────────────────────────────────────────────── def get_user_profile(user_id: str) -> Optional[dict]: if not engine or not user_id: return None try: with engine.connect() as conn: user_row = conn.execute( text("SELECT full_name, email FROM users WHERE id = :uid"), {"uid": user_id} ).fetchone() if not user_row: return None profile_row = conn.execute( text("SELECT age, sex, family_history, allergies, medications FROM medical_profiles WHERE user_id = :uid"), {"uid": user_id} ).fetchone() profile = {"full_name": user_row[0] or "", "email": user_row[1] or ""} if profile_row: fh = profile_row[2] if isinstance(fh, str): try: fh = json.loads(fh) except: fh = [] profile.update({ "age": profile_row[0], "sex": profile_row[1], "family_history": fh or [], "allergies": profile_row[3] or "", "medications": profile_row[4] or "" }) return profile except Exception as e: print(f"Erreur get_user_profile: {e}") return None # ───────────────────────────────────────────────────────────────────────────── # NEARBY HOSPITALS # ───────────────────────────────────────────────────────────────────────────── async def get_nearby_hospitals(lat: float, lon: float) -> str: try: overpass_url = "https://overpass.kumi.systems/api/interpreter" query = ( f'[out:json];' f'(node["amenity"="hospital"](around:5000,{lat},{lon});' f'way["amenity"="hospital"](around:5000,{lat},{lon}););' f'out tags;' ) response = await asyncio.to_thread( requests.get, overpass_url, params={'data': query}, timeout=5 ) data = response.json() names = [ el['tags'].get('name') for el in data.get('elements', []) if el['tags'].get('name') ] return ", ".join(names[:3]) if names else "" except Exception as e: print(f"Erreur Géolocalisation: {e}") return "" # ───────────────────────────────────────────────────────────────────────────── # TRANSLATION # ───────────────────────────────────────────────────────────────────────────── async def translate_to_english(text_to_translate: str) -> str: try: prompt = f"Translate only this medical text into English. Return only the translation:\n{text_to_translate}" response = await groq_client.chat.completions.create( messages=[{"role": "user", "content": prompt}], model=CHAT_MODEL, temperature=0.1 ) return response.choices[0].message.content.strip() except Exception: return text_to_translate # ───────────────────────────────────────────────────────────────────────────── # DISEASE DETAILS VIA LLM # ───────────────────────────────────────────────────────────────────────────── async def generate_disease_details_via_llm(disease_name_en: str, lang: str = "fr") -> dict: """ Génère description et traitement réels dans la langue demandée. Les symptômes proviennent exclusivement du dataset. """ try: symptomes_reels = get_symptoms_from_dataset(disease_name_en) lang_label = "French" if lang == "fr" else "English" if lang == "en" else "French" prompt = ( f"You are a medical expert. Analyze the disease: '{disease_name_en}'.\n" f"Respond EXCLUSIVELY in {lang_label}.\n" "Return ONLY a strict JSON object with these 4 keys:\n" "- 'name_fr': Translated name of the disease in the response language.\n" "- 'description': Clear explanation of what this disease is, its causes, " "and who it typically affects. 2-3 sentences, accessible language, warm tone.\n" "- 'treatment': Concrete actionable treatment: first-line medications or interventions, " "lifestyle advice, specific steps the patient can take. " "Do NOT only say consult a doctor — give real guidance first, " "then suggest seeing a doctor for confirmation. 3-4 sentences.\n" f"- 'typical_symptoms': Copy this value exactly without any modification: {repr(symptomes_reels)}\n" "Return only the JSON, no preamble." ) response = await groq_client.chat.completions.create( messages=[{"role": "user", "content": prompt}], model=CHAT_MODEL, temperature=0.3, response_format={"type": "json_object"} ) return json.loads(response.choices[0].message.content) except Exception: # Fallback : appel LLM simplifié sans response_format try: symptomes_reels = get_symptoms_from_dataset(disease_name_en) fallback_prompt = ( f"Tu es un médecin. Décris en français la maladie '{disease_name_en}' en 2 phrases simples. " f"Puis propose un traitement concret en 2 phrases (médicaments, conseils pratiques). " f"Format JSON strict avec les clés: name_fr, description, treatment. " f"Ne réponds que le JSON." ) r = await groq_client.chat.completions.create( messages=[{"role": "user", "content": fallback_prompt}], model=CHAT_MODEL, temperature=0.3, max_tokens=300 ) import re text = r.choices[0].message.content.strip() match = re.search(r'\{.*\}', text, re.DOTALL) data = json.loads(match.group()) if match else {} return { "name_fr": data.get("name_fr", disease_name_en), "description": data.get("description", f"Analyse clinique de {disease_name_en}."), "treatment": data.get("treatment", "Consultez un médecin."), "typical_symptoms": get_symptoms_from_dataset(disease_name_en) } except Exception: return { "name_fr": disease_name_en, "description": f"Analyse clinique de {disease_name_en}.", "treatment": "Consultez un médecin.", "typical_symptoms": get_symptoms_from_dataset(disease_name_en) } async def transcribe_audio_groq(audio_bytes: bytes) -> str: try: from groq import Groq as SyncGroq api_key = os.getenv("GROQ_API_KEY") def _sync_transcribe(): sync_client = SyncGroq(api_key=api_key) return sync_client.audio.transcriptions.create( file=("recording.webm", audio_bytes), model="whisper-large-v3", response_format="json" ).text.strip() return await asyncio.to_thread(_sync_transcribe) except Exception as e: print(f"[ERREUR TRANSCRIPTION] : {e}") return "" # ───────────────────────────────────────────────────────────────────────────── # IMAGE OCR # ───────────────────────────────────────────────────────────────────────────── async def image_to_text_groq(image_bytes: bytes) -> str: try: import base64 from config import VISION_MODEL b64_image = base64.b64encode(image_bytes).decode("utf-8") response = await groq_client.chat.completions.create( model=VISION_MODEL, messages=[{ "role": "user", "content": [ { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_image}"} }, {"type": "text", "text": "Extract all text faithfully."} ] }], max_tokens=1024, temperature=0.1, ) return response.choices[0].message.content.strip() except Exception as e: print(f"[ERREUR OCR] : {e}") return "" # ───────────────────────────────────────────────────────────────────────────── # OCR VALIDATION # ───────────────────────────────────────────────────────────────────────────── async def check_is_medical_text(text_content: str) -> bool: try: response = await groq_client.chat.completions.create( messages=[{ "role": "user", "content": f"Is this text medical? Answer YES or NO.\n{text_content[:500]}" }], model=CHAT_MODEL, max_tokens=5, temperature=0.0, ) return "YES" in response.choices[0].message.content.upper() except Exception: return False