madia / app.py
hamba-ho's picture
fix: clear input fields when clearing history
0e144e9
Raw
History Blame Contribute Delete
12.4 kB
"""
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 = """
<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",
)
# 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('<div class="madia-footer">Madia — Propulsé par l\'intelligence collective</div>')
# 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,
)