| import os
|
| import random
|
| import torch
|
| import pandas as pd
|
| from flask import Flask, render_template, request, jsonify
|
| from sentence_transformers import SentenceTransformer, util
|
| import gradio as gr
|
| import uvicorn
|
| import nest_asyncio
|
| from fastapi import FastAPI
|
| from fastapi.middleware.wsgi import WSGIMiddleware
|
|
|
|
|
|
|
|
|
| BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
|
| CSV_DATA = "dataset_2026.csv"
|
| EMB_FILE = "embeddings_questions.pt"
|
| TOP_K_RECOMMANDATIONS = 5
|
|
|
|
|
|
|
|
|
| app = Flask(
|
| __name__,
|
| template_folder=os.path.join(BASE_DIR, "templates"),
|
| static_folder=os.path.join(BASE_DIR, "static")
|
| )
|
|
|
|
|
|
|
|
|
| print("🔄 Chargement du modèle...")
|
|
|
| try:
|
| model = SentenceTransformer(
|
| "OrdalieTech/Solon-embeddings-mini-beta-1.1",
|
| device="cpu",
|
| trust_remote_code=True
|
| )
|
| print("✓ Modèle principal chargé")
|
| except Exception as e:
|
| print("⚠️ Modèle principal échoué:", e)
|
| model = SentenceTransformer(
|
| "paraphrase-multilingual-MiniLM-L12-v2",
|
| device="cpu"
|
| )
|
| print("✓ Modèle fallback chargé")
|
|
|
|
|
|
|
|
|
| df = None
|
| embeddings = None
|
|
|
|
|
|
|
|
|
|
|
| def load_data():
|
| global df
|
|
|
| try:
|
| try:
|
| df = pd.read_excel(CSV_DATA, engine="openpyxl")
|
| except Exception:
|
|
|
| df = pd.read_csv(CSV_DATA, sep=None, engine="python", encoding="utf-8", on_bad_lines="skip")
|
|
|
|
|
| df.columns = df.columns.str.strip()
|
|
|
| print(f"✓ Données chargées: {len(df)} lignes")
|
| print("📌 Colonnes:", df.columns.tolist())
|
|
|
| return df
|
|
|
| except FileNotFoundError:
|
| print("❌ Dataset introuvable → création...")
|
|
|
| df = pd.DataFrame({
|
| "Question": ["Bonjour", "Comment ça va?", "Qu'est-ce que c'est?"],
|
| "Response": [
|
| "Bonjour! Comment puis-je vous aider?",
|
| "Je vais bien, merci!",
|
| "C'est une application Q/A"
|
| ],
|
| "Intent": ["salutation", "conversation", "information"]
|
| })
|
|
|
| df.to_csv(CSV_DATA, index=False)
|
| return df
|
|
|
|
|
|
|
|
|
|
|
| def load_embeddings():
|
| global embeddings, df
|
|
|
| if embeddings is not None:
|
| return embeddings
|
|
|
| if os.path.exists(EMB_FILE):
|
| print("📂 Chargement embeddings...")
|
| loaded_embs = torch.load(EMB_FILE, map_location="cpu")
|
|
|
| expected_dim = model.get_sentence_embedding_dimension()
|
| if loaded_embs.shape[1] == expected_dim and loaded_embs.shape[0] == len(df):
|
| embeddings = loaded_embs
|
| return embeddings
|
| else:
|
| print(f"⚠️ Incohérence détectée (dim: {loaded_embs.shape[1]} vs {expected_dim}, taille: {loaded_embs.shape[0]} vs {len(df)}). Recréation des embeddings...")
|
|
|
| print("🔨 Création embeddings...")
|
|
|
| questions = df["Question"].astype(str).tolist()
|
|
|
| embeddings = model.encode(
|
| questions,
|
| convert_to_tensor=True,
|
| normalize_embeddings=True,
|
| show_progress_bar=True
|
| )
|
|
|
| torch.save(embeddings, EMB_FILE)
|
| print("✓ Embeddings sauvegardés")
|
|
|
| return embeddings
|
|
|
|
|
|
|
|
|
|
|
| def enrich_message(text):
|
| prefixes = [
|
| "Bonne question 🙂",
|
| "Voici la réponse :",
|
| "Intéressant !",
|
| "D'après mes données :",
|
| "Réponse :",
|
| "🤖"
|
| ]
|
| return f"{random.choice(prefixes)} {text}"
|
|
|
|
|
| def get_column(df, name):
|
| """
|
| SAFE column getter (fixes Intent/intent/spacing issues)
|
| """
|
| for col in df.columns:
|
| if col.lower() == name.lower():
|
| return col
|
| raise KeyError(f"Column '{name}' not found. Available: {df.columns.tolist()}")
|
|
|
|
|
|
|
|
|
|
|
| def process_question(question):
|
| global df, embeddings
|
|
|
| if not question or not question.strip():
|
| return {
|
| "response": "Veuillez poser une question valide.",
|
| "confidence": 0,
|
| "matched": "—",
|
| "intent": "Invalid",
|
| "recs": []
|
| }
|
|
|
| try:
|
| df = load_data()
|
| embeddings = load_embeddings()
|
|
|
| q_col = get_column(df, "Question")
|
| r_col = get_column(df, "Response")
|
| i_col = get_column(df, "Intent")
|
|
|
| service_col = get_column(df, "Service")
|
| link_col = get_column(df, "ServiceLink")
|
| lat_col = get_column(df, "Latitude")
|
| lon_col = get_column(df, "Longitude")
|
|
|
| img_col = None
|
| for col in df.columns:
|
| if col.lower() in ["imageplan", "image", "plan"]:
|
| img_col = col
|
| break
|
|
|
| emb_q = model.encode(
|
| question,
|
| convert_to_tensor=True,
|
| normalize_embeddings=True
|
| )
|
|
|
| scores = util.pytorch_cos_sim(emb_q, embeddings)[0]
|
|
|
| best_idx = torch.argmax(scores).item()
|
| confidence = int(scores[best_idx].item() * 100)
|
|
|
|
|
| if confidence < 40:
|
|
|
| try:
|
| unique_intents = df[i_col].drop_duplicates()
|
| sampled_intents = unique_intents.sample(n=min(TOP_K_RECOMMANDATIONS, len(unique_intents)))
|
|
|
| orientation_recs = []
|
| for ui in sampled_intents:
|
|
|
| first_q = df[df[i_col] == ui][q_col].iloc[0]
|
| orientation_recs.append(str(first_q))
|
| except Exception:
|
|
|
| orientation_recs = df[q_col].sample(min(TOP_K_RECOMMANDATIONS, len(df))).tolist()
|
|
|
| return {
|
| "response": "Je ne suis pas certain d'avoir la réponse exacte. Cependant, pour vous orienter, voici quelques exemples de sujets sur lesquels je peux vous accompagner :",
|
| "confidence": confidence,
|
| "matched": "—",
|
| "intent": "Besoin d'orientation",
|
| "recs": orientation_recs,
|
| "service": None,
|
| "link": None,
|
| "lat": None,
|
| "lon": None
|
| }
|
|
|
|
|
| answer = df[r_col].iloc[best_idx]
|
| intent = df[i_col].iloc[best_idx]
|
|
|
| service = df[service_col].iloc[best_idx] if service_col else None
|
| link = df[link_col].iloc[best_idx] if link_col else None
|
| lat = df[lat_col].iloc[best_idx] if lat_col else None
|
| lon = df[lon_col].iloc[best_idx] if lon_col else None
|
|
|
|
|
| service_val = str(service) if pd.notna(service) and str(service).lower() not in ['nan', 'none', 'null', ''] else None
|
| link_val = str(link) if pd.notna(link) and str(link).lower() not in ['nan', 'none', 'null', ''] else None
|
|
|
| image_val = None
|
| if img_col:
|
| raw_img = df[img_col].iloc[best_idx]
|
| image_val = str(raw_img) if pd.notna(raw_img) and str(raw_img).lower() not in ['nan', 'none', 'null', ''] else None
|
|
|
| try:
|
| lat_val = float(lat) if pd.notna(lat) and str(lat).lower() not in ['nan', 'none', 'null', ''] else None
|
| lon_val = float(lon) if pd.notna(lon) and str(lon).lower() not in ['nan', 'none', 'null', ''] else None
|
| except Exception:
|
| lat_val = None
|
| lon_val = None
|
|
|
|
|
| recs = []
|
|
|
| return {
|
| "response": enrich_message(answer),
|
| "confidence": confidence,
|
| "matched": df[q_col].iloc[best_idx],
|
| "intent": intent,
|
| "recs": recs,
|
| "service": service_val,
|
| "link": link_val,
|
| "lat": lat_val,
|
| "lon": lon_val,
|
| "image": image_val
|
| }
|
|
|
| except Exception as e:
|
| print("❌ Erreur:", e)
|
| return {
|
| "response": "Erreur technique.",
|
| "confidence": 0,
|
| "matched": "—",
|
| "intent": "Error",
|
| "recs": [],
|
| "service": None,
|
| "link": None,
|
| "lat": None,
|
| "lon": None,
|
| "image": None
|
| }
|
|
|
|
|
|
|
|
|
|
|
| @app.route("/")
|
| def index():
|
| return render_template("index.html")
|
|
|
|
|
| @app.route("/ask", methods=["POST"])
|
| def ask():
|
| try:
|
| data = request.get_json()
|
| question = data.get("question", "")
|
| return jsonify(process_question(question))
|
| except Exception as e:
|
| print(e)
|
| return jsonify({"response": "Erreur serveur"})
|
|
|
| @app.route("/api/services", methods=["GET"])
|
| def get_services():
|
| try:
|
| global df
|
| if df is None:
|
| df = load_data()
|
|
|
| service_col = get_column(df, "Service")
|
| lat_col = get_column(df, "Latitude")
|
| lon_col = get_column(df, "Longitude")
|
|
|
|
|
| valid_df = df.dropna(subset=[service_col, lat_col, lon_col])
|
|
|
| services_list = []
|
| seen = set()
|
| for _, row in valid_df.iterrows():
|
| srv = str(row[service_col]).strip()
|
| if srv and srv.lower() not in ['nan', 'none', 'null', ''] and srv not in seen:
|
| try:
|
| lat_val = float(row[lat_col])
|
| lon_val = float(row[lon_col])
|
| if not pd.isna(lat_val) and not pd.isna(lon_val):
|
| services_list.append({
|
| "service": srv,
|
| "lat": lat_val,
|
| "lon": lon_val
|
| })
|
| seen.add(srv)
|
| except Exception:
|
| pass
|
|
|
| return jsonify({"status": "success", "services": services_list})
|
| except Exception as e:
|
| print("❌ Error API Services:", e)
|
| return jsonify({"status": "error", "services": []})
|
|
|
|
|
|
|
|
|
|
|
| def gradio_chat(message, history):
|
| return process_question(message)["response"]
|
|
|
| iface = gr.ChatInterface(
|
| fn=gradio_chat,
|
| title="AskLaQ Assistant",
|
| description="Posez vos questions"
|
| )
|
|
|
|
|
|
|
|
|
|
|
| fastapi_app = FastAPI(title="AskLaQ API")
|
|
|
| fastapi_app.mount("/", WSGIMiddleware(app))
|
| fastapi_app = gr.mount_gradio_app(fastapi_app, iface, path="/chat")
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| nest_asyncio.apply()
|
|
|
| print("=" * 60)
|
| print("🚀 ASKLAQ SYSTEM (ROBUST VERSION)")
|
| print("=" * 60)
|
|
|
| uvicorn.run(
|
| fastapi_app,
|
| host="0.0.0.0",
|
| port=7860,
|
| log_level="info"
|
| )
|
|
|