Multi-Agent / app.py
Danielfonseca1212's picture
Create app.py
cdfde13 verified
# app.py — Multi-Agent Debate System | ATLAS vs NEXUS vs ORACLE
import streamlit as st
import os
st.set_page_config(
page_title="Debate Arena · Multi-Agent",
page_icon="⚔️",
layout="wide",
initial_sidebar_state="expanded",
)
# ── CSS: ARENA THEME ──────────────────────────────────────────
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=IBM+Plex+Mono:wght@400;600&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&display=swap');
html, body, [class*="css"] {
background: #08080e;
color: #d8d8e8;
font-family: 'Crimson Pro', Georgia, serif;
}
#MainMenu, footer, header { visibility: hidden; }
.block-container { padding-top: 1rem; padding-bottom: 2rem; max-width: 1200px; }
/* ── ARENA HEADER ── */
.arena-header {
text-align: center;
padding: 1.5rem 0 0.5rem;
position: relative;
}
.arena-title {
font-family: 'Bebas Neue', sans-serif;
font-size: 4rem;
letter-spacing: 0.12em;
background: linear-gradient(180deg, #ffffff 0%, #888899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin: 0;
}
.arena-sub {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: #44445a;
letter-spacing: 0.25em;
text-transform: uppercase;
margin-top: 0.2rem;
}
/* ── AGENT NAMEPLATES ── */
.nameplate-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1.5rem 0 1rem;
gap: 1rem;
}
.nameplate-atlas {
flex: 1;
background: linear-gradient(135deg, #0a0a1a 0%, #0d1a2e 100%);
border: 1px solid #1a4080;
border-left: 4px solid #2d7fff;
border-radius: 8px;
padding: 0.8rem 1.2rem;
text-align: left;
}
.nameplate-nexus {
flex: 1;
background: linear-gradient(135deg, #1a0a0a 0%, #2e0d0d 100%);
border: 1px solid #801a1a;
border-right: 4px solid #ff2d2d;
border-radius: 8px;
padding: 0.8rem 1.2rem;
text-align: right;
}
.nameplate-oracle {
background: linear-gradient(135deg, #1a1400 0%, #2e2400 100%);
border: 1px solid #806600;
border-bottom: 4px solid #ffd700;
border-radius: 8px;
padding: 0.6rem 1.5rem;
text-align: center;
min-width: 140px;
}
.agent-name {
font-family: 'Bebas Neue', sans-serif;
font-size: 1.6rem;
letter-spacing: 0.1em;
}
.atlas-name { color: #2d7fff; }
.nexus-name { color: #ff2d2d; }
.oracle-name { color: #ffd700; font-size: 1.3rem; }
.agent-role {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.15em;
text-transform: uppercase;
margin-top: 0.1rem;
}
.atlas-role { color: #1a5aaa; }
.nexus-role { color: #aa1a1a; }
.oracle-role { color: #aa9000; }
/* ── SPEECH BUBBLES ── */
.bubble-atlas {
background: #080d18;
border: 1px solid #1a3060;
border-left: 3px solid #2d7fff;
border-radius: 2px 12px 12px 12px;
padding: 1rem 1.3rem;
margin: 0.6rem 0 0.6rem 0;
font-size: 1rem;
line-height: 1.7;
position: relative;
animation: slideInLeft 0.3s ease;
}
.bubble-nexus {
background: #180808;
border: 1px solid #601a1a;
border-right: 3px solid #ff2d2d;
border-radius: 12px 2px 12px 12px;
padding: 1rem 1.3rem;
margin: 0.6rem 0 0.6rem 0;
font-size: 1rem;
line-height: 1.7;
position: relative;
animation: slideInRight 0.3s ease;
}
.bubble-status {
background: #0f0f0f;
border: 1px solid #222;
border-radius: 20px;
padding: 0.3rem 1rem;
margin: 0.4rem auto;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
color: #44445a;
text-align: center;
width: fit-content;
}
.speaker-tag {
font-family: 'Bebas Neue', sans-serif;
font-size: 0.85rem;
letter-spacing: 0.1em;
margin-bottom: 0.4rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.tag-atlas { color: #2d7fff; }
.tag-nexus { color: #ff2d2d; }
.turn-badge {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.6rem;
color: #333355;
border: 1px solid #222233;
padding: 0.1rem 0.4rem;
border-radius: 10px;
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInRight {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
/* ── VERDICT CARD ── */
.verdict-container {
background: linear-gradient(135deg, #0f0e00 0%, #1a1800 100%);
border: 1px solid #806600;
border-top: 4px solid #ffd700;
border-radius: 12px;
padding: 1.5rem 2rem;
margin-top: 1.5rem;
}
.verdict-title {
font-family: 'Bebas Neue', sans-serif;
font-size: 1.2rem;
color: #ffd700;
letter-spacing: 0.2em;
margin-bottom: 1rem;
}
.winner-banner {
font-family: 'Bebas Neue', sans-serif;
font-size: 3rem;
letter-spacing: 0.15em;
text-align: center;
margin: 0.5rem 0;
}
.winner-atlas { color: #2d7fff; text-shadow: 0 0 30px #2d7fff44; }
.winner-nexus { color: #ff2d2d; text-shadow: 0 0 30px #ff2d2d44; }
.winner-empate { color: #ffd700; }
.score-bar-container { margin: 0.8rem 0; }
.score-label {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: #666688;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.2rem;
}
.score-bar-track {
background: #111118;
border-radius: 4px;
height: 8px;
width: 100%;
overflow: hidden;
}
.score-bar-fill-atlas {
height: 100%;
background: linear-gradient(90deg, #1a4080, #2d7fff);
border-radius: 4px;
transition: width 0.8s ease;
}
.score-bar-fill-nexus {
height: 100%;
background: linear-gradient(90deg, #801a1a, #ff2d2d);
border-radius: 4px;
transition: width 0.8s ease;
}
.criteria-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem;
margin-top: 0.8rem;
}
.criteria-item {
background: #0a0a10;
border: 1px solid #1a1a2a;
border-radius: 6px;
padding: 0.5rem 0.8rem;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
}
.crit-name { color: #555577; text-transform: uppercase; letter-spacing: 0.08em; }
.crit-scores { display: flex; gap: 0.8rem; margin-top: 0.2rem; }
.crit-atlas { color: #2d7fff; }
.crit-nexus { color: #ff2d2d; }
.insight-box {
background: #0d0d00;
border: 1px solid #333300;
border-left: 3px solid #ffd700;
border-radius: 4px;
padding: 0.7rem 1rem;
margin-top: 1rem;
font-style: italic;
color: #cccc88;
font-size: 0.95rem;
}
/* ── SIDEBAR ── */
section[data-testid="stSidebar"] {
background: #05050a;
border-right: 1px solid #111120;
}
section[data-testid="stSidebar"] * { color: #9090aa !important; }
/* ── INPUT ── */
.stChatInput textarea {
background: #0a0a14 !important;
border: 1px solid #222233 !important;
border-radius: 8px !important;
color: #d8d8e8 !important;
font-family: 'IBM Plex Mono', monospace !important;
}
.stButton button {
background: #0a0a14 !important;
border: 1px solid #222233 !important;
color: #7070aa !important;
border-radius: 6px !important;
font-family: 'IBM Plex Mono', monospace !important;
font-size: 0.75rem !important;
transition: all 0.2s !important;
}
.stButton button:hover {
border-color: #ffd700 !important;
color: #ffd700 !important;
}
hr { border-color: #111120 !important; }
</style>
""", unsafe_allow_html=True)
# ── SESSION STATE ─────────────────────────────────────────────
for k, v in {
'debates': [],
'openai_key': '',
'running': False,
}.items():
if k not in st.session_state:
st.session_state[k] = v
# ── HELPERS ───────────────────────────────────────────────────
def get_key():
try:
if 'OPENAI_API_KEY' in st.secrets:
return st.secrets['OPENAI_API_KEY']
except Exception:
pass
return os.getenv('OPENAI_API_KEY', st.session_state.openai_key)
# ── SIDEBAR ───────────────────────────────────────────────────
with st.sidebar:
st.markdown("""
<div style='font-family:Bebas Neue;font-size:1.8rem;letter-spacing:0.12em;
background:linear-gradient(180deg,#fff,#666);-webkit-background-clip:text;
-webkit-text-fill-color:transparent;background-clip:text'>DEBATE ARENA</div>
<div style='font-family:IBM Plex Mono;font-size:0.6rem;color:#333355;
letter-spacing:0.2em;text-transform:uppercase'>Multi-Agent System v1.0</div>
""", unsafe_allow_html=True)
st.divider()
st.markdown("#### 🔑 OpenAI API Key")
key_in = st.text_input("", type="password", value=st.session_state.openai_key,
placeholder="sk-...", label_visibility="collapsed")
if key_in:
st.session_state.openai_key = key_in
if get_key():
st.success("✅ Key configurada")
else:
st.warning("Configure a API Key")
st.divider()
st.markdown("#### ⚙️ Configuração")
n_rounds = st.slider("Rounds de debate", 1, 4, 2,
help="Cada round = 1 turno NEXUS + 1 turno ATLAS")
st.divider()
st.markdown("""
<div style='font-family:IBM Plex Mono;font-size:0.65rem;color:#222244'>
<div style='color:#2d7fff;margin-bottom:0.3rem'>▮ ATLAS — Arguer PRO</div>
Defende a posição com dados e lógica.<br><br>
<div style='color:#ff2d2d;margin-bottom:0.3rem'>▮ NEXUS — Arguer CON</div>
Ataca com contra-argumentos e edge cases.<br><br>
<div style='color:#ffd700;margin-bottom:0.3rem'>▮ ORACLE — Judge</div>
Avalia 4 critérios e emite veredicto JSON.
</div>
""", unsafe_allow_html=True)
st.divider()
st.markdown("""
<div style='font-family:IBM Plex Mono;font-size:0.62rem;color:#222233;text-align:center'>
gpt-4o-mini · 3 system prompts<br>
orquestração manual · CPU-only
</div>
""", unsafe_allow_html=True)
st.divider()
if st.button("🗑️ Limpar arena", use_container_width=True):
st.session_state.debates = []
st.rerun()
# ── HEADER ────────────────────────────────────────────────────
st.markdown("""
<div class="arena-header">
<div class="arena-title">⚔ DEBATE ARENA ⚔</div>
<div class="arena-sub">ATLAS vs NEXUS · Julgado por ORACLE · Sistema Multi-Agente</div>
</div>
""", unsafe_allow_html=True)
# ── NAMEPLATES ────────────────────────────────────────────────
st.markdown("""
<div class="nameplate-row">
<div class="nameplate-atlas">
<div class="agent-name atlas-name">ATLAS</div>
<div class="agent-role atlas-role">◈ Arguer PRO · Defende</div>
</div>
<div class="nameplate-oracle">
<div class="agent-name oracle-name">ORACLE</div>
<div class="agent-role oracle-role">◆ Judge</div>
</div>
<div class="nameplate-nexus">
<div class="agent-name nexus-name">NEXUS</div>
<div class="agent-role nexus-role">Ataca · Arguer CON ◈</div>
</div>
</div>
""", unsafe_allow_html=True)
# ── TÓPICOS SUGERIDOS ─────────────────────────────────────────
TOPICS = [
"RAG é superior a Fine-tuning para domínios especializados",
"LLMs vão substituir engenheiros de software até 2030",
"Graph Neural Networks superam Transformers em dados relacionais",
"Modelos open-source já rivalizam com GPT-4 em tarefas práticas",
"Prompting é mais importante que arquitetura do modelo",
"Agentes autônomos de IA são prontos para produção em 2025",
]
if not st.session_state.debates:
st.markdown("""
<div style='font-family:IBM Plex Mono;font-size:0.7rem;color:#333355;
text-transform:uppercase;letter-spacing:0.15em;margin:1rem 0 0.5rem'>
◈ Escolha um tema ou digite o seu
</div>
""", unsafe_allow_html=True)
cols = st.columns(3)
for i, topic in enumerate(TOPICS):
with cols[i % 3]:
if st.button(topic, key=f"t{i}", use_container_width=True):
st.session_state["pending_topic"] = topic
st.rerun()
# ── HISTÓRICO DE DEBATES ──────────────────────────────────────
for debate in st.session_state.debates:
topic = debate["topic"]
st.markdown(f"""
<div style='font-family:IBM Plex Mono;font-size:0.65rem;color:#333355;
text-align:center;text-transform:uppercase;letter-spacing:0.12em;
margin:1.5rem 0 0.5rem'>◆ TEMA: {topic} ◆</div>
""", unsafe_allow_html=True)
for ev in debate.get("events", []):
if ev.get("type") == "status":
st.markdown(f'<div class="bubble-status">⟳ {ev["msg"]}</div>', unsafe_allow_html=True)
elif ev.get("agent") == "ATLAS":
st.markdown(f"""
<div class="bubble-atlas">
<div class="speaker-tag tag-atlas">
ATLAS <span class="turn-badge">T{ev['turn']}</span>
</div>
{ev['content']}
</div>
""", unsafe_allow_html=True)
elif ev.get("agent") == "NEXUS":
st.markdown(f"""
<div class="bubble-nexus">
<div class="speaker-tag tag-nexus" style="justify-content:flex-end">
<span class="turn-badge">T{ev['turn']}</span> NEXUS
</div>
{ev['content']}
</div>
""", unsafe_allow_html=True)
# Veredicto
if debate.get("verdict"):
v = debate["verdict"]
winner = v.get("vencedor", "EMPATE")
pa = v.get("placar_atlas", 0)
pn = v.get("placar_nexus", 0)
winner_class = {
"ATLAS": "winner-atlas",
"NEXUS": "winner-nexus",
"EMPATE": "winner-empate"
}.get(winner, "winner-empate")
crit = v.get("criterios", {})
crit_items = ""
crit_labels = {
"qualidade_argumentos": "Qualidade dos Argumentos",
"uso_de_evidencias": "Uso de Evidências",
"coerencia_logica": "Coerência Lógica",
"poder_de_refutacao": "Poder de Refutação",
}
for key, label in crit_labels.items():
ca = crit.get(key, {}).get("atlas", 0)
cn = crit.get(key, {}).get("nexus", 0)
crit_items += f"""
<div class="criteria-item">
<div class="crit-name">{label}</div>
<div class="crit-scores">
<span class="crit-atlas">ATLAS {ca}/10</span>
<span class="crit-nexus">NEXUS {cn}/10</span>
</div>
</div>"""
st.markdown(f"""
<div class="verdict-container">
<div class="verdict-title">◆ VEREDICTO DE ORACLE ◆</div>
<div class="winner-banner {winner_class}">{winner} VENCE</div>
<div style="display:flex;gap:2rem;margin:1rem 0">
<div style="flex:1">
<div class="score-label">ATLAS — {pa}/10</div>
<div class="score-bar-track">
<div class="score-bar-fill-atlas" style="width:{pa*10}%"></div>
</div>
</div>
<div style="flex:1">
<div class="score-label">NEXUS — {pn}/10</div>
<div class="score-bar-track">
<div class="score-bar-fill-nexus" style="width:{pn*10}%"></div>
</div>
</div>
</div>
<div class="criteria-grid">{crit_items}</div>
<div style="margin-top:1rem;display:grid;grid-template-columns:1fr 1fr;gap:0.8rem">
<div style="font-family:IBM Plex Mono;font-size:0.75rem;color:#666688">
<div style="color:#2d7fff;margin-bottom:0.3rem">ANÁLISE ATLAS</div>
{v.get('analise_atlas','')}
</div>
<div style="font-family:IBM Plex Mono;font-size:0.75rem;color:#666688">
<div style="color:#ff2d2d;margin-bottom:0.3rem">ANÁLISE NEXUS</div>
{v.get('analise_nexus','')}
</div>
</div>
<div style="font-family:IBM Plex Mono;font-size:0.72rem;color:#666644;
margin-top:0.8rem;border-top:1px solid #222200;padding-top:0.6rem">
◈ PONTO DECISIVO: {v.get('ponto_decisivo','')}
</div>
<div class="insight-box">
💡 {v.get('licao','')}
</div>
</div>
""", unsafe_allow_html=True)
st.divider()
# ── INPUT ─────────────────────────────────────────────────────
pending = st.session_state.pop("pending_topic", None)
topic = st.chat_input("Digite o tema do debate...") or pending
if topic:
if not get_key():
st.warning("Configure a OpenAI API Key na sidebar.")
st.stop()
from agents import DebateEngine
debate_record = {"topic": topic, "events": [], "verdict": None}
st.session_state.debates.append(debate_record)
# Header do debate
topic_ph = st.empty()
topic_ph.markdown(f"""
<div style='font-family:IBM Plex Mono;font-size:0.65rem;color:#333355;
text-align:center;text-transform:uppercase;letter-spacing:0.12em;
margin:1.5rem 0 0.5rem'>◆ TEMA: {topic} ◆</div>
""", unsafe_allow_html=True)
engine = DebateEngine(get_key())
placeholders = []
for event in engine.run_debate(topic, n_rounds=n_rounds):
if event.get("type") == "status":
ph = st.empty()
ph.markdown(f'<div class="bubble-status">⟳ {event["msg"]}</div>', unsafe_allow_html=True)
debate_record["events"].append(event)
elif event.get("type") == "verdict":
verdict = event["data"]
debate_record["verdict"] = verdict
v = verdict
winner = v.get("vencedor", "EMPATE")
pa = v.get("placar_atlas", 0)
pn = v.get("placar_nexus", 0)
winner_class = {"ATLAS": "winner-atlas", "NEXUS": "winner-nexus", "EMPATE": "winner-empate"}.get(winner, "winner-empate")
crit = v.get("criterios", {})
crit_labels = {
"qualidade_argumentos": "Qualidade dos Argumentos",
"uso_de_evidencias": "Uso de Evidências",
"coerencia_logica": "Coerência Lógica",
"poder_de_refutacao": "Poder de Refutação",
}
crit_items = ""
for key, label in crit_labels.items():
ca = crit.get(key, {}).get("atlas", 0)
cn = crit.get(key, {}).get("nexus", 0)
crit_items += f"""
<div class="criteria-item">
<div class="crit-name">{label}</div>
<div class="crit-scores">
<span class="crit-atlas">ATLAS {ca}/10</span>
<span class="crit-nexus">NEXUS {cn}/10</span>
</div>
</div>"""
st.markdown(f"""
<div class="verdict-container">
<div class="verdict-title">◆ VEREDICTO DE ORACLE ◆</div>
<div class="winner-banner {winner_class}">{winner} VENCE</div>
<div style="display:flex;gap:2rem;margin:1rem 0">
<div style="flex:1">
<div class="score-label">ATLAS — {pa}/10</div>
<div class="score-bar-track"><div class="score-bar-fill-atlas" style="width:{pa*10}%"></div></div>
</div>
<div style="flex:1">
<div class="score-label">NEXUS — {pn}/10</div>
<div class="score-bar-track"><div class="score-bar-fill-nexus" style="width:{pn*10}%"></div></div>
</div>
</div>
<div class="criteria-grid">{crit_items}</div>
<div style="margin-top:1rem;display:grid;grid-template-columns:1fr 1fr;gap:0.8rem">
<div style="font-family:IBM Plex Mono;font-size:0.75rem;color:#666688">
<div style="color:#2d7fff;margin-bottom:0.3rem">ANÁLISE ATLAS</div>{v.get('analise_atlas','')}
</div>
<div style="font-family:IBM Plex Mono;font-size:0.75rem;color:#666688">
<div style="color:#ff2d2d;margin-bottom:0.3rem">ANÁLISE NEXUS</div>{v.get('analise_nexus','')}
</div>
</div>
<div style="font-family:IBM Plex Mono;font-size:0.72rem;color:#666644;
margin-top:0.8rem;border-top:1px solid #222200;padding-top:0.6rem">
◈ PONTO DECISIVO: {v.get('ponto_decisivo','')}
</div>
<div class="insight-box">💡 {v.get('licao','')}</div>
</div>
""", unsafe_allow_html=True)
else:
# Speech bubble ao vivo
agent = event.get("agent", "")
content = event.get("content", "")
turn = event.get("turn", 0)
debate_record["events"].append(event)
if agent == "ATLAS":
st.markdown(f"""
<div class="bubble-atlas">
<div class="speaker-tag tag-atlas">
ATLAS <span class="turn-badge">T{turn}</span>
</div>
{content}
</div>""", unsafe_allow_html=True)
elif agent == "NEXUS":
st.markdown(f"""
<div class="bubble-nexus">
<div class="speaker-tag tag-nexus" style="justify-content:flex-end">
<span class="turn-badge">T{turn}</span> NEXUS
</div>
{content}
</div>""", unsafe_allow_html=True)