""" Madia — Multi-Agent Debate Application. Gradio interface where 4 AI agents (Chef, Nutrition, Éco, Modérateur) debate to propose the best meal for the user's request. """ import gradio as gr import spaces from langchain_core.messages import HumanMessage from graph.workflow import build_graph from utils.helpers import extract_final_decision, format_message_for_display # Agent name-to-display mapping for step-by-step updates AGENT_NAMES = ["Chef cuisinier", "Nutritionniste", "Écologiste", "Modérateur"] # Build the graph once at startup graph = build_graph() @spaces.GPU(duration=10) def dummy_gpu_function(): pass def run_debate(user_request: str, location: str, history: list, agent_messages: list): """ Generator function that runs the debate and yields updates step-by-step. Each time an agent speaks, the chatbot history is updated and yielded so the user sees the debate unfold progressively. Args: user_request: The user's meal request. history: Current Gradio chatbot history. agent_messages: LangChain messages state. Yields: Tuples of (chatbot_history, decision_markdown, status_text, agent_messages) after each agent speaks. """ if not user_request.strip(): yield history, "", "Veuillez entrer une demande.", agent_messages return # Add the user's message to the chatbot display history = history + [{"role": "user", "content": user_request}] yield history, "", "Lancement du débat...", agent_messages # Add the new request to the agent history agent_messages = agent_messages + [HumanMessage(content=user_request)] # Prepare the initial state for the graph initial_state = { "messages": agent_messages, "user_request": user_request, "location": location, "current_round": 1, "decision_reached": False, "final_decision": "", } # Stream the graph execution step by step final_decision_text = "" try: for event in graph.stream(initial_state, stream_mode="updates"): # Each event is a dict: {node_name: state_update} for node_name, state_update in event.items(): if node_name == "__start__": continue # Get the new messages from this step new_messages = state_update.get("messages", []) current_round = state_update.get("current_round", None) # Append to our persistent LangChain history agent_messages = agent_messages + new_messages for msg in new_messages: agent_name = getattr(msg, "name", node_name.capitalize()) display_msg = format_message_for_display( agent_name, msg.content ) history = history + [display_msg] # Map node name to display info agent_display = { "chef": "Chef cuisinier", "nutrition": "Nutritionniste", "eco": "Écologiste", "moderator": "Modérateur", } agent_status = agent_display.get(node_name, "") status = f"{agent_status} a parlé" # Check for final decision if state_update.get("decision_reached", False): final_decision_text = state_update.get( "final_decision", "" ) status = "Décision finale atteinte" yield history, format_decision_panel(final_decision_text), status, agent_messages except Exception as e: error_msg = f"Erreur : {str(e)}" history = history + [ {"role": "assistant", "content": error_msg} ] yield history, "", error_msg, agent_messages return # If no decision was found in the stream, try to extract from last message if not final_decision_text and history: last_content = history[-1].get("content", "") final_decision_text = extract_final_decision(last_content) or "" # Ultimate fallback: use the last agent message as the decision if not final_decision_text and history: for msg in reversed(history): if msg.get("role") == "assistant" and msg.get("content", "").strip(): final_decision_text = msg["content"] break final_status = "Débat terminé" yield history, format_decision_panel(final_decision_text), final_status, agent_messages def format_decision_panel(decision_text: str) -> str: """ Format the final decision as a styled Markdown panel. Args: decision_text: Raw decision text from the moderator. Returns: Formatted Markdown string for display. """ if not decision_text: return "" return f""" --- {decision_text} --- """ # ───────────────────────────────────────────────────────────── # Gradio Interface # ───────────────────────────────────────────────────────────── TITLE = """

Madia

Intelligence collective pour vos choix gastronomiques

Nos experts — chef cuisinier, nutritionniste, écologiste et modérateur — débattent ensemble pour vous proposer le repas idéal, adapté à vos envies et vos valeurs.

""" EXAMPLES = [ "Propose un repas équilibré pour 4 personnes ce soir en été", "Je veux un déjeuner rapide et sain pour le bureau", "Un dîner romantique pour 2 personnes avec un budget de 30€", "Un repas végétarien pour un dimanche en famille", "Que manger après une séance de sport intense ?", ] CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); /* ── Global Reset ── */ .gradio-container { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; max-width: 960px !important; margin: 0 auto !important; } /* ── Header ── */ .madia-header { text-align: center; padding: 32px 16px 24px; } .madia-header h1 { font-size: 2.8rem; font-weight: 700; letter-spacing: -0.03em; margin: 0 0 8px; background: linear-gradient(135deg, #6366f1, #8b5cf6, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .madia-tagline { font-size: 1.1rem; font-weight: 500; color: #64748b; margin: 0 0 12px; letter-spacing: 0.01em; } .madia-desc { font-size: 0.92rem; color: #94a3b8; max-width: 600px; margin: 0 auto; line-height: 1.6; } /* ── Input Area ── */ .input-area textarea { border-radius: 12px !important; border: 1.5px solid #e2e8f0 !important; transition: border-color 0.2s ease, box-shadow 0.2s ease !important; font-size: 0.95rem !important; } .input-area textarea:focus { border-color: #8b5cf6 !important; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1) !important; } /* ── Buttons ── */ .primary-btn { background: linear-gradient(135deg, #6366f1, #8b5cf6) !important; border: none !important; border-radius: 12px !important; color: white !important; font-weight: 600 !important; font-size: 0.95rem !important; letter-spacing: 0.02em !important; padding: 12px 24px !important; transition: all 0.25s ease !important; box-shadow: 0 4px 14px rgba(99, 102, 241, 0.25) !important; } .primary-btn:hover { transform: translateY(-1px) !important; box-shadow: 0 6px 20px rgba(99, 102, 241, 0.35) !important; } .secondary-btn { background: transparent !important; border: 1.5px solid #e2e8f0 !important; border-radius: 12px !important; color: #64748b !important; font-weight: 500 !important; font-size: 0.85rem !important; padding: 8px 16px !important; transition: all 0.2s ease !important; } .secondary-btn:hover { border-color: #cbd5e1 !important; color: #475569 !important; background: #f8fafc !important; } /* ── Status Bar ── */ .status-bar { text-align: center; padding: 8px 0; } .status-bar p { font-size: 0.88rem; color: #64748b; font-weight: 500; } /* ── Chatbot ── */ .chatbot-area .chatbot { border-radius: 16px !important; border: 1.5px solid #e2e8f0 !important; } /* ── Decision Panel ── */ .decision-panel { border-radius: 16px !important; border: 1.5px solid #e2e8f0 !important; padding: 4px !important; } /* ── Examples ── */ .examples-section { margin-top: 8px; } .examples-section .label-wrap span { font-size: 0.9rem !important; font-weight: 600 !important; color: #475569 !important; } /* ── Footer ── */ footer { display: none !important; } .madia-footer { text-align: center; padding: 20px 0 8px; font-size: 0.8rem; color: #94a3b8; } .madia-footer a { color: #8b5cf6; text-decoration: none; font-weight: 500; } """ with gr.Blocks( title="Madia — Votre assistant gastronomique", ) as demo: gr.HTML(TITLE) with gr.Row(equal_height=False): with gr.Column(scale=4, elem_classes="input-area"): user_input = gr.Textbox( label="Votre demande", placeholder="Ex: Propose un repas équilibré pour 4 personnes ce soir...", lines=2, max_lines=4, ) user_location = gr.Textbox( label="Votre ville ou région (optionnel)", placeholder="Ex: Paris, Kinshasa, London...", lines=1, ) with gr.Column(scale=1, min_width=140): submit_btn = gr.Button( "Lancer le débat", variant="primary", size="lg", elem_classes="primary-btn", ) clear_btn = gr.Button( "Effacer l'historique", variant="secondary", elem_classes="secondary-btn", ) status_text = gr.Markdown( "*En attente de votre demande...*", elem_classes="status-bar", ) # Hidden state to store LangChain BaseMessage history across runs agent_messages = gr.State([]) chatbot = gr.Chatbot( label="Débat entre experts", height=480, elem_classes="chatbot-area", ) decision_output = gr.Markdown( label="Décision Finale", value="", elem_classes="decision-panel", ) with gr.Accordion("Exemples de demandes", open=False, elem_classes="examples-section"): gr.Examples( examples=EXAMPLES, inputs=user_input, ) gr.HTML('') # Wire up the UI def clear_all(): return [], "", "*En attente de votre demande...*", [], "", "" clear_btn.click( fn=clear_all, inputs=[], outputs=[chatbot, decision_output, status_text, agent_messages, user_input, user_location], ) submit_btn.click( fn=run_debate, inputs=[user_input, user_location, chatbot, agent_messages], outputs=[chatbot, decision_output, status_text, agent_messages], ) user_input.submit( fn=run_debate, inputs=[user_input, user_location, chatbot, agent_messages], outputs=[chatbot, decision_output, status_text, agent_messages], ) user_location.submit( fn=run_debate, inputs=[user_input, user_location, chatbot, agent_messages], outputs=[chatbot, decision_output, status_text, agent_messages], ) if __name__ == "__main__": demo.launch( css=CSS, theme=gr.themes.Soft( primary_hue="violet", secondary_hue="slate", neutral_hue="slate", font=gr.themes.GoogleFont("Inter"), ), ssr_mode=False, )