Spaces:
Sleeping
Sleeping
| """ | |
| Avis'IA Resto : application de gestion des réponses aux avis clients | |
| Génération assistée par IA (Mistral) avec validation humaine (Human-in-the-Loop) | |
| Déploiement : Hugging Face Spaces (SDK Gradio) | |
| Prérequis : secret MISTRAL_API_KEY défini dans Settings > Variables and secrets | |
| """ | |
| import os | |
| import gradio as gr | |
| from mistralai import Mistral | |
| # --------------------------------------------------------------- | |
| # 1. Configuration et client Mistral | |
| # --------------------------------------------------------------- | |
| MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY") | |
| MODEL = "mistral-small-latest" | |
| client = Mistral(api_key=MISTRAL_API_KEY) if MISTRAL_API_KEY else None | |
| # --------------------------------------------------------------- | |
| # 2. Templates de secours (fallback) par note | |
| # Utilisés uniquement si l'API est indisponible | |
| # --------------------------------------------------------------- | |
| TEMPLATES_FALLBACK = { | |
| 1: ("Nous sommes sincèrement désolés que votre expérience n'ait pas été " | |
| "à la hauteur. Vos remarques sont précieuses et nous allons agir. " | |
| "N'hésitez pas à nous contacter directement pour en discuter."), | |
| 2: ("Merci pour votre retour honnête. Nous regrettons que votre visite " | |
| "n'ait pas été pleinement satisfaisante et nous prenons vos remarques " | |
| "très au sérieux. Nous espérons pouvoir vous reconquérir."), | |
| 3: ("Merci pour votre avis. Nous notons vos points d'amélioration et " | |
| "travaillons chaque jour à offrir une meilleure expérience. " | |
| "Au plaisir de vous accueillir à nouveau."), | |
| 4: ("Merci beaucoup pour ce retour positif ! Nous sommes ravis que votre " | |
| "expérience vous ait plu et prenons note de vos suggestions. " | |
| "À très bientôt !"), | |
| 5: ("Un immense merci pour ce superbe avis ! Toute l'équipe est touchée. " | |
| "Nous avons hâte de vous retrouver très vite !"), | |
| } | |
| # --------------------------------------------------------------- | |
| # 3. Construction du prompt à partir du contenu réel de l'avis | |
| # --------------------------------------------------------------- | |
| def construire_prompt(avis_texte: str, note: int, nom_restaurant: str) -> str: | |
| if note <= 2: | |
| consigne_ton = ( | |
| "Le client est mécontent. Adopte un ton empathique et professionnel. " | |
| "Présente des excuses sincères, reprends explicitement le ou les " | |
| "problèmes concrets mentionnés dans l'avis, et propose un contact " | |
| "direct ou un geste commercial pour rétablir la confiance." | |
| ) | |
| elif note == 3: | |
| consigne_ton = ( | |
| "Le client est mitigé. Remercie-le, reconnais les points négatifs " | |
| "qu'il mentionne, valorise les points positifs, et montre que ses " | |
| "remarques seront prises en compte." | |
| ) | |
| else: | |
| consigne_ton = ( | |
| "Le client est satisfait. Adopte un ton chaleureux et reconnaissant. " | |
| "Reprends un ou deux éléments précis qu'il a appréciés et invite-le " | |
| "à revenir." | |
| ) | |
| return f"""Tu es le gérant du restaurant {nom_restaurant}. | |
| Rédige une réponse publique à l'avis client ci-dessous. | |
| Avis du client ({note}/5 étoiles) : | |
| \"\"\"{avis_texte}\"\"\" | |
| Consignes : | |
| - {consigne_ton} | |
| - Réponds en français, en 3 à 5 phrases maximum. | |
| - Personnalise la réponse avec les éléments concrets de l'avis (plats, service, ambiance...). Ne reste jamais générique. | |
| - Ne mentionne jamais que tu es une IA. | |
| - Pas de formule pompeuse, un ton naturel de restaurateur qui répond à ses clients. | |
| - Termine par une signature simple : "L'équipe de {nom_restaurant}". | |
| Réponds uniquement avec le texte de la réponse, sans commentaire ni guillemets.""" | |
| # --------------------------------------------------------------- | |
| # 4. Génération de la réponse avec bascule automatique sur fallback | |
| # --------------------------------------------------------------- | |
| def generer_reponse(avis_texte: str, note: int, nom_restaurant: str): | |
| """Retourne (reponse, source) où source vaut 'llm' ou 'fallback'.""" | |
| note = int(note) | |
| avis_texte = (avis_texte or "").strip() | |
| nom_restaurant = (nom_restaurant or "notre restaurant").strip() | |
| if not avis_texte: | |
| return TEMPLATES_FALLBACK.get(note, TEMPLATES_FALLBACK[3]), "fallback" | |
| if client is None: | |
| return TEMPLATES_FALLBACK.get(note, TEMPLATES_FALLBACK[3]), "fallback" | |
| try: | |
| reponse_api = client.chat.complete( | |
| model=MODEL, | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": construire_prompt(avis_texte, note, nom_restaurant), | |
| } | |
| ], | |
| temperature=0.7, | |
| max_tokens=300, | |
| ) | |
| texte = reponse_api.choices[0].message.content.strip() | |
| if texte: | |
| return texte, "llm" | |
| return TEMPLATES_FALLBACK.get(note, TEMPLATES_FALLBACK[3]), "fallback" | |
| except Exception as e: | |
| print(f"[Avis'IA Resto] Erreur API Mistral, bascule sur template : {e}") | |
| return TEMPLATES_FALLBACK.get(note, TEMPLATES_FALLBACK[3]), "fallback" | |
| # --------------------------------------------------------------- | |
| # 5. Fonction branchée sur l'interface Gradio | |
| # --------------------------------------------------------------- | |
| def repondre_avis(avis_texte, note, nom_restaurant): | |
| reponse, source = generer_reponse(avis_texte, note, nom_restaurant) | |
| if source == "fallback": | |
| info = "ℹ️ Réponse générée en mode standard (modèle IA indisponible)." | |
| else: | |
| info = "✅ Réponse générée par IA (Mistral). Relisez et ajustez avant publication." | |
| return reponse, info | |
| # --------------------------------------------------------------- | |
| # 6. Interface Gradio | |
| # --------------------------------------------------------------- | |
| with gr.Blocks(title="Avis'IA Resto") as demo: | |
| gr.Markdown( | |
| """ | |
| # 🍽️ Avis'IA Resto | |
| ### Réponses aux avis clients assistées par IA, validées par l'humain | |
| Générez une proposition de réponse personnalisée, puis relisez et | |
| ajustez avant publication. L'humain garde toujours la main. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| nom = gr.Textbox( | |
| label="Nom du restaurant", | |
| value="Chez Coralie", | |
| ) | |
| avis = gr.Textbox( | |
| label="Texte de l'avis client", | |
| lines=6, | |
| placeholder="Collez ici l'avis du client...", | |
| ) | |
| etoiles = gr.Slider( | |
| minimum=1, | |
| maximum=5, | |
| step=1, | |
| value=3, | |
| label="Note (étoiles)", | |
| ) | |
| bouton = gr.Button("Générer la réponse", variant="primary") | |
| with gr.Column(): | |
| sortie = gr.Textbox( | |
| label="Réponse proposée (modifiable avant publication)", | |
| lines=8, | |
| interactive=True, | |
| ) | |
| statut = gr.Markdown() | |
| regenerer = gr.Button("🔄 Régénérer une autre version") | |
| bouton.click( | |
| fn=repondre_avis, | |
| inputs=[avis, etoiles, nom], | |
| outputs=[sortie, statut], | |
| ) | |
| regenerer.click( | |
| fn=repondre_avis, | |
| inputs=[avis, etoiles, nom], | |
| outputs=[sortie, statut], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |