
Proyecto Cavendish: Creando una Arena Multi-Agente LLM en un Solo Archivo Python
En el campo de la Inteligencia Artificial, la interacción entre múltiples agentes coordinados es uno de los terrenos más fascinantes de explorar. Hoy presentamos el Proyecto Cavendish, una aplicación integral en Python que levanta un servidor de backend y una interfaz gráfica interactiva en el puerto 8062 para simular debates entre modelos de lenguaje (LLM) con intervención humana en tiempo real.
1. ¿Por qué “Proyecto Cavendish” y quién fue Margaret Cavendish?
Este proyecto rinde homenaje a Margaret Cavendish (1623–1673), Duquesa de Newcastle, una de las figuras más extraordinarias y adelantadas a su tiempo de la historia del pensamiento occidental.
- Pionera de la Ciencia Ficción: En 1666, publicó “The Blazing World” (El mundo resplandeciente), obra considerada por la crítica literaria e historiadores como la primera novela de ciencia ficción de la historia. En ella, describe un viaje a un mundo paralelo habitado por seres híbridos parlantes y sabios con los que la protagonista debate sobre ciencia, filosofía, teología y la estructura del universo.
- Rompiendo Barreras Científicas: En una época en la que las mujeres tenían vetado el acceso al ámbito académico formal, Margaret Cavendish fue la primera mujer en ser admitida a una sesión oficial de la Royal Society de Londres (1667), desafiando a las mentes más brillantes de su época como Robert Hooke y Christiaan Huygens.
- Filosofía del “Mundo Espejo”: Cavendish defendió que la mente humana puede construir universos virtuales y mundos de debate donde explorar ideas complejas sin las ataduras físicas del momento.
Siguiendo su espíritu visionario de confrontación dialéctica y creación de mundos sintéticos, el Proyecto Cavendish recrea un “mundo espejo” donde dos entidades de IA defienden posturas filosóficas, éticas o técnicas contrapuestas en un entorno controlado.
2. ¿Cómo se “juega” a la Arena de Debate?
La aplicación funciona como un simulador dialéctico e interactivo que permite observar cómo razonan, argumentan e improvisan dos modelos de lenguaje con directrices contrapuestas.
El flujo de uso se divide en 4 pasos sencillos:
- Selección de Contendientes:Puedes elegir entre tres parejas preconfiguradas o editar directamente las directrices (System Prompts) de cada agente:
- 🛡️ Hacker Ético vs. 💻 Ingeniero de Software: Seguridad restrictiva e integridades del ENS frente a velocidad de despliegue y rendimiento.
- 🏛️ Filósofo del Futuro vs. 🚀 Científico Optimista: Límites éticos y cautela humanista frente a aceleración tecnológica.
- 🌿 Ecologista Radical vs. ⚡ Tecno-Solucionista: Cuidado del consumo energético y decrecimiento frente a geoingeniería e IA optimizadora.
- Planteamiento del Dilema:Escribe el problema central que los agentes van a debatir (por ejemplo: “¿Se debe desplegar un modelo de IA en el sistema sanitario sin auditoría previa?”).
- Orquestación del Debate (Bucle Automático o Paso a Paso):
- Iniciar Debate: Arranca un bucle donde los agentes responden alternadamente, enviando el contexto histórico al backend y actualizando el panel en tiempo real.
- Turno (+1): Permite hacer avanzar la conversación respuesta a respuesta para analizar en detalle cada argumento.
- Inyección de Eventos (Human-In-The-Loop / HITL):El usuario actúa como Moderador del Destino. En cualquier momento del debate, puedes escribir un evento inesperado (ej. “¡Se acaba de filtrar una base de datos sensible!” o “¡El presupuesto se ha recortado a la mitad!”). Al inyectar el evento, el sistema altera la conversación obligando al agente activo a reaccionar de inmediato sin romper su personaje.

3. Código Fuente Completo: app_cavendish.py
Copia este código y guárdalo en un único archivo llamado app_cavendish.py. Incluye tanto el backend FastAPI en el puerto 8062 como el cliente web interactivo con Tailwind CSS y FontAwesome.
Python
# app_cavendish.py
import os
import urllib.request
import json
from typing import List, Optional
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
app = FastAPI(
title="Proyecto Cavendish - Arena Multi-Agente LLM",
description="Servidor web y API para orquestación de debate de IAs en tiempo real (Puerto 8062)",
version="1.0.0"
)
# Permitir peticiones CORS desde cualquier origen
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Configuración del motor LLM por defecto (LM Studio / Ollama / OpenAI local)
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "http://localhost:1234/v1/chat/completions")
LLM_MODEL_NAME = os.getenv("LLM_MODEL_NAME", "local-model")
# Modelos Pydantic para la API
class Message(BaseModel):
role: str # "system", "user", "assistant"
content: str
class AgentConfig(BaseModel):
name: str = Field(..., example="Hacker Ético")
system_prompt: str = Field(..., example="Eres un auditor de ciberseguridad prudente...")
class TurnRequest(BaseModel):
topic: str = Field(..., example="¿Se debe desplegar IA en sanidad sin auditoría previa?")
active_agent: AgentConfig
opponent_agent: AgentConfig
chat_history: List[Message] = []
human_injection: Optional[str] = None # Intervención del Moderador (HITL)
def query_llm_provider(messages: List[dict]) -> str:
"""Envía la conversación al LLM local (LM Studio/Ollama) y devuelve la respuesta."""
payload = {
"model": LLM_MODEL_NAME,
"messages": messages,
"temperature": 0.7,
"max_tokens": 250
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
LLM_BASE_URL,
data=data,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=30) as response:
res_body = json.loads(response.read().decode("utf-8"))
return res_body["choices"][0]["message"]["content"]
except Exception:
# Modo Fallback / Simulación si no hay un servidor LLM en ejecución en el puerto 1234
agente_nombre = messages[0]['content'].split('\n')[0]
return (
f"Desde mi perspectiva estratégica como {agente_nombre}, considero fundamental analizar los riesgos. "
f"Debemos garantizar un equilibrio riguroso sobre este dilema sin comprometer los principios de nuestro rol."
)
# ==============================================================================
# FRONTEND INTERFAZ WEB (Servida directamente en la raíz "/")
# ==============================================================================
HTML_CONTENT = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proyecto CAVENDISH | Arena Multi-Agente IA (Puerto :8062)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--junta-green: #007A33;
--junta-green-dark: #005826;
--junta-green-light: #EAF4EC;
}
.bg-junta { background-color: var(--junta-green); }
.bg-junta-dark { background-color: var(--junta-green-dark); }
.bg-junta-light { background-color: var(--junta-green-light); }
.text-junta { color: var(--junta-green); }
.border-junta { border-color: var(--junta-green); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #007A33; border-radius: 3px; }
.typing-dot { animation: typing 1.4s infinite ease-in-out both; }
.typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
</style>
</head>
<body class="bg-gray-50 text-gray-800 font-sans min-h-screen flex flex-col">
<!-- HEADER INSTITUCIONAL: PROYECTO CAVENDISH -->
<header class="bg-junta text-white shadow-md border-b-4 border-emerald-600">
<div class="max-w-7xl mx-auto px-4 py-3 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center space-x-3">
<div class="bg-white p-2 rounded-lg text-junta font-black text-xl flex items-center justify-center w-10 h-10 shadow">
JA
</div>
<div>
<div class="flex items-center gap-2">
<span class="text-xs tracking-wider uppercase opacity-90 font-semibold">Junta de Andalucía — Innovación Educativa</span>
<span class="px-2 py-0.5 bg-emerald-800 text-[10px] font-mono rounded text-emerald-200 border border-emerald-600">Puerto :8062</span>
</div>
<h1 class="text-xl font-bold tracking-tight flex items-center gap-2">
<span>PROYECTO CAVENDISH</span>
<span class="text-xs bg-amber-400 text-gray-900 px-2 py-0.5 rounded-full font-extrabold uppercase">Arena Multi-Agente LLM</span>
</h1>
</div>
</div>
<div class="flex items-center space-x-2">
<span id="status-badge" class="px-3 py-1 bg-emerald-800 text-emerald-100 rounded-full text-xs font-semibold flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-300 animate-pulse"></span>
Servidor app_cavendish.py Listo (:8062)
</span>
</div>
</div>
</header>
<!-- CONTENIDO PRINCIPAL -->
<main class="max-w-7xl mx-auto px-4 py-6 flex-1 w-full space-y-6">
<!-- HOMENAJE CIENTÍFICO + SELECTOR DE PAREJAS -->
<section class="bg-white p-5 rounded-xl shadow-sm border border-gray-200 space-y-4">
<div class="bg-gradient-to-r from-emerald-900 via-junta to-emerald-800 text-white p-3.5 rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-amber-400/20 flex items-center justify-center text-amber-300 text-xl font-bold border border-amber-300/30">
<i class="fa-solid fa-shuttle-space"></i>
</div>
<div>
<div class="text-[11px] uppercase tracking-wider text-emerald-200 font-semibold">Científica Homenajeada en el Proyecto</div>
<div class="text-sm font-bold">Margaret Cavendish (1623 - 1673) — Autora de "The Blazing World" (1ª Novela Sci-Fi, 1666)</div>
</div>
</div>
<span class="hidden md:inline-block text-xs text-emerald-100 italic bg-white/10 px-3 py-1 rounded">
"Prefiero crear mundos propios antes que conquistar los de otros."
</span>
</div>
<h2 class="text-sm font-bold text-gray-600 uppercase tracking-wider flex items-center gap-2 pt-1">
<i class="fa-solid fa-users-line text-junta"></i> 1. Selecciona la Pareja de Contendientes
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<button onclick="loadPreset(0)" id="preset-0" class="preset-btn p-3.5 rounded-lg border-2 text-left transition flex flex-col justify-between hover:border-junta border-junta bg-junta-light shadow-sm">
<div class="font-bold text-sm text-gray-900 mb-1">🛡️ vs 💻</div>
<div class="text-xs font-semibold text-junta">Hacker Ético vs. Ingeniero de Software</div>
<p class="text-[11px] text-gray-600 mt-1">Seguridad restrictiva vs. Despliegue rápido y rendimiento.</p>
</button>
<button onclick="loadPreset(1)" id="preset-1" class="preset-btn p-3.5 rounded-lg border-2 border-gray-200 text-left transition flex flex-col justify-between hover:border-junta bg-white">
<div class="font-bold text-sm text-gray-900 mb-1">🏛️ vs 🚀</div>
<div class="text-xs font-semibold text-gray-800">Filósofo del Futuro vs. Científico Optimista</div>
<p class="text-[11px] text-gray-600 mt-1">Límites éticos y humanismo vs. Aceleración e innovación.</p>
</button>
<button onclick="loadPreset(2)" id="preset-2" class="preset-btn p-3.5 rounded-lg border-2 border-gray-200 text-left transition flex flex-col justify-between hover:border-junta bg-white">
<div class="font-bold text-sm text-gray-900 mb-1">🌿 vs ⚡</div>
<div class="text-xs font-semibold text-gray-800">Ecologista Radical vs. Tecno-Solucionista</div>
<p class="text-[11px] text-gray-600 mt-1">Decrecimiento y cautela vs. Geoingeniería e IA verde.</p>
</button>
</div>
<!-- CAMPO DE TEMA/DILEMA -->
<div class="flex flex-col sm:flex-row gap-3 items-end pt-2">
<div class="flex-1 w-full">
<label for="topic-input" class="block text-xs font-bold text-gray-700 uppercase mb-1">Dilema o Problema a Debate:</label>
<input type="text" id="topic-input" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-junta focus:outline-none" value="¿Se debe desplegar un modelo de IA en el sistema sanitario autonómico sin auditoría previa?">
</div>
<button onclick="resetDebate()" class="w-full sm:w-auto px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-semibold text-sm rounded-lg border border-gray-300 transition flex items-center justify-center gap-2">
<i class="fa-solid fa-rotate-left"></i> Cargar Dilema
</button>
</div>
</section>
<!-- PANEL DE CONTROL Y SIMULACIÓN -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- CONFIGURACIÓN DE AGENTES -->
<section class="lg:col-span-4 space-y-4">
<div class="bg-white p-4 rounded-xl border-l-4 border-emerald-600 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-2">
<span id="agentA-title" class="font-bold text-emerald-800 text-sm">🛡️ Agente A: Hacker Ético</span>
<span class="text-[10px] bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded font-mono font-bold">Rol 1</span>
</div>
<label class="block text-[11px] font-semibold text-gray-500 uppercase mb-1">System Prompt (Directriz):</label>
<textarea id="agentA-prompt" rows="3" class="w-full p-2 border border-gray-200 rounded text-xs bg-gray-50 font-mono focus:bg-white focus:ring-1 focus:ring-junta focus:outline-none resize-y"></textarea>
</div>
<div class="bg-white p-4 rounded-xl border-l-4 border-teal-600 shadow-sm border border-gray-200">
<div class="flex items-center justify-between mb-2">
<span id="agentB-title" class="font-bold text-teal-800 text-sm">💻 Agente B: Ingeniero Software</span>
<span class="text-[10px] bg-teal-100 text-teal-800 px-2 py-0.5 rounded font-mono font-bold">Rol 2</span>
</div>
<label class="block text-[11px] font-semibold text-gray-500 uppercase mb-1">System Prompt (Directriz):</label>
<textarea id="agentB-prompt" rows="3" class="w-full p-2 border border-gray-200 rounded text-xs bg-gray-50 font-mono focus:bg-white focus:ring-1 focus:ring-junta focus:outline-none resize-y"></textarea>
</div>
<!-- CONTROL HITL -->
<div class="bg-amber-50/80 p-4 rounded-xl border border-amber-200 shadow-sm space-y-3">
<h3 class="text-xs font-bold text-amber-900 uppercase tracking-wider flex items-center gap-1.5">
<i class="fa-solid fa-user-gear text-amber-600"></i> Control Moderador (HITL)
</h3>
<div>
<label class="block text-[11px] font-bold text-amber-900 mb-1">⚡ Inyectar Evento / Giro Inesperado:</label>
<div class="flex gap-2">
<input type="text" id="event-input" placeholder="Ej: ¡Se ha filtrado la base de datos!" class="flex-1 px-2.5 py-1.5 border border-amber-300 rounded text-xs bg-white focus:outline-none">
<button onclick="injectEvent()" class="px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white font-bold text-xs rounded shadow-sm">Inyectar</button>
</div>
</div>
</div>
</section>
<!-- ARENA DE DEBATE EN VIVO -->
<section class="lg:col-span-8 flex flex-col bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden min-h-[520px]">
<div class="p-3.5 bg-gray-100 border-b border-gray-200 flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center space-x-2">
<button id="btn-play" onclick="startDebate()" class="px-4 py-1.5 bg-junta hover:bg-junta-dark text-white font-bold text-xs rounded shadow transition flex items-center gap-1.5">
<i class="fa-solid fa-play"></i> Iniciar Debate
</button>
<button id="btn-pause" onclick="pauseDebate()" disabled class="px-3 py-1.5 bg-gray-200 text-gray-400 font-bold text-xs rounded cursor-not-allowed transition flex items-center gap-1.5">
<i class="fa-solid fa-pause"></i> Pausar
</button>
<button id="btn-step" onclick="stepDebate()" class="px-3 py-1.5 bg-emerald-100 hover:bg-emerald-200 text-emerald-900 font-semibold text-xs rounded border border-emerald-300 transition flex items-center gap-1.5">
<i class="fa-solid fa-forward-step"></i> Turno (+1)
</button>
</div>
<div class="flex items-center space-x-4 text-xs font-mono text-gray-600">
<span>Backend: <strong class="text-emerald-700 font-bold">app_cavendish.py (:8062)</strong></span>
<span>Turno: <strong id="turn-counter" class="text-junta font-bold">0</strong></span>
</div>
</div>
<!-- CHAT -->
<div id="chat-feed" class="flex-1 p-4 space-y-4 overflow-y-auto max-h-[440px] bg-gray-50/50">
<div id="empty-state" class="text-center py-12 px-4">
<div class="w-12 h-12 rounded-full bg-emerald-100 text-junta flex items-center justify-center mx-auto mb-3 text-xl">
<i class="fa-solid fa-comments"></i>
</div>
<h3 class="text-sm font-bold text-gray-700">Arena de Debate Lista (Proyecto CAVENDISH)</h3>
<p class="text-xs text-gray-500 max-w-md mx-auto mt-1">Haz clic en <strong>"Iniciar Debate"</strong> para ejecutar las llamadas al backend en <code>app_cavendish.py</code> (:8062).</p>
</div>
</div>
<div id="typing-indicator" class="hidden px-4 py-2 bg-gray-100 border-t border-gray-200 text-xs text-gray-500 flex items-center space-x-2">
<span id="typing-agent-name" class="font-bold text-junta">Agente A</span> está consultando al backend / LLM...
<div class="flex space-x-1 items-center ml-2">
<div class="w-1.5 h-1.5 bg-junta rounded-full typing-dot"></div>
<div class="w-1.5 h-1.5 bg-junta rounded-full typing-dot"></div>
<div class="w-1.5 h-1.5 bg-junta rounded-full typing-dot"></div>
</div>
</div>
</section>
</div>
</main>
<footer class="bg-gray-800 text-gray-300 text-xs py-4 mt-auto border-t border-gray-700">
<div class="max-w-7xl mx-auto px-4 flex justify-between items-center">
<div><span class="font-semibold text-white">PROYECTO CAVENDISH</span> — Junta de Andalucía</div>
<div class="font-mono">app_cavendish.py Activo en :8062</div>
</div>
</footer>
<script>
const PRESETS = [
{
id: 0,
nameA: "Hacker Ético", iconA: "🛡️",
promptA: "Eres un auditor de Ciberseguridad Red Team. Tu prioridad es la protección estricta de datos y el cumplimiento normativo.",
nameB: "Ingeniero Software", iconB: "💻",
promptB: "Eres un Ingeniero Lead de Software. Priorizas la velocidad de despliegue, el rendimiento y la utilidad directa para el usuario."
},
{
id: 1,
nameA: "Filósofo del Futuro", iconA: "🏛️",
promptA: "Eres un filósofo de la bioética. Cuestionas los riesgos de la IA automatizada y defiendes la primacía del juicio humano.",
nameB: "Científico Optimista", iconB: "🚀",
promptB: "Eres un investigador de tecnología avanzada. Sostienes que la aceleración científica resuelve la mayoría de problemas sociales."
},
{
id: 2,
nameA: "Ecologista Radical", iconA: "🌿",
promptA: "Eres un científico ambiental. Denuncias el consumo energético excesivo de los centros de datos y defiendes la sostenibilidad.",
nameB: "Tecno-Solucionista", iconB: "⚡",
promptB: "Eres un experto en IA verde. Afirmas que la supercomputación es la única herramienta para optimizar la transición energética."
}
];
let currentPreset = 0, isRunning = false, turn = 0, currentAgent = 'A', timer = null, pendingEvent = null;
let chatHistory = [];
document.addEventListener("DOMContentLoaded", () => loadPreset(0));
function loadPreset(idx) {
pauseDebate();
currentPreset = idx;
const p = PRESETS[idx];
document.getElementById('agentA-title').innerText = `${p.iconA} Agente A: ${p.nameA}`;
document.getElementById('agentA-prompt').value = p.promptA;
document.getElementById('agentB-title').innerText = `${p.iconB} Agente B: ${p.nameB}`;
document.getElementById('agentB-prompt').value = p.promptB;
resetDebate();
}
function resetDebate() {
pauseDebate();
turn = 0; currentAgent = 'A'; chatHistory = []; pendingEvent = null;
document.getElementById('turn-counter').innerText = '0';
document.getElementById('chat-feed').innerHTML = `
<div id="empty-state" class="text-center py-12 px-4">
<div class="w-12 h-12 rounded-full bg-emerald-100 text-junta flex items-center justify-center mx-auto mb-3 text-xl">
<i class="fa-solid fa-comments"></i>
</div>
<h3 class="text-sm font-bold text-gray-700">Arena de Debate Lista (Proyecto CAVENDISH)</h3>
<p class="text-xs text-gray-500 max-w-md mx-auto mt-1">Haz clic en <strong>"Iniciar Debate"</strong> para ejecutar las llamadas al backend en <code>app_cavendish.py</code> (:8062).</p>
</div>
`;
}
function startDebate() {
if (isRunning) return;
isRunning = true;
document.getElementById('btn-play').disabled = true;
document.getElementById('btn-pause').disabled = false;
document.getElementById('btn-pause').className = "px-3 py-1.5 bg-amber-600 text-white hover:bg-amber-700 font-bold text-xs rounded transition";
runLoop();
}
function pauseDebate() {
isRunning = false;
clearTimeout(timer);
document.getElementById('btn-play').disabled = false;
document.getElementById('btn-pause').disabled = true;
document.getElementById('btn-pause').className = "px-3 py-1.5 bg-gray-200 text-gray-400 font-bold text-xs rounded cursor-not-allowed";
document.getElementById('typing-indicator').classList.add('hidden');
}
function stepDebate() { pauseDebate(); executeTurn(); }
function runLoop() {
if (!isRunning) return;
executeTurn(() => { if (isRunning) timer = setTimeout(runLoop, 4000); });
}
async function executeTurn(callback) {
const emptyState = document.getElementById('empty-state');
if (emptyState) emptyState.remove();
const p = PRESETS[currentPreset];
const isA = currentAgent === 'A';
const activeName = isA ? p.nameA : p.nameB;
const activePrompt = isA ? document.getElementById('agentA-prompt').value : document.getElementById('agentB-prompt').value;
const oppName = isA ? p.nameB : p.nameA;
const oppPrompt = isA ? document.getElementById('agentB-prompt').value : document.getElementById('agentA-prompt').value;
document.getElementById('typing-agent-name').innerText = activeName;
document.getElementById('typing-indicator').classList.remove('hidden');
const payload = {
topic: document.getElementById('topic-input').value,
active_agent: { name: activeName, system_prompt: activePrompt },
opponent_agent: { name: oppName, system_prompt: oppPrompt },
chat_history: chatHistory,
human_injection: pendingEvent
};
if (pendingEvent) {
appendEvent(pendingEvent);
pendingEvent = null;
}
try {
const res = await fetch('/api/turn', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
document.getElementById('typing-indicator').classList.add('hidden');
appendMessage(isA ? 'A' : 'B', activeName, isA ? p.iconA : p.iconB, data.response);
chatHistory.push({ role: "assistant", content: `${activeName}: ${data.response}` });
turn++;
document.getElementById('turn-counter').innerText = turn;
currentAgent = isA ? 'B' : 'A';
} catch (err) {
document.getElementById('typing-indicator').classList.add('hidden');
appendMessage(isA ? 'A' : 'B', activeName, isA ? p.iconA : p.iconB, "Error de conexión con el backend.");
}
if (callback) callback();
}
function appendMessage(agentKey, name, icon, text) {
const feed = document.getElementById('chat-feed');
const div = document.createElement('div');
const isA = agentKey === 'A';
div.className = `p-3.5 rounded-lg border-l-4 ${isA ? 'border-emerald-500 bg-emerald-50/50' : 'border-teal-500 bg-teal-50/50'} shadow-sm space-y-1`;
div.innerHTML = `
<div class="flex items-center justify-between text-xs font-bold text-gray-900 mb-1">
<span>${icon} ${name}</span>
<span class="text-[10px] px-2 py-0.5 rounded font-mono ${isA ? 'bg-emerald-100 text-emerald-800' : 'bg-teal-100 text-teal-800'}">
${isA ? 'Agente A' : 'Agente B'}
</span>
</div>
<p class="text-xs text-gray-700 leading-relaxed">${text}</p>
`;
feed.appendChild(div);
feed.scrollTop = feed.scrollHeight;
}
function appendEvent(text) {
const feed = document.getElementById('chat-feed');
const div = document.createElement('div');
div.className = "p-3 rounded-lg bg-amber-100 border border-amber-300 text-amber-900 text-xs shadow-sm my-2";
div.innerHTML = `<strong>⚡ MODERADOR HUMANO (HITL):</strong> <em>${text}</em>`;
feed.appendChild(div);
feed.scrollTop = feed.scrollHeight;
}
function injectEvent() {
const val = document.getElementById('event-input').value.trim();
if (!val) return;
pendingEvent = val;
document.getElementById('event-input').value = '';
if (!isRunning) stepDebate();
}
</script>
</body>
</html>
"""
# ==============================================================================
# ENDPOINTS API (Backend en Puerto 8062)
# ==============================================================================
@app.get("/", response_class=HTMLResponse)
def serve_web_interface():
"""Sirve la interfaz gráfica interactiva del Proyecto Cavendish."""
return HTMLResponse(content=HTML_CONTENT)
@app.get("/health")
def health_check():
return {"status": "ok", "port": 8062, "file": "app_cavendish.py"}
@app.post("/api/turn")
def generate_agent_turn(request: TurnRequest):
"""Genera el siguiente turno de debate utilizando el LLM conectado."""
messages = [{"role": "system", "content": request.active_agent.system_prompt}]
context_prompt = f"Tema de debate: '{request.topic}'. Estás debatiendo contra '{request.opponent_agent.name}'."
messages.append({"role": "system", "content": context_prompt})
for msg in request.chat_history:
messages.append({"role": msg.role, "content": msg.content})
if request.human_injection:
event_prompt = f"¡EVENTO DEL MODERADOR HUMANO!: '{request.human_injection}'. Reacciona de inmediato manteniendo tu rol."
messages.append({"role": "user", "content": event_prompt})
else:
messages.append({"role": "user", "content": "Es tu turno de responder al argumento anterior. Sé claro, directo y mantén tu postura."})
response_text = query_llm_provider(messages)
return {
"agent": request.active_agent.name,
"response": response_text,
"port_used": 8062
}
@app.post("/v1/chat/completions")
def openai_compat_endpoint(body: dict):
"""Endpoint compatible con la API de OpenAI."""
messages = body.get("messages", [])
response_text = query_llm_provider(messages)
return {
"id": "cavendish-8062",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": response_text},
"finish_reason": "stop"
}
]
}
if __name__ == "__main__":
print("------------------------------------------------------------------")
print(" PROYECTO CAVENDISH — Arena Multi-Agente (app_cavendish.py)")
print(" Servidor Web + API ejecutándose en: http://localhost:8062")
print(" Documentación Swagger en: http://localhost:8062/docs")
print("------------------------------------------------------------------")
uvicorn.run("app_cavendish:app", host="0.0.0.0", port=8062, reload=True)
4. Instalación y Puesta en Marcha
Sigue estos pasos simples para instalar las librerías necesarias y arrancar el proyecto en tu máquina local.
Paso 1: Requisitos e Instalación
Asegúrate de contar con Python 3.9 o superior e instala las dependencias mediante pip:
Bash
pip install fastapi uvicorn pydantic
Paso 2: Guardar el Archivo
Guarda el código anterior en un archivo llamado app_cavendish.py.
Paso 3: Arrancar el Servidor
Ejecuta el script directamente desde la consola:
Bash
python app_cavendish.py
O arráncalo utilizando el comando oficial de uvicorn:
Bash
uvicorn app_cavendish:app --host 0.0.0.0 --port 8062 --reload
Paso 4: Abrir la Aplicación
Una vez iniciado el servidor, abre tu navegador web preferido e ingresa a:
👉 http://localhost:8062
(Opcional: Si deseas consultar la documentación técnica interactiva OpenAPI/Swagger de los endpoints, puedes acceder a http://localhost:8062/docs).



