Space3 / app.py
Hakim18's picture
Upload 10 files
46ce62a verified
Raw
History Blame Contribute Delete
12 kB
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
# ==============================
# CONFIG
# ==============================
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
CSV_DATA = "dataset_2026.csv"
EMB_FILE = "embeddings_questions.pt"
TOP_K_RECOMMANDATIONS = 5
# ==============================
# FLASK APP
# ==============================
app = Flask(
__name__,
template_folder=os.path.join(BASE_DIR, "templates"),
static_folder=os.path.join(BASE_DIR, "static")
)
# ==============================
# MODEL
# ==============================
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é")
# ==============================
# GLOBAL CACHE (IMPORTANT FIX)
# ==============================
df = None
embeddings = None
# ==============================
# DATA LOADING (ROBUST FIX)
# ==============================
def load_data():
global df
try:
try:
df = pd.read_excel(CSV_DATA, engine="openpyxl")
except Exception:
# Fallback for csv
df = pd.read_csv(CSV_DATA, sep=None, engine="python", encoding="utf-8", on_bad_lines="skip")
# normalize column names (VERY IMPORTANT FIX)
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
# ==============================
# EMBEDDINGS (CACHE FIX)
# ==============================
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
# ==============================
# UTILS
# ==============================
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()}")
# ==============================
# CORE LOGIC (FIXED)
# ==============================
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)
# LOW CONFIDENCE
if confidence < 40:
# Orientation : pick a few distinct topics to guide the user
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:
# Take the first question from this intent
first_q = df[df[i_col] == ui][q_col].iloc[0]
orientation_recs.append(str(first_q))
except Exception:
# Fallback to random if something goes wrong
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
}
# CONFIDENCE >= 40% (Final answer without recommendations)
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
# Clean NaN/Null values
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
# No recommendations if similarity >= 40%
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
}
# ==============================
# FLASK ROUTES
# ==============================
@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")
# Filter rows having valid lat and lon and service
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": []})
# ==============================
# GRADIO
# ==============================
def gradio_chat(message, history):
return process_question(message)["response"]
iface = gr.ChatInterface(
fn=gradio_chat,
title="AskLaQ Assistant",
description="Posez vos questions"
)
# ==============================
# FASTAPI WRAPPER
# ==============================
fastapi_app = FastAPI(title="AskLaQ API")
fastapi_app.mount("/", WSGIMiddleware(app))
fastapi_app = gr.mount_gradio_app(fastapi_app, iface, path="/chat")
# ==============================
# MAIN
# ==============================
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"
)