| """ |
| 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_NAMES = ["Chef cuisinier", "Nutritionniste", "Écologiste", "Modérateur"] |
|
|
| |
| 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 |
|
|
| |
| history = history + [{"role": "user", "content": user_request}] |
| yield history, "", "Lancement du débat...", agent_messages |
|
|
| |
| agent_messages = agent_messages + [HumanMessage(content=user_request)] |
|
|
| |
| initial_state = { |
| "messages": agent_messages, |
| "user_request": user_request, |
| "location": location, |
| "current_round": 1, |
| "decision_reached": False, |
| "final_decision": "", |
| } |
|
|
| |
| final_decision_text = "" |
|
|
| try: |
| for event in graph.stream(initial_state, stream_mode="updates"): |
| |
| for node_name, state_update in event.items(): |
| if node_name == "__start__": |
| continue |
|
|
| |
| new_messages = state_update.get("messages", []) |
| current_round = state_update.get("current_round", None) |
|
|
| |
| 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] |
|
|
| |
| 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é" |
|
|
| |
| 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 not final_decision_text and history: |
| last_content = history[-1].get("content", "") |
| final_decision_text = extract_final_decision(last_content) or "" |
|
|
| |
| 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} |
| |
| --- |
| """ |
|
|
|
|
| |
| |
| |
|
|
| TITLE = """ |
| <div class="madia-header"> |
| <h1>Madia</h1> |
| <p class="madia-tagline">Intelligence collective pour vos choix gastronomiques</p> |
| <p class="madia-desc">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.</p> |
| </div> |
| """ |
|
|
| 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", |
| ) |
|
|
| |
| 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('<div class="madia-footer">Madia — Propulsé par l\'intelligence collective</div>') |
|
|
| |
| 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, |
| ) |
|
|