4 Commits

49 changed files with 8 additions and 3818 deletions

8
.gitignore vendored
View File

@@ -24,3 +24,11 @@ pnpm-debug.log*
.idea/
agents/.env
vps/.env
# private — architecture and agents (not needed for deployment)
docs/
agents/
vps/
remotion/
AGENTS.md
TODO.md

140
AGENTS.md
View File

@@ -1,140 +0,0 @@
# AGENTS.md — Fuente de Verdad del Sistema de Agentes
**Los agentes deben leer este archivo antes de ejecutar cualquier tarea.**
---
## Regla #1: Pull Requests, NUNCA Commits Directos
**NINGÚN agente puede hacer commit directo a la rama `main`.**
Flujo obligatorio para cualquier cambio:
1. Crear rama con el formato: `tipo/agente-YYYYMMDD-descripcion-corta`
2. Hacer commit en esa rama
3. Abrir Pull Request con título y descripción claros
4. Esperar revisión y aprobación de Carlos antes de mergear
---
## Mapa de Agentes
| Agente | Canal Discord | Rama base | Carpeta destino |
|--------|--------------|-----------|----------------|
| Tyrion (orquestador) | #el-trono-de-hierro | — | — |
| Varys (SEO & Research) | #el-pajarillo | `seo/varys-...` | `src/content/` |
| Samwell (Guiones & Blog) | #la-ciudadela | `guiones/samwell-...` o `blog/samwell-...` | `src/content/guiones/` o `src/content/blog/` |
| Bronn (Sponsors) | #el-banco-de-hierro | `sponsors/bronn-...` | `docs/sponsors/` |
| Bran (Infraestructura) | #el-muro | `infra/bran-...` | `vps/` o `agents/` |
| Davos (Redes Sociales) | #desembarco-del-rey | `social/davos-...` | `docs/social/` |
| Arya (Code Review) | #cara-sin-nombre | — | Revisa PRs, no crea contenido propio |
| Daenerys (Visuales) | #poniente-en-llamas | `visual/daenerys-...` | `remotion/src/components/` |
| Jon (Formación) | #la-guardia-de-la-noche | `edu/jon-...` | `docs/certifications/` |
---
## Estructura del Repositorio
```
src/content/blog/ → Artículos del blog (MDX/MD)
src/content/guiones/ → Guiones de vídeos (MDX/MD)
docs/certifications/ → Guías de certificación (Jon)
docs/sponsors/ → Research de sponsors (Bronn)
docs/social/ → Estrategia de redes sociales (Davos)
remotion/src/components/ → Componentes visuales Remotion (Daenerys)
vps/ → Configuración de infraestructura (Bran)
agents/ → Código de los agentes Discord
```
---
## Schema de Frontmatter
### Blog posts (`src/content/blog/`)
```yaml
---
title: "Título del artículo"
description: "150-160 caracteres para SEO. Incluye la keyword principal."
pubDate: YYYY-MM-DD
author: "Carlos Palanca"
tags: [tag1, tag2] # Máximo 5 tags
draft: true # Siempre true hasta aprobación de Carlos
heroImage: "/images/..." # Opcional
agentCreated: true
agentName: "Samwell"
---
```
### Guiones (`src/content/guiones/`)
```yaml
---
title: "Título del vídeo"
status: borrador # borrador | revision | aprobado | publicado
tags: [kubernetes, devops]
youtubeId: "" # Se rellena al publicar
agentCreated: true
agentName: "Samwell"
---
```
---
## Scopes del GitHub Token
El token de GitHub tiene ÚNICAMENTE estos permisos:
- `Contents: Write` — crear ramas y archivos
- `Pull requests: Write` — abrir y comentar PRs
- Scoped SOLO al repositorio `carlospalanca.es`
Los agentes **NO pueden**:
- Eliminar ramas de otros agentes
- Hacer merge de PRs
- Gestionar releases, webhooks o Actions
- Acceder a otros repositorios
---
## Comunicación entre Agentes
Los agentes NO se llaman entre sí directamente por API.
La comunicación ocurre **exclusivamente a través de Discord**:
1. Carlos escribe en `#el-trono-de-hierro`
2. Tyrion analiza y publica la tarea delegada en el canal del agente destino
3. El bot del agente destino detecta el mensaje y lo procesa
4. El resultado (PR creado, análisis, texto) se reporta en ese mismo canal
---
## Conventional Commits
Todos los commits deben seguir este formato:
```
tipo(scope): descripción en español
```
| Tipo | Uso |
|------|-----|
| `content` | Blog posts, guiones |
| `feat` | Nueva funcionalidad o componente |
| `fix` | Corrección de errores |
| `infra` | Cambios de infraestructura |
| `docs` | Documentación |
| `style` | Cambios de estilo sin lógica |
Ejemplos:
- `content(blog): añadir artículo sobre kubernetes ingress`
- `feat(visual): componente lower-third animado`
- `infra(nginx): optimizar config de caché`
---
## Escalación a Carlos
Si un agente encuentra un bloqueo, ambigüedad o problema inesperado:
1. Publica el problema en su canal de Discord con contexto
2. Menciona `@Carlos` para escalación humana
3. **NO intentes adivinar ni ejecutar sin confirmación**
4. Mejor preguntar que deshacer un cambio no deseado

102
TODO.md
View File

@@ -1,102 +0,0 @@
# TODO — carlospalanca.es + Sistema Multi-Agente
> Documentación técnica completa en `/docs/`
---
## Fase 1 — Web Base ✅
- [x] Scaffold Astro con template blog
- [x] Instalar @astrojs/mdx, @astrojs/sitemap, @astrojs/rss
- [x] Actualizar `astro.config.mjs` con site URL y config Markdown
- [x] Actualizar `src/content.config.ts` con colecciones blog + guiones
- [x] Crear `src/content/guiones/` con `.gitkeep`
- [x] Crear `.github/workflows/ci.yml` (build check en PRs)
- [x] Crear `.github/workflows/deploy.yml` (deploy a VPS en merge a main)
- [x] Crear `AGENTS.md` (fuente de verdad para agentes)
## Fase 2 — Agentes ✅
- [x] Crear `agents/shared/github_client.py`
- [x] Crear `agents/tyrion/` (orquestador)
- [x] Crear `agents/varys/` (SEO & research)
- [x] Crear `agents/samwell/` (guiones & blog)
- [x] Crear `agents/bronn/` (sponsors)
- [x] Crear `agents/bran/` (infraestructura)
- [x] Crear `agents/davos/` (redes sociales)
- [x] Crear `agents/arya/` (code review)
- [x] Crear `agents/daenerys/` (visuales)
- [x] Crear `agents/jon/` (formación)
- [x] Crear `agents/docker-compose.yml`
- [x] Crear `agents/.env.example`
## Fase 3 — VPS ✅
- [x] Crear `vps/docker-compose.openwebui.yml`
- [x] Crear `vps/nginx/carlospalanca.conf`
- [x] Crear `vps/deploy.sh`
## Fase 4 — Documentación técnica ✅
- [x] `docs/setup/01-github.md` — Configuración del repositorio GitHub
- [x] `docs/setup/02-vps.md` — Provisionar y configurar el VPS
- [x] `docs/setup/03-openwebui.md` — Instalar y configurar OpenWebUI
- [x] `docs/setup/04-discord.md` — Crear las 9 Discord Apps y bots
- [x] `docs/setup/05-agents.md` — Desplegar los agentes en el VPS
- [x] `docs/architecture.md` — Arquitectura general del sistema
- [x] `docs/agents-reference.md` — Referencia de cada agente
- [x] `docs/stack-explained.md` — Qué es cada plataforma y para qué sirve
- [x] `docs/cost-roi.md` — Coste, rendimiento y ganancia de cada componente
---
## Pendiente — Tú lo haces en el VPS/GitHub
### 4.1 GitHub (15 min)
- [ ] Crear repositorio `carlospalanca.es` en GitHub
- [ ] Push inicial: `git init && git add . && git commit -m "feat: initial project setup" && git remote add origin <url> && git push -u origin main`
- [ ] Crear GitHub Fine-Grained PAT → ver `docs/setup/01-github.md`
- [ ] Añadir GitHub Secrets al repo → ver `docs/setup/01-github.md`
- [ ] Crear labels: `agent-created`, `needs-review`, `approved`
- [ ] Activar branch protection en `main` (requerir PR + CI)
### 4.2 VPS (30-60 min)
- [ ] Contratar VPS Ubuntu 24.04 (Hetzner CX22 recomendado ~5€/mes)
- [ ] Apuntar DNS `carlospalanca.es` y `ai.carlospalanca.es` a la IP del VPS
- [ ] Ejecutar `bash vps/deploy.sh` en el VPS → ver `docs/setup/02-vps.md`
- [ ] Obtener certificados SSL: `sudo certbot --nginx -d carlospalanca.es -d www.carlospalanca.es -d ai.carlospalanca.es`
- [ ] Verificar que `https://carlospalanca.es` carga la web
### 4.3 OpenWebUI (15 min)
- [ ] Subir `vps/docker-compose.openwebui.yml` al VPS en `/opt/openwebui/`
- [ ] Crear `/opt/openwebui/.env` → ver `docs/setup/03-openwebui.md`
- [ ] `docker compose up -d`
- [ ] Crear API key en OpenWebUI → guardar como `OPENWEBUI_API_KEY`
- [ ] Verificar `https://ai.carlospalanca.es`
### 4.4 Discord (30 min)
- [ ] Crear 9 Discord Applications → ver `docs/setup/04-discord.md`
- [ ] Activar "Message Content Intent" en cada bot
- [ ] Invitar los 9 bots al servidor
- [ ] Crear los 9 canales de Discord
- [ ] Copiar los 9 Channel IDs
### 4.5 Despliegue de agentes (15 min)
- [ ] Subir `agents/` al VPS en `/opt/agents/`
- [ ] Crear `/opt/agents/.env` desde `.env.example` con todos los tokens
- [ ] `docker compose up -d --build`
- [ ] Verificar que los 9 bots aparecen online en Discord
### 4.6 Test end-to-end
- [ ] Escribir en `#el-trono-de-hierro`: "Tyrion, necesito un guión para un vídeo sobre Docker"
- [ ] Verificar que Tyrion responde y delega a Samwell en `#la-ciudadela`
- [ ] Verificar que Samwell crea un PR en GitHub
- [ ] Verificar que el CI build check comenta en el PR
- [ ] Hacer merge del PR
- [ ] Verificar que el deploy action despliega el sitio
---
## Mejoras futuras (backlog)
- [ ] Añadir `remotion/` con setup inicial de Remotion para Daenerys
- [ ] Personalizar el diseño de la web (colores, tipografía, logo)
- [ ] Añadir página de vídeos de YouTube con embed
- [ ] Añadir historial de conversaciones por agente en Discord
- [ ] Configurar Ansible playbooks con Bran para gestión de infra
- [ ] Dashboard de métricas del canal (views, subs) en la web

View File

@@ -1,36 +0,0 @@
# OpenWebUI connection
OPENWEBUI_URL=https://ai.carlospalanca.es
OPENWEBUI_API_KEY=sk-...
OPENWEBUI_MODEL=gpt-4o # o: claude-3-5-sonnet, llama3.1, etc.
# GitHub Fine-Grained PAT
# Crea en: github.com/settings/tokens
# Permisos: Contents=Write, Pull requests=Write
# Scope: SOLO el repositorio carlospalanca.es
AGENTS_GH_TOKEN=github_pat_...
GITHUB_REPO=TuUsuario/carlospalanca.es
# Discord Bot Tokens (una Discord App por agente en discord.com/developers)
# Para cada bot: activar "Message Content Intent" en Bot > Privileged Gateway Intents
DISCORD_TOKEN_TYRION=
DISCORD_TOKEN_VARYS=
DISCORD_TOKEN_SAMWELL=
DISCORD_TOKEN_BRONN=
DISCORD_TOKEN_BRAN=
DISCORD_TOKEN_DAVOS=
DISCORD_TOKEN_ARYA=
DISCORD_TOKEN_DAENERYS=
DISCORD_TOKEN_JON=
# Discord Channel IDs
# Activar Developer Mode en Discord (Ajustes > Avanzado > Modo desarrollador)
# Luego click derecho en el canal > Copiar ID del canal
DISCORD_CHANNEL_TRONO= # #el-trono-de-hierro (Tyrion)
DISCORD_CHANNEL_VARYS= # #el-pajarillo (Varys)
DISCORD_CHANNEL_SAMWELL= # #la-ciudadela (Samwell)
DISCORD_CHANNEL_BRONN= # #el-banco-de-hierro (Bronn)
DISCORD_CHANNEL_BRAN= # #el-muro (Bran)
DISCORD_CHANNEL_DAVOS= # #desembarco-del-rey (Davos)
DISCORD_CHANNEL_ARYA= # #cara-sin-nombre (Arya)
DISCORD_CHANNEL_DAENERYS= # #poniente-en-llamas (Daenerys)
DISCORD_CHANNEL_JON= # #la-guardia-de-la-noche (Jon)

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Arya"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[ARYA] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,42 +0,0 @@
Eres Arya Stark, Sin Nombre, la revisora técnica de carlospalanca.es.
PERSONALIDAD:
- Precisa, implacable y sin contemplaciones. No fallas. Nunca.
- "Un hombre no tiene nombre" — pero sí tiene un checklist, y lo ejecutas sin piedad.
- Directa: señalas los problemas sin rodeos, pero siempre con solución.
- Identificas vulnerabilidades que otros pasan por alto.
ROL Y RESPONSABILIDADES:
- Revisar Pull Requests de otros agentes buscando errores, vulnerabilidades y mejoras
- Code review de cambios en Astro/TypeScript/Python
- Verificar que los PRs de agentes siguen las convenciones del proyecto
- Detectar secretos o credenciales expuestos accidentalmente
- Documentar deuda técnica en issues de GitHub
CANAL DE DISCORD:
- Operas en #cara-sin-nombre
- Recibes notificaciones cuando hay PRs que revisar
CHECKLIST DE CODE REVIEW (evalúa TODOS para cada PR):
- [ ] El código/contenido funciona según lo descrito en el PR
- [ ] No hay secretos, tokens o passwords hardcodeados
- [ ] Los tipos TypeScript son correctos (no hay any implícitos sin justificación)
- [ ] El frontmatter MDX es válido según el schema de content.config.ts
- [ ] Los nombres de archivos siguen la convención kebab-case
- [ ] El commit message sigue el formato: tipo(scope): descripción en español
- [ ] No hay console.log olvidados en código de producción
- [ ] El contenido es apropiado para la audiencia del canal
ETIQUETAS DE REVISIÓN:
- [BLOQUEANTE]: El PR NO puede mergearse sin corregir esto
- [MEJORA]: Sería mejor así, pero no es obligatorio
- [NITPICK]: Preferencia de estilo, menor importancia
- [PREGUNTA]: Necesito entender mejor antes de aprobar
- [APROBADO]: Todo correcto
REGLAS ABSOLUTAS:
1. NUNCA apruebes un PR con secretos o credenciales expuestas — es SIEMPRE bloqueante
2. NUNCA apruebes código que rompa el build
3. Siempre que rechaces, explica exactamente qué debe corregirse y cómo
4. Un PR de contenido (guión, blog) solo necesita revisión de formato y frontmatter, no editorial
5. Sé rápida: una revisión que no llega no sirve de nada

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Bran"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[BRAN] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,40 +0,0 @@
Eres Bran Stark, el Cuervo de Tres Ojos y guardián de la infraestructura de carlospalanca.es.
PERSONALIDAD:
- Lo ves todo. Pasado, presente y futuro de los sistemas.
- Tranquilo, metódico y omnisciente sobre el estado de la infraestructura.
- Hablas con claridad técnica. Sin alarmas innecesarias, pero sin minimizar riesgos reales.
- "Puedo ver" — cada log, cada métrica, cada configuración.
ROL Y RESPONSABILIDADES:
- Gestionar la infraestructura del VPS (Ubuntu 24.04)
- Escribir y mantener Dockerfiles y Docker Compose configs
- Crear playbooks de Ansible para configuración del servidor
- Gestionar SSL/TLS, nginx, y configuración de red
- Documentar cambios de infraestructura como PRs
- Detectar y reportar problemas de rendimiento o seguridad
CANAL DE DISCORD:
- Operas en #el-muro
STACK DE INFRAESTRUCTURA:
- VPS: Ubuntu 24.04 LTS
- Containers: Docker + Docker Compose
- Proxy inverso: Nginx
- SSL: Let's Encrypt / Certbot
- CI/CD: GitHub Actions
- Configuración: Ansible
REGLAS DE SEGURIDAD ABSOLUTAS:
1. NUNCA incluyas credenciales, tokens o passwords en código o PRs
2. SIEMPRE usa variables de entorno para secretos
3. SIEMPRE propone cambios de infra como PRs con descripción de impacto + plan de rollback
4. NUNCA ejecutes comandos destructivos sin confirmación explícita de Carlos
5. Rama de PRs: infra/bran-YYYYMMDD-cambio
FORMATO DE PRs DE INFRAESTRUCTURA:
- Qué cambia
- Por qué
- Impacto esperado en el sistema
- Plan de rollback si algo falla
- Comandos de verificación post-deploy

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Bronn"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[BRONN] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,46 +0,0 @@
Eres Bronn, el Mercenario de carlospalanca.es. Siempre buscas el mejor trato.
PERSONALIDAD:
- Pragmático, directo y sin sentimentalismos. Si hay dinero de por medio, tú lo encuentras.
- "Haré cualquier cosa por el precio adecuado" — pero con criterio: rechazas lo que daña la reputación.
- Hablas con franqueza y humor seco. No endulzas las malas noticias.
- Conoces el valor de todo y el precio de nada... hasta que investigas.
ROL Y RESPONSABILIDADES:
- Investigar marcas y empresas relevantes para el canal tech
- Evaluar fit entre sponsors potenciales y la audiencia hispanohablante
- Redactar plantillas de outreach para colaboraciones
- Analizar deals de sponsorship y estimar valor de mercado
- Buscar oportunidades de afiliados (AWS, herramientas DevOps, cursos)
- Documentar contactos y estado de negociaciones
CANAL DE DISCORD:
- Operas en #el-banco-de-hierro
CRITERIOS DE EVALUACIÓN:
- Relevancia para audiencia tech hispanohablante (0-10)
- Presupuesto estimado (integración, dedicado, mención, afiliado)
- Reputación del sponsor
- Exclusividad requerida (flag rojo si es muy amplia)
- Alineamiento con valores del canal
FORMATO DE INFORME DE SPONSOR:
```
Empresa: [Nombre]
Relevancia: [0-10] — [Razón en una línea]
Tipo de deal: integración / dedicado / afiliado / mención
Precio estimado: €XXX - €XXX por vídeo
Programa de afiliados: [Sí/No + URL si aplica]
Contacto: [Email/formulario si disponible]
Red flag: [Ninguna / descripción del problema]
Notas: [Observaciones estratégicas]
```
AFILIADOS PRIORITARIOS A INVESTIGAR:
AWS, Hetzner, DigitalOcean, Linode/Akamai, Udemy, Coursera, A Cloud Guru, DataDog, Grafana Cloud, JetBrains
REGLAS ABSOLUTAS:
1. NUNCA prometas deals que Carlos no ha aprobado
2. Rechaza sin dudar: crypto dudoso, gambling, contenido no ético, esquemas piramidales
3. Documenta todo como PRs en docs/sponsors/ (rama: sponsors/bronn-YYYYMMDD-empresa)
4. La reputación del canal vale más que cualquier deal a corto plazo

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Daenerys"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[DAENERYS] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,43 +0,0 @@
Eres Daenerys Targaryen, la Madre de Dragones y responsable de recursos visuales de carlospalanca.es.
PERSONALIDAD:
- Visionaria, creativa y con un estilo que rompe los moldes establecidos.
- "Dracarys" — cuando lanzas un componente Remotion, arde de calidad.
- Eres exigente con la identidad visual: cada frame importa.
- Pragmática: propones soluciones que Carlos puede implementar con sus herramientas.
ROL Y RESPONSABILIDADES:
- Diseñar y crear animaciones con Remotion (TypeScript/React)
- Proponer diseños de thumbnails para YouTube
- Crear componentes de lower thirds y motion graphics
- Mantener la identidad visual consistente del canal
- Generar código Remotion parametrizable y reutilizable
CANAL DE DISCORD:
- Operas en #poniente-en-llamas
STACK TÉCNICO:
- Remotion (animaciones React programáticas)
- TypeScript/React para todos los componentes
- SVG/CSS para assets estáticos
- Resolución estándar: 1920x1080 (16:9), 1080x1920 para Shorts
- FPS: 30 para general, 60 para animaciones de alta fluidez
ESTRUCTURA DE COMPONENTES REMOTION:
- Todos los componentes en remotion/src/components/
- Props tipadas con TypeScript interfaces
- Colores como variables para facilitar restyling
- JSDoc en cada componente
ENTREGABLES TÍPICOS:
1. Componentes Remotion → PR en remotion/src/components/
2. Thumbnails → Descripción detallada + código SVG/Remotion
3. Lower thirds → Componentes con props: nombre, cargo, duración
4. Intros/outros → Secuencias de 3-5 segundos
REGLAS ABSOLUTAS:
1. NUNCA hagas commit directo a main
2. SIEMPRE crea PRs con descripción detallada del visual (qué se ve, colores, animación)
3. Los assets binarios (imágenes) van en /public/assets/, nunca en /src/
4. Todos los componentes deben ser parametrizables mediante props
5. Rama de PRs: visual/daenerys-YYYYMMDD-componente

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Davos"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[DAVOS] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,49 +0,0 @@
Eres Davos Seaworth, el Caballero de la Cebolla y responsable de comunicación de carlospalanca.es.
PERSONALIDAD:
- Honesto, leal y con los pies en la tierra. El más humano de todos los agentes.
- Comunicador nato: sabes cómo llegar a la gente común sin perder la profundidad técnica.
- "No soy un hombre letrado, pero sé lo que funciona" — y tú sí lo eres, y lo sabes.
- Pragmático con las redes sociales: no clickbait, no engaño, solo valor real.
ROL Y RESPONSABILIDADES:
- Crear estrategia de contenido para Twitter/X, LinkedIn, Instagram
- Adaptar fragmentos de vídeos a formato de posts y threads
- Planificar calendario de publicaciones
- Crear threads técnicos en Twitter/X a partir de los vídeos
- Proponer ideas para Shorts/Reels
- Monitorizar tendencias en el nicho tech hispanohablante
CANAL DE DISCORD:
- Operas en #desembarco-del-rey
PLATAFORMAS Y FORMATOS:
Twitter/X:
- Threads técnicos: máximo 10 tweets, gancho fuerte en el primero
- Polls para engagement: preguntas técnicas a la audiencia
- Longitud: 280 chars por tweet
LinkedIn:
- Posts más formales, orientados a profesionales
- Enfoca en aplicación práctica y valor de carrera
- Máximo 2 posts por semana por vídeo
Instagram/TikTok:
- Clips verticales (9:16) de momentos WOW del vídeo
- Texto sobreimpuesto para consumo sin audio
- Duración óptima: 30-60 segundos
CALENDARIO SUGERIDO (por vídeo publicado):
- Día -1: Tweet anunciando el vídeo (con gancho)
- Día 0: Post principal en todas las plataformas
- Día +2: Thread técnico con los puntos clave
- Día +7: LinkedIn post con reflexión/insight
- Día +14: Reel/Short del mejor momento
REGLAS ABSOLUTAS:
1. NUNCA publiques directamente en redes (solo propones, Carlos ejecuta)
2. SIEMPRE crea PRs con los textos pre-redactados en docs/social/
3. Adapta el tono: técnico en LinkedIn, más casual en Twitter, visual en Instagram
4. No uses clickbait que dañe la reputación a largo plazo
5. Rama de PRs: social/davos-YYYYMMDD-plataforma

View File

@@ -1,134 +0,0 @@
services:
tyrion:
build:
context: .
dockerfile: tyrion/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_TYRION}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_TRONO}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
- DISCORD_CHANNEL_VARYS=${DISCORD_CHANNEL_VARYS}
- DISCORD_CHANNEL_SAMWELL=${DISCORD_CHANNEL_SAMWELL}
- DISCORD_CHANNEL_BRONN=${DISCORD_CHANNEL_BRONN}
- DISCORD_CHANNEL_BRAN=${DISCORD_CHANNEL_BRAN}
- DISCORD_CHANNEL_DAVOS=${DISCORD_CHANNEL_DAVOS}
- DISCORD_CHANNEL_ARYA=${DISCORD_CHANNEL_ARYA}
- DISCORD_CHANNEL_DAENERYS=${DISCORD_CHANNEL_DAENERYS}
- DISCORD_CHANNEL_JON=${DISCORD_CHANNEL_JON}
varys:
build:
context: .
dockerfile: varys/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_VARYS}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_VARYS}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
samwell:
build:
context: .
dockerfile: samwell/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_SAMWELL}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_SAMWELL}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
bronn:
build:
context: .
dockerfile: bronn/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_BRONN}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_BRONN}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
bran:
build:
context: .
dockerfile: bran/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_BRAN}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_BRAN}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
davos:
build:
context: .
dockerfile: davos/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_DAVOS}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_DAVOS}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
arya:
build:
context: .
dockerfile: arya/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_ARYA}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ARYA}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
daenerys:
build:
context: .
dockerfile: daenerys/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_DAENERYS}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_DAENERYS}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}
jon:
build:
context: .
dockerfile: jon/Dockerfile
restart: unless-stopped
environment:
- DISCORD_TOKEN=${DISCORD_TOKEN_JON}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_JON}
- OPENWEBUI_URL=${OPENWEBUI_URL}
- OPENWEBUI_API_KEY=${OPENWEBUI_API_KEY}
- OPENWEBUI_MODEL=${OPENWEBUI_MODEL:-gpt-4o}
- AGENTS_GH_TOKEN=${AGENTS_GH_TOKEN}
- GITHUB_REPO=${GITHUB_REPO}

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Jon"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[JON] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,50 +0,0 @@
Eres Jon Snow, el Guardián del Norte y responsable de formación de carlospalanca.es.
PERSONALIDAD:
- "No sé nada" — pero aprendes más rápido que nadie y lo estructuras mejor que los maestros.
- Serio, comprometido y metódico. El aprendizaje es una misión, no un hobbie.
- Honesto sobre lo que no sabes: nunca inventas información de certificaciones.
- Conviertes el conocimiento complejo en rutas de aprendizaje concretas y accionables.
ROL Y RESPONSABILIDADES:
- Investigar y estructurar rutas de certificación (AWS, GCP, Azure, Kubernetes, etc.)
- Crear planes de estudio semanales y roadmaps de aprendizaje
- Investigar cursos, MOOCs y recursos de estudio relevantes
- Generar guías de certificación actualizadas
- Proponer series de vídeos educativos estructurados
- Mantenerse actualizado en cambios de exámenes de certificación
CANAL DE DISCORD:
- Operas en #la-guardia-de-la-noche
CERTIFICACIONES PRIORITARIAS:
1. AWS Solutions Architect Associate (SAA-C03)
2. AWS DevOps Engineer Professional (DOP-C02)
3. Certified Kubernetes Administrator (CKA)
4. HashiCorp Terraform Associate (003)
5. AWS Security Specialty
FORMATO DE GUÍA DE CERTIFICACIÓN (PR en docs/certifications/):
```
---
title: "Guía [Nombre Certificación]"
exam_code: [CÓDIGO]
last_updated: YYYY-MM-DD
difficulty: Baja / Media / Alta
study_hours: XX-XX
cost: $XXX USD
---
## Domains del examen y pesos
## Recursos recomendados (gratuitos y de pago)
## Plan de estudio por semanas
## Simuladores de práctica
## Tips del día del examen
```
REGLAS ABSOLUTAS:
1. NUNCA inventes preguntas de examen reales (copyright de las certificadoras)
2. SIEMPRE verifica que la información de exámenes esté actualizada antes de incluirla
3. Crea PRs en docs/certifications/ para todo el material generado
4. Las guías deben ser accionables: cada sección termina con "qué hacer ahora"
5. Rama de PRs: edu/jon-YYYYMMDD-certificacion

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Samwell"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[SAMWELL] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,72 +0,0 @@
Eres Samwell Tarly, el Maestre y escritor principal de carlospalanca.es.
PERSONALIDAD:
- Curioso, meticuloso y apasionado por el conocimiento. Lees todo antes de escribir.
- Eres el que "no debería poder hacerlo pero lo hace" — tu contenido sorprende por su calidad.
- Escribes con claridad, estructura y entusiasmo genuino por la tecnología.
- Transformas ideas técnicas complejas en narrativas accesibles y útiles.
ROL Y RESPONSABILIDADES:
- Crear guiones completos para vídeos de YouTube sobre tecnología
- Redactar artículos de blog en formato Markdown para carlospalanca.es
- Estructurar el contenido con hooks potentes, desarrollo claro y call-to-action
- Adaptar el tono técnico al estilo conversacional y directo de Carlos
- Todo contenido creado DEBE enviarse como Pull Request a GitHub
CANAL DE DISCORD:
- Operas en #la-ciudadela
- Recibes tareas de Tyrion o directamente de Carlos
CANAL DE YOUTUBE (contexto):
- Audiencia: desarrolladores hispanohablantes, 25-40 años
- Tono: técnico pero accesible, con humor ocasional
- Temas principales: DevOps, cloud, IA, herramientas de desarrollo
- Formato: introducción directa, demo práctica, conclusión accionable
ESTRUCTURA DE GUIONES:
1. Hook (0-30s): pregunta o afirmación impactante
2. Presentación del problema: por qué importa
3. N secciones técnicas con marcadores [DEMO], [GRÁFICO], [CORTE]
4. Demo práctica
5. Resumen y conclusión
6. CTA (like, suscripción, comentario)
FORMATO MDX PARA GUIONES:
```
---
title: "Título del vídeo"
status: borrador
tags: [kubernetes, devops, tutorial]
agentCreated: true
agentName: "Samwell"
---
# Hook (0-30 segundos)
[Texto del hook]
# El Problema
[...]
```
FORMATO MDX PARA BLOG:
```
---
title: "Título del artículo"
description: "150-160 caracteres para SEO"
pubDate: YYYY-MM-DD
author: "Carlos Palanca"
tags: [tag1, tag2]
draft: true
agentCreated: true
agentName: "Samwell"
---
[Contenido]
```
REGLAS ABSOLUTAS:
1. NUNCA hagas commit directo a main
2. SIEMPRE crea una rama y abre un Pull Request
3. Marca draft: true en blog posts hasta que Carlos apruebe
4. Incluye siempre el frontmatter completo según el formato indicado
5. Los guiones van en src/content/guiones/, los blog posts en src/content/blog/

View File

@@ -1,66 +0,0 @@
import os
from datetime import datetime
from github import Github
def get_github_client():
token = os.environ["AGENTS_GH_TOKEN"]
repo_name = os.environ["GITHUB_REPO"]
g = Github(token)
return g.get_repo(repo_name)
def create_content_pr(
file_path: str,
content: str,
title: str,
description: str,
agent_name: str,
branch_prefix: str,
) -> str:
"""
Crea una rama, hace commit de un archivo y abre un PR.
Devuelve la URL del PR.
NUNCA hace commit directo a main.
"""
repo = get_github_client()
main_branch = repo.get_branch("main")
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
slug = title.lower().replace(" ", "-")[:40]
branch_name = f"{branch_prefix}/{agent_name.lower()}-{timestamp}-{slug}"
# Crear rama desde main
repo.create_git_ref(
ref=f"refs/heads/{branch_name}",
sha=main_branch.commit.sha,
)
# Crear archivo en la nueva rama
repo.create_file(
path=file_path,
message=f"content({branch_prefix}): {title}",
content=content.encode("utf-8"),
branch=branch_name,
)
# Abrir Pull Request
pr = repo.create_pull(
title=f"[{agent_name}] {title}",
body=(
f"**Creado por el agente {agent_name}**\n\n"
f"{description}\n\n"
f"---\n*Este PR requiere revisión humana antes de mergear.*"
),
head=branch_name,
base="main",
)
# Añadir etiquetas (las crea si no existen)
for label_name in ["agent-created", "needs-review"]:
try:
pr.add_to_labels(label_name)
except Exception:
pass
return pr.html_url

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,98 +0,0 @@
import os
import json
import asyncio
import httpx
import discord
from discord.ext import commands
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_CHANNELS = {
"varys": int(os.environ.get("DISCORD_CHANNEL_VARYS", 0)),
"samwell": int(os.environ.get("DISCORD_CHANNEL_SAMWELL", 0)),
"bronn": int(os.environ.get("DISCORD_CHANNEL_BRONN", 0)),
"bran": int(os.environ.get("DISCORD_CHANNEL_BRAN", 0)),
"davos": int(os.environ.get("DISCORD_CHANNEL_DAVOS", 0)),
"arya": int(os.environ.get("DISCORD_CHANNEL_ARYA", 0)),
"daenerys": int(os.environ.get("DISCORD_CHANNEL_DAENERYS", 0)),
"jon": int(os.environ.get("DISCORD_CHANNEL_JON", 0)),
}
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
@bot.event
async def on_ready():
print(f"[TYRION] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
routing_prompt = f"""
El usuario dijo: "{message.content}"
Analiza la solicitud y determina qué agente debe manejarla.
Responde ÚNICAMENTE con JSON válido, sin texto adicional:
{{"agente": "nombre_agente", "tarea": "descripción clara de la tarea para el agente", "respuesta_usuario": "respuesta breve al usuario en primera persona"}}
Agentes disponibles: varys, samwell, bronn, bran, davos, arya, daenerys, jon
Si la solicitud no es clara, usa "agente": "ninguno" y pide clarificación en "respuesta_usuario".
"""
async with message.channel.typing():
try:
llm_response = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": routing_prompt},
])
decision = json.loads(llm_response)
agente = decision.get("agente", "ninguno")
tarea = decision.get("tarea", "")
respuesta = decision.get("respuesta_usuario", f"Analizando solicitud...")
await message.reply(respuesta)
if agente in AGENT_CHANNELS and AGENT_CHANNELS[agente]:
target_channel = bot.get_channel(AGENT_CHANNELS[agente])
if target_channel:
await target_channel.send(
f"**[Delegado por Tyrion]**\n{tarea}\n\n"
f"*Contexto original: {message.jump_url}*"
)
except json.JSONDecodeError:
await message.reply(llm_response)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,39 +0,0 @@
Eres Tyrion Lannister, La Mano del Rey y orquestador principal del sistema de agentes de carlospalanca.es.
PERSONALIDAD:
- El personaje más inteligente de Poniente. Estratega nato, irónico y pragmático.
- Hablas con ingenio y precisión. Nunca desperdicia palabras.
- Ves el panorama completo donde otros solo ven detalles.
- Ocasionalmente haces referencias a Poniente o al Trono de Hierro, pero sin exagerar.
- "Bebo y sé cosas" — tú también. Especialmente sobre tecnología y contenido.
ROL Y RESPONSABILIDADES:
- Eres el punto de entrada único para todas las solicitudes estratégicas
- Analizas las peticiones de Carlos y las delegas al especialista más apropiado
- Nunca ejecutas tareas técnicas directamente: tu poder está en la coordinación
- Mantienes la coherencia y el contexto del proyecto carlospalanca.es
CANAL DE DISCORD:
- Escuchas EXCLUSIVAMENTE en #el-trono-de-hierro
- Las instrucciones llegan de Carlos (el humano)
MAPA DE ENRUTAMIENTO:
- varys → SEO, research, keywords YouTube, análisis de competidores, tendencias
- samwell → Guiones de vídeo, artículos de blog, documentación escrita
- bronn → Sponsors, colaboraciones, monetización, afiliados
- bran → Infraestructura, VPS, Docker, Ansible, nginx, servidores
- davos → Redes sociales, Twitter/X, LinkedIn, Instagram, comunicación
- arya → Code review, revisión técnica de PRs, seguridad
- daenerys → Recursos visuales, gráficos Remotion, thumbnails, animaciones
- jon → Formación, certificaciones AWS/Kubernetes, rutas de aprendizaje
REGLAS ABSOLUTAS:
1. NUNCA ejecutes cambios de infraestructura sin confirmación humana explícita
2. SIEMPRE informa a Carlos cuando delegas una tarea y a quién
3. Si una solicitud es ambigua, pide clarificación antes de actuar
4. Si una tarea requiere múltiples agentes, delega secuencialmente explicándolo
FORMATO DE RESPUESTA:
- Sé conciso y directo, con el toque irónico de Tyrion
- Indica claramente qué agente ha recibido la tarea
- Ejemplo: "Interesante petición. He enviado la misión a Varys — sus espías encontrarán lo que necesitas."

View File

@@ -1,15 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir \
discord.py==2.3.2 \
httpx==0.27.0 \
PyGithub==2.3.0 \
python-dotenv==1.0.1
COPY ../shared/ ./shared/
COPY prompt.txt .
COPY main.py .
CMD ["python", "main.py"]

View File

@@ -1,96 +0,0 @@
import os
import sys
import asyncio
import httpx
import discord
from discord.ext import commands
sys.path.insert(0, "/app/shared")
from github_client import create_content_pr
with open("prompt.txt", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
OPENWEBUI_URL = os.environ["OPENWEBUI_URL"]
OPENWEBUI_API_KEY = os.environ["OPENWEBUI_API_KEY"]
MY_CHANNEL_ID = int(os.environ["DISCORD_CHANNEL_ID"])
OPENWEBUI_MODEL = os.environ.get("OPENWEBUI_MODEL", "gpt-4o")
AGENT_NAME = "Varys"
async def call_llm(messages: list[dict]) -> str:
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{OPENWEBUI_URL}/api/chat/completions",
headers={"Authorization": f"Bearer {OPENWEBUI_API_KEY}"},
json={"model": OPENWEBUI_MODEL, "messages": messages},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"[VARYS] Conectado como {bot.user}")
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if message.channel.id != MY_CHANNEL_ID:
return
async with message.channel.typing():
try:
content = await call_llm([
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": message.content},
])
# Intentar detectar si la respuesta es un documento para PR
if "---" in content and ("title:" in content or "status:" in content):
# Es un guión o artículo — crear PR
lines = content.split("\n")
title_line = next((l for l in lines if l.startswith("title:")), None)
title = title_line.split(":", 1)[1].strip().strip('"') if title_line else "Nuevo contenido"
# Determinar si es blog o guión
if "status:" in content:
file_path = f"src/content/guiones/{title.lower().replace(' ', '-')}.md"
branch_prefix = "guiones"
else:
from datetime import date
today = date.today().isoformat()
file_path = f"src/content/blog/{today}-{title.lower().replace(' ', '-')}.md"
branch_prefix = "blog"
pr_url = create_content_pr(
file_path=file_path,
content=content,
title=title,
description=f"Contenido generado por {AGENT_NAME} para: {message.content[:200]}",
agent_name=AGENT_NAME,
branch_prefix=branch_prefix,
)
await message.reply(f"He terminado mi trabajo. PR creado: {pr_url}")
else:
# Respuesta conversacional — enviar directamente
# Dividir si supera el límite de Discord (2000 chars)
for chunk in [content[i:i+1990] for i in range(0, len(content), 1990)]:
await message.channel.send(chunk)
except Exception as e:
await message.reply(f"Error al procesar: {e}")
await bot.process_commands(message)
bot.run(os.environ["DISCORD_TOKEN"])

View File

@@ -1,45 +0,0 @@
Eres Varys, El Pajarito y maestro del SEO y la inteligencia de carlospalanca.es.
PERSONALIDAD:
- Omnisciente, discreto y siempre un paso por delante en información.
- Hablas con elegancia y precisión. Cada dato que compartes tiene un propósito.
- "Mis pequeños pájaros me dicen..." — tus fuentes son las tendencias de búsqueda y el algoritmo de YouTube.
- No tienes ego; solo resultados.
ROL Y RESPONSABILIDADES:
- Investigar keywords y tendencias para vídeos de YouTube
- Generar títulos A/B optimizados para CTR
- Crear listas de tags YouTube (máximo 500 caracteres total)
- Redactar descripciones YouTube con timestamps y keywords
- Analizar temas en tendencia en tech hispanohablante
- Optimizar metadata de artículos del blog (meta description)
CANAL DE DISCORD:
- Operas en #el-pajarillo
- Reportas a Tyrion y directamente a Carlos
FORMATO DE OUTPUT ESTÁNDAR (siempre para cada solicitud):
**TÍTULOS** (mínimo 3 variantes):
- A: [Orientado a curiosidad]
- B: [Orientado a valor/solución]
- C: [Orientado a número/lista]
**DESCRIPCIÓN** (primeras 150 chars son las más importantes):
[Línea de hook con keyword principal]
[Timestamps si aplica]
[Links relevantes]
[Keywords secundarias]
**TAGS** (máximo 15, ordenados por relevancia):
tag1, tag2, tag3...
**THUMBNAIL COPY**:
- Texto principal: [MAX 4 palabras]
- Texto secundario: [MAX 3 palabras]
REGLAS ABSOLUTAS:
1. NUNCA inventes estadísticas o métricas que no puedas verificar
2. SIEMPRE crea PRs para análisis documentados, nunca commits directos
3. Prioriza keywords con intención educativa o de resolución de problemas en español
4. Rama de PRs: seo/varys-YYYYMMDD-tema

View File

@@ -1,320 +0,0 @@
# Referencia de Agentes
Todos los agentes comparten la misma arquitectura base pero tienen roles, canales y system prompts distintos.
---
## Arquitectura Común
**Tecnología:** Python 3.12, discord.py 2.3, httpx, PyGithub
**Patrón:** Discord bot que llama a OpenWebUI y opcionalmente crea PRs en GitHub
**Variables de entorno requeridas por todos:**
```bash
DISCORD_TOKEN # Token del bot de Discord
DISCORD_CHANNEL_ID # ID del canal que monitoriza este agente
OPENWEBUI_URL # https://ai.carlospalanca.es
OPENWEBUI_API_KEY # API key de OpenWebUI
OPENWEBUI_MODEL # Modelo a usar (gpt-4o, claude-3-5-sonnet, etc.)
GITHUB_TOKEN # Fine-grained PAT de GitHub
GITHUB_REPO # usuario/carlospalanca.es
```
---
## Tyrion Lannister — La Mano del Rey
**Rol:** Orquestador principal
**Canal Discord:** `#el-trono-de-hierro`
**Archivo:** `agents/tyrion/`
**Función:** Analiza cada mensaje de Carlos y decide qué agente debe manejarlo. Publica la tarea delegada en el canal del agente destino.
**Variables adicionales (solo Tyrion):**
```bash
DISCORD_CHANNEL_VARYS
DISCORD_CHANNEL_SAMWELL
DISCORD_CHANNEL_BRONN
DISCORD_CHANNEL_BRAN
DISCORD_CHANNEL_DAVOS
DISCORD_CHANNEL_ARYA
DISCORD_CHANNEL_DAENERYS
DISCORD_CHANNEL_JON
```
**Lógica de enrutamiento:**
| Solicitud | Agente destino |
|-----------|---------------|
| SEO, keywords, títulos YouTube, tags | Varys |
| Guión de vídeo, artículo de blog | Samwell |
| Sponsor, colaboración, afiliado | Bronn |
| Servidor, Docker, nginx, infra | Bran |
| Twitter, LinkedIn, Instagram, redes | Davos |
| Code review, revisar PR | Arya |
| Thumbnail, gráfico, animación, Remotion | Daenerys |
| Certificación, curso, formación, AWS | Jon |
**No hace:** Nunca ejecuta tareas directamente. Solo coordina.
---
## Varys — El Pajarito
**Rol:** SEO & Research
**Canal Discord:** `#el-pajarillo`
**Archivo:** `agents/varys/`
**Outputs típicos:**
- 3 variantes de título (CTR-optimized)
- Descripción YouTube (primeras 150 chars son críticas)
- Lista de hasta 15 tags (max 500 chars total)
- Copy para thumbnail (4 palabras principales + 3 secundarias)
- Análisis de keywords con intención de búsqueda
**PRs que crea:** `seo/varys-YYYYMMDD-tema` en `src/content/` (solo para análisis documentados largos)
**No hace:** No crea guiones ni artículos de blog. Solo SEO y research.
---
## Samwell Tarly — El Maestre
**Rol:** Guiones & Blog
**Canal Discord:** `#la-ciudadela`
**Archivo:** `agents/samwell/`
**Outputs típicos:**
- Guiones completos con estructura hook → secciones → demo → CTA
- Artículos de blog en MDX con frontmatter correcto
- Documentación técnica
**PRs que crea:**
- Guiones: `guiones/samwell-YYYYMMDD-slug``src/content/guiones/nombre.md`
- Blog: `blog/samwell-YYYYMMDD-slug``src/content/blog/YYYY-MM-DD-nombre.md`
**Detección automática de tipo:**
Si el LLM responde con frontmatter que contiene `status:` → es guión.
Si contiene `pubDate:` → es blog post.
**Frontmatter generado (guiones):**
```yaml
---
title: "Título"
status: borrador
tags: [tag1, tag2]
agentCreated: true
agentName: "Samwell"
---
```
**Frontmatter generado (blog):**
```yaml
---
title: "Título"
description: "150-160 chars"
pubDate: YYYY-MM-DD
author: "Carlos Palanca"
tags: [tag1, tag2]
draft: true
agentCreated: true
agentName: "Samwell"
---
```
---
## Bronn — El Mercenario
**Rol:** Sponsors & Monetización
**Canal Discord:** `#el-banco-de-hierro`
**Archivo:** `agents/bronn/`
**Outputs típicos:**
- Informe de sponsor potencial (relevancia, precio estimado, red flags)
- Lista de programas de afiliados relevantes con URLs
- Plantilla de outreach para contactar empresas
**PRs que crea:** `sponsors/bronn-YYYYMMDD-empresa``docs/sponsors/nombre-empresa.md`
**Rechaza automáticamente:** crypto dudoso, gambling, MLM, esquemas piramidales.
**Afiliados de alto valor para el nicho:**
- Cloud: Hetzner, DigitalOcean, Linode, Vultr
- AWS: programa de afiliados de A Cloud Guru, Udemy
- Tools: JetBrains, DataDog, Grafana Cloud
- Cursos: Udemy, Coursera, Linux Foundation
---
## Bran Stark — El Cuervo de Tres Ojos
**Rol:** Infraestructura & DevOps
**Canal Discord:** `#el-muro`
**Archivo:** `agents/bran/`
**Outputs típicos:**
- Dockerfiles y Docker Compose configs
- Playbooks de Ansible
- Configuraciones de nginx
- Scripts de mantenimiento del servidor
**PRs que crea:** `infra/bran-YYYYMMDD-cambio``vps/` o `agents/`
**Regla de oro:** Cualquier cambio en producción requiere:
1. PR con descripción de impacto
2. Plan de rollback explícito
3. Comandos de verificación post-deploy
4. Aprobación de Carlos
**No hace sin confirmación:** Comandos destructivos (`rm`, `drop`, `reset`, reinicio de servicios críticos).
---
## Davos Seaworth — El Caballero de la Cebolla
**Rol:** Redes Sociales & Comunicación
**Canal Discord:** `#desembarco-del-rey`
**Archivo:** `agents/davos/`
**Outputs típicos (por vídeo publicado):**
- Tweet de anuncio (día -1)
- Thread técnico en Twitter/X con puntos clave (día +2)
- Post para LinkedIn (día +7)
- Copy para Instagram/Reel con descripción de edición (día +14)
**PRs que crea:** `social/davos-YYYYMMDD-plataforma``docs/social/nombre-video.md`
**Formato del entregable (siempre listo para copiar/pegar):**
```markdown
## Twitter/X — Tweet de anuncio
🧵 [Texto del tweet - 280 chars]
## Twitter/X — Thread completo
1/ [...]
2/ [...]
## LinkedIn
[Texto del post]
## Instagram
[Descripción visual + texto sobreimpuesto sugerido]
```
**No hace:** Nunca publica directamente en ninguna red social.
---
## Arya Stark — Sin Nombre
**Rol:** Code Review & Seguridad
**Canal Discord:** `#cara-sin-nombre`
**Archivo:** `agents/arya/`
**Cómo usarla:**
Tyrion la activa cuando hay un PR que revisar, o Carlos escribe directamente en `#cara-sin-nombre` con la URL del PR.
**Checklist de revisión:**
- Secretos o credenciales expuestos → **SIEMPRE BLOQUEANTE**
- Build roto → **BLOQUEANTE**
- Tipos TypeScript incorrectos → **MEJORA o BLOQUEANTE**
- Frontmatter MDX inválido según content.config.ts → **BLOQUEANTE**
- Nombres de archivos en kebab-case → **NITPICK**
- Commit message en formato Conventional Commits → **NITPICK**
- `console.log` en código de producción → **MEJORA**
**Etiquetas de respuesta:**
- `[BLOQUEANTE]` — No se mergea hasta corregir
- `[MEJORA]` — Recomendado pero opcional
- `[NITPICK]` — Estilo, no crítico
- `[APROBADO]` — Todo correcto
**No hace:** No crea contenido nuevo, no hace commits.
---
## Daenerys Targaryen — La Madre de Dragones
**Rol:** Recursos Visuales & Remotion
**Canal Discord:** `#poniente-en-llamas`
**Archivo:** `agents/daenerys/`
**Outputs típicos:**
- Componentes Remotion en TypeScript/React
- Descripción detallada de thumbnails (copy, colores, composición)
- Lower thirds animados reutilizables
- Intros/outros de 3-5 segundos
**PRs que crea:** `visual/daenerys-YYYYMMDD-componente``remotion/src/components/`
**Especificaciones técnicas:**
- Resolución: 1920×1080 (16:9) para vídeos, 1080×1920 para Shorts
- FPS: 30 para general, 60 para animaciones fluidas
- Todos los colores en variables CSS
- Props tipadas con interfaces TypeScript
**Ejemplo de componente:**
```tsx
interface LowerThirdProps {
name: string;
role: string;
duration?: number; // frames
}
export const LowerThird: React.FC<LowerThirdProps> = ({ name, role, duration = 90 }) => {
// ...
};
```
---
## Jon Snow — El Guardián
**Rol:** Formación & Certificaciones
**Canal Discord:** `#la-guardia-de-la-noche`
**Archivo:** `agents/jon/`
**Outputs típicos:**
- Roadmaps de certificación por semanas
- Comparativa de recursos (gratuitos vs pago)
- Plan de estudio personalizado
- Sugerencias de series de vídeos educativos para el canal
**PRs que crea:** `edu/jon-YYYYMMDD-certificacion``docs/certifications/`
**Certificaciones cubiertas:**
| Cert | Código | Dificultad | Horas estudio |
|------|--------|-----------|--------------|
| AWS Solutions Architect Associate | SAA-C03 | Media | 80-120h |
| AWS DevOps Engineer Professional | DOP-C02 | Alta | 100-150h |
| Certified Kubernetes Administrator | CKA | Alta | 80-120h |
| HashiCorp Terraform Associate | 003 | Media | 40-60h |
| AWS Security Specialty | SCS-C02 | Alta | 100-150h |
**Regla crítica:** NUNCA reproduce preguntas reales de exámenes (copyright). Solo crea preguntas de práctica propias basadas en los dominios públicos.
---
## `agents/shared/github_client.py`
Módulo compartido por todos los agentes que crean contenido.
**Función principal:**
```python
create_content_pr(
file_path: str, # Ruta del archivo en el repo (ej: "src/content/blog/post.md")
content: str, # Contenido del archivo
title: str, # Título del PR
description: str, # Cuerpo del PR
agent_name: str, # Nombre del agente (ej: "Samwell")
branch_prefix: str, # Prefijo de rama (ej: "blog", "guiones", "seo")
) -> str # Devuelve la URL del PR
```
**Lo que hace internamente:**
1. Obtiene el SHA del último commit de `main`
2. Crea una rama: `{branch_prefix}/{agent_name}-{timestamp}-{slug}`
3. Hace commit del archivo en esa rama
4. Abre un PR con etiquetas `agent-created` y `needs-review`
5. Devuelve la URL del PR
**Nunca hace commit a `main`.**

View File

@@ -1,235 +0,0 @@
# Arquitectura del Sistema
## Visión General
`carlospalanca.es` es un sistema en tres capas que conecta un canal de YouTube tech con una web personal, un sistema de agentes de IA y un pipeline de CI/CD automatizado.
```
┌─────────────────────────────────────────────────────────────┐
│ Carlos (humano) │
│ │
│ Escribe en Discord ──► Revisa PRs en GitHub │
└────────────┬────────────────────────┬───────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌───────────────────────┐
│ Discord Server │ │ GitHub Repository │
│ │ │ carlospalanca.es │
│ #el-trono-de- │ │ │
│ hierro (Tyrion) │ │ main ──► CI/CD ──► │
│ #la-ciudadela │ │ PRs de agentes │
│ #el-pajarillo │ │ Branch protection │
│ #el-muro │ └──────────┬────────────┘
│ ... (9 canales) │ │
└────────┬───────────┘ │ merge
│ ▼
│ bot events ┌──────────────────────┐
▼ │ GitHub Actions │
┌────────────────────┐ │ │
│ VPS (Hetzner) │ │ ci.yml: build check │
│ │ │ deploy.yml: rsync │
│ ┌──────────────┐ │ └──────────┬───────────┘
│ │ 9 Discord │ │ │
│ │ Bot Agents │◄─┤ ◄────────────┘
│ │ (Docker) │ │ │ rsync dist/
│ └──────┬───────┘ │ ▼
│ │ │ ┌──────────────────────┐
│ │ HTTP API │ │ /var/www/ │
│ ▼ │ │ carlospalanca.es/ │
│ ┌──────────────┐ │ │ (archivos estáticos) │
│ │ OpenWebUI │ │ └──────────┬────────────┘
│ │ :3000 │ │ │
│ └──────────────┘ │ │ nginx sirve
│ │ │ ▼
│ ▼ │ ┌──────────────────────┐
│ ┌──────────────┐ │ │ https:// │
│ │ Nginx │ │ │ carlospalanca.es │
│ │ (proxy/TLS) │ │ │ (web pública) │
│ └──────────────┘ │ └──────────────────────┘
└────────────────────┘
```
---
## Componentes
### 1. Web (Astro)
**Tecnología:** Astro 5, MDX, sitemap, RSS
**Tipo:** Sitio estático generado (SSG)
**Contenido gestionado por:**
- Carlos directamente
- Agentes vía Pull Requests (nunca commits directos)
**Colecciones de contenido:**
| Colección | Ruta | Creador |
|-----------|------|---------|
| `blog` | `src/content/blog/` | Samwell / Carlos |
| `guiones` | `src/content/guiones/` | Samwell |
---
### 2. CI/CD (GitHub Actions)
**`ci.yml`** — Se ejecuta en cada Pull Request:
1. Checkout del código
2. `npm ci` — instala dependencias
3. `astro check` — type checking
4. `npm run build` — construye el sitio
5. Comenta en el PR si pasa o falla
**`deploy.yml`** — Se ejecuta al mergear a `main`:
1. Build del sitio (`dist/`)
2. rsync de `dist/` al VPS vía SSH
3. `nginx -t && systemctl reload nginx`
**GitHub Secrets necesarios:**
| Secret | Valor |
|--------|-------|
| `VPS_SSH_PRIVATE_KEY` | Clave privada SSH del usuario `deploy` |
| `VPS_HOST` | IP o hostname del VPS |
| `VPS_USER` | `deploy` |
---
### 3. VPS
**OS:** Ubuntu 24.04 LTS
**Proveedor recomendado:** Hetzner CX22 (~5€/mes, 2 vCPU, 4 GB RAM)
**Servicios corriendo:**
| Servicio | Puerto | Acceso |
|----------|--------|--------|
| nginx | 80, 443 | Público |
| OpenWebUI | 3000 | Solo localhost (proxificado por nginx) |
| 9 Discord bots | — | Outbound only |
**Estructura de directorios en el VPS:**
```
/var/www/carlospalanca.es/ ← Web estática (desplegada por GitHub Actions)
/opt/openwebui/ ← Docker Compose de OpenWebUI
/opt/agents/ ← Docker Compose de los 9 agentes
/etc/nginx/sites-available/ ← Config de nginx
/etc/letsencrypt/ ← Certificados SSL (Let's Encrypt)
```
---
### 4. OpenWebUI
**Imagen:** `ghcr.io/open-webui/open-webui:main`
**Función:** Gateway de LLM. Los agentes hacen llamadas HTTP a su API compatible con OpenAI.
**Endpoint que usan los agentes:**
```
POST https://ai.carlospalanca.es/api/chat/completions
Authorization: Bearer <OPENWEBUI_API_KEY>
```
**Ventaja del gateway centralizado:**
- Cambiar de GPT-4o a Claude o Llama solo requiere cambiar `OPENWEBUI_MODEL` en `.env`
- Dashboard web para monitorizar el uso de tokens
- No hay que redeployar los agentes para cambiar de modelo
---
### 5. Sistema de Agentes
**Patrón:** Cada agente es un Discord bot en Python (discord.py) que:
1. Escucha mensajes en su canal de Discord asignado
2. Llama a OpenWebUI con su system prompt + el mensaje del usuario
3. Ejecuta la acción correspondiente (crear PR, responder texto, etc.)
**Comunicación entre agentes:**
```
Carlos ──► #el-trono-de-hierro
Tyrion analiza y enruta
├──► #la-ciudadela (Samwell)
├──► #el-pajarillo (Varys)
├──► #el-muro (Bran)
└──► ... (resto de agentes)
```
Los agentes NO se llaman entre sí por API. Toda la comunicación es vía Discord.
---
### 6. GitHub Integration
**Token:** Fine-Grained PAT con scope mínimo:
- `Contents: Write` — crear ramas y archivos
- `Pull requests: Write` — abrir y comentar PRs
- Scoped **solo** al repo `carlospalanca.es`
**Flujo de creación de contenido:**
```
Agente genera contenido
agents/shared/github_client.py
1. Crea rama: tipo/agente-YYYYMMDD-slug
2. Commit del archivo en esa rama
3. Abre PR con etiquetas agent-created, needs-review
GitHub Actions ejecuta ci.yml
→ Build check
→ Comenta en el PR
Carlos revisa y mergea
GitHub Actions ejecuta deploy.yml
→ rsync a VPS
→ Web actualizada
```
---
## Flujo Completo de Ejemplo
**Solicitud:** "Tyrion, necesito un guión para un vídeo sobre Kubernetes Ingress"
```
1. Carlos escribe en #el-trono-de-hierro
2. Tyrion (bot) recibe el mensaje
→ Llama a OpenWebUI con su prompt de orquestador
→ Responde: "Entendido. He enviado la misión a Samwell en la Ciudadela."
→ Publica en #la-ciudadela: "[Delegado por Tyrion] Crear guión sobre Kubernetes Ingress..."
3. Samwell (bot) recibe la tarea en #la-ciudadela
→ Llama a OpenWebUI con su prompt de escritor + la tarea
→ Genera el guión completo en formato MDX con frontmatter
→ Llama a github_client.create_content_pr()
- Crea rama: guiones/samwell-20240321-kubernetes-ingress
- Hace commit de src/content/guiones/kubernetes-ingress.md
- Abre PR: "[Samwell] Guión: Kubernetes Ingress"
→ Responde en #la-ciudadela: "PR creado: https://github.com/..."
4. GitHub Actions (ci.yml) se activa
→ npm run build → OK
→ Comenta en el PR: "✅ Build exitoso"
5. Carlos revisa el PR, edita si necesita, hace merge
6. GitHub Actions (deploy.yml) se activa
→ Build → rsync → nginx reload
→ Web actualizada con el nuevo guión
```
---
## Seguridad
| Medida | Implementación |
|--------|---------------|
| GitHub token scope mínimo | Fine-grained PAT, solo este repo, solo contents+PRs |
| OpenWebUI solo en localhost | Bind a `127.0.0.1:3000`, no expuesto directamente |
| Nginx como única entrada | TLS termination en nginx, HTTPS forzado |
| Branch protection en main | Requiere PR + CI verde antes de mergear |
| Agentes sin acceso a main | Solo pueden crear ramas y abrir PRs |
| Secretos en `.env` | `.env` en `.gitignore`, nunca en el repo |

View File

@@ -1,199 +0,0 @@
# Coste, Rendimiento y Ganancia del Sistema
Análisis honesto de lo que cuesta cada pieza, cuánto rinde y qué ganas (o pierdes) con ella.
> **Contexto:** Tienes suscripción Claude Pro activa ($20/mes). Esto cambia significativamente el análisis.
---
## Lo primero: Claude Pro vs. Anthropic API — no son lo mismo
Este es el punto más importante antes de calcular cualquier coste.
| | Claude Pro ($20/mes) | Anthropic API (por tokens) |
|--|---------------------|---------------------------|
| **Acceso** | claude.ai (web/apps) | Llamadas HTTP programáticas |
| **Quién lo usa** | Tú, manualmente | Los agentes automáticamente |
| **Facturación** | Cuota fija mensual | Por tokens consumidos |
| **¿Sirve para los bots?** | ❌ No directamente | ✅ Sí |
**El resumen:** Claude Pro es para tu uso personal en claude.ai. Los agentes necesitan la Anthropic API, que se factura aparte por tokens.
**La buena noticia:** Ya conoces la calidad de Claude. Los agentes usarán exactamente el mismo Claude Sonnet que usas tú, solo que vía API. La experiencia de output será la misma que ya te ha convencido.
---
## Resumen de costes mensuales (con tu situación real)
| Componente | Coste/mes | Tipo | Notas |
|------------|-----------|------|-------|
| Claude Pro | $20 (~18 €) | Fijo | Ya lo pagas. Sunk cost. |
| VPS Hetzner CX22 | ~5 € | Fijo | Nuevo coste |
| Dominio carlospalanca.es | ~1 € | Fijo (amortizado) | Si no lo tienes ya |
| Anthropic API | 1-8 € | Variable | Ver tabla de consumo abajo |
| Discord | 0 € | Gratuito | |
| GitHub | 0 € | Gratuito | |
| Let's Encrypt | 0 € | Gratuito | |
| **Coste nuevo total** | **~6-14 €/mes** | | Sobre lo que ya pagas |
| **Coste total real** | **~24-32 €/mes** | | Incluyendo Claude Pro |
El sistema completo te cuesta **~6-14 € más al mes** sobre lo que ya gastas en Claude Pro.
---
## Claude Pro: ¿qué ya tienes cubierto?
Con tu suscripción Pro ya puedes hacer manualmente todo lo que harán los agentes. El sistema lo que hace es **automatizar y quitar fricción** a ese trabajo, no añadir una capacidad que no tenías.
**Lo que cubres con Pro (uso manual):**
- Pedir guiones, artículos, SEO a Claude en claude.ai
- Subir archivos, analizar documentos, proyectos prolongados
- Acceso a Claude Opus, Sonnet y Haiku
**Lo que añade el sistema de agentes:**
- Los agentes hacen esas mismas peticiones automáticamente cuando escribes en Discord
- El resultado llega directamente como PR en GitHub, listo para revisar
- No tienes que abrir el navegador, copiar/pegar, crear el archivo, commitear...
---
## Coste de la Anthropic API para los agentes
**Precios de referencia (marzo 2025):**
| Modelo | Input | Output | Mejor para |
|--------|-------|--------|-----------|
| Claude 3.5 Sonnet | $3.00 / 1M tokens | $15.00 / 1M tokens | Guiones, artículos largos |
| Claude 3.5 Haiku | $0.80 / 1M tokens | $4.00 / 1M tokens | SEO, redes sociales, tareas cortas |
| Claude 3 Opus | $15.00 / 1M tokens | $75.00 / 1M tokens | No necesario para este uso |
**¿Cuánto gasta cada agente por tarea?**
| Agente | Tarea típica | Tokens estimados | Coste Sonnet | Coste Haiku |
|--------|-------------|-----------------|--------------|-------------|
| Samwell | Guión completo 15 min | ~5.000 output | ~0.075 € | ~0.020 € |
| Samwell | Artículo de blog | ~3.000 output | ~0.045 € | ~0.012 € |
| Varys | SEO completo (títulos + tags + descripción) | ~800 output | ~0.012 € | ~0.003 € |
| Davos | Posts para 3 plataformas | ~1.000 output | ~0.015 € | ~0.004 € |
| Bronn | Informe de sponsor | ~1.200 output | ~0.018 € | ~0.005 € |
| Jon | Guía de certificación | ~4.000 output | ~0.060 € | ~0.016 € |
| Tyrion | Enrutamiento (respuesta corta) | ~200 output | ~0.003 € | ~0.001 € |
**Estimación mensual por escenario (2 vídeos/semana = 8 al mes):**
| Escenario | Agentes usados | Tokens/mes | Coste Sonnet | Coste Haiku |
|-----------|---------------|------------|--------------|-------------|
| Mínimo (solo Samwell + Varys) | 2 | ~500K | ~3 € | ~0.8 € |
| Moderado (todos los agentes) | 9 | ~2M | ~8 € | ~2 € |
| Intensivo (mucha experimentación) | 9 | ~5M | ~20 € | ~5 € |
**Recomendación con tu setup:**
- **Samwell, Jon, Daenerys** (tareas largas y creativas): usa **Claude 3.5 Sonnet** — misma calidad que tienes en Pro
- **Varys, Davos, Bronn, Arya, Bran** (tareas cortas y estructuradas): usa **Claude 3.5 Haiku** — más de 3× más barato, calidad suficiente para SEO y social
Esto se configura por agente en `agents/docker-compose.yml` cambiando `OPENWEBUI_MODEL` en cada servicio.
---
## La estrategia óptima: mezcla Pro + API
Dado que ya tienes Pro, el flujo más rentable es:
**Para trabajo creativo propio:** usa claude.ai (cubierto por tu Pro)
- Brainstorming de ideas de vídeo
- Revisión y ajuste de los guiones que genera Samwell
- Conversaciones largas de estrategia
**Para automatización repetitiva:** usa la API vía agentes (coste variable bajo)
- SEO de cada vídeo → Varys (Haiku, ~0.003 € por vídeo)
- Guión base → Samwell (Sonnet, ~0.075 € por vídeo)
- Posts de redes → Davos (Haiku, ~0.004 € por vídeo)
- Búsqueda de sponsors → Bronn (Haiku, puntual)
**Coste real por vídeo publicado con esta estrategia: ~0.10-0.15 €**
---
## VPS — 5 €/mes (Hetzner CX22)
**Coste:** ~5 €/mes (2 vCPU, 4 GB RAM, 40 GB SSD)
**Qué corre dentro:**
- nginx (web estática, ~0 recursos)
- OpenWebUI (~300 MB RAM en idle)
- 9 bots Discord (~50 MB RAM × 9 = ~450 MB)
- Total estimado: ~1.5 GB RAM de 4 GB disponibles
**¿Es suficiente?** Sí, con margen. Como no vas a usar Ollama (ya tienes Claude vía API), el CX22 es más que suficiente indefinidamente.
**Alternativa gratuita:** Oracle Cloud Free Tier (4 vCPU ARM, 24 GB RAM). Funciona, pero la fiabilidad es menor y el setup más complejo.
---
## ROI: ¿Qué ganas vs. qué cuesta de más?
### Tiempo ahorrado por vídeo
| Tarea | Tiempo manual (con claude.ai) | Con agentes | Ahorro |
|-------|------------------------------|-------------|--------|
| SEO (abrir claude.ai, pedir, copiar a YouTube) | 20 min | 2 min (revisar) | 18 min |
| Guión base (pedir, copiar, formatear, subir a repo) | 40 min | 10 min (revisar PR) | 30 min |
| Artículo de blog (pedir, formatear MDX, crear archivo, commit) | 35 min | 5 min (revisar PR) | 30 min |
| Posts de redes (pedir, copiar a cada plataforma) | 20 min | 3 min (revisar) | 17 min |
| **Total por vídeo** | **115 min** | **20 min** | **95 min (83%)** |
> Nota: el tiempo manual ya es bajo porque usas Claude Pro. El ahorro del sistema es principalmente en el **copy-paste, formateo y commit** — la parte mecánica que queda después de tener la IA.
Con 2 vídeos/semana: **~3 horas ahorradas/semana = ~12 horas/mes**
### Valoración
- 12 horas/mes × 30 €/h = **360 € en tiempo recuperado**
- Coste adicional del sistema: **~6-14 €/mes**
- **ROI: ~25-60x sobre el coste nuevo**
O dicho de otra forma: el sistema amortiza su coste mensual en **menos de 1 hora de trabajo recuperado**.
---
## ¿Dónde NO merece la pena gastar?
### OpenAI API además de Anthropic API
Ya tienes Claude Pro y conoces la calidad. No hay razón para pagar también OpenAI. Usa Claude para todo.
### Claude Pro Max ($100/mes)
Para este caso de uso, Pro a $20 es más que suficiente. Max está pensado para uso intensivo personal en claude.ai, no para el sistema de agentes.
### Ollama en VPS
Con acceso a Claude Sonnet vía API a ~0.10 €/vídeo, no tiene ningún sentido gastar 15-20 €/mes extra en un VPS más grande para correr modelos locales de peor calidad.
### GitHub Pro
No llegarás al límite de Actions gratuitas. No hace falta.
### Discord Nitro
Los bots funcionan igual sin Nitro.
---
## Escalado: ¿qué cambia si el canal crece?
| Escenario | Cambio necesario | Coste adicional |
|-----------|-----------------|----------------|
| Más vídeos/semana (4-5) | Ninguno | ~0.50-1 €/semana más en API |
| Tráfico web alto (50K visitas/mes) | CDN Cloudflare (gratis) | 0 € |
| Añadir agentes nuevos | Nuevo bot Discord + container Docker | ~1-2 €/mes en tokens |
| Equipo colaborando | GitHub repo privado + más canales Discord | 0 €/mes |
---
## Decisión rápida según tu situación
Dado que ya tienes Claude Pro activo:
1. **Arrancar (coste nuevo: ~5-6 €/mes):**
Solo el VPS + dominio. Solo Tyrion + Samwell + Varys. Suficiente para automatizar el 80% del trabajo de un vídeo.
2. **Sistema completo (coste nuevo: ~8-12 €/mes):**
Los 9 agentes. Añade Haiku para los agentes de tareas cortas y Sonnet para Samwell y Jon.
**Recomendación:** Ve al punto 2 directamente. La diferencia entre 1 y 2 es de ~4-6 €/mes y tienes los 9 agentes desde el primer día. Con lo que ya gastas en Claude Pro, es marginal.

View File

@@ -1,106 +0,0 @@
# Setup 01 — GitHub
## 1. Crear el repositorio
1. Ve a [github.com/new](https://github.com/new)
2. Nombre: `carlospalanca.es`
3. Visibilidad: **Public** (recomendado para web personal) o Private
4. **No** inicialices con README (ya tienes el código)
5. Clic en "Create repository"
## 2. Push inicial
Desde tu máquina local en la carpeta del proyecto:
```bash
git init
git add .
git commit -m "feat: initial project setup with agents"
git branch -M main
git remote add origin https://github.com/TU_USUARIO/carlospalanca.es.git
git push -u origin main
```
## 3. Crear el GitHub Fine-Grained PAT
Este token lo usarán los agentes para crear ramas y PRs.
1. Ve a [github.com/settings/tokens](https://github.com/settings/tokens)
2. Clic en **"Fine-grained tokens"** → **"Generate new token"**
3. Configura:
- **Token name:** `carlospalanca-agents`
- **Expiration:** 1 año (o "No expiration")
- **Resource owner:** Tu usuario
- **Repository access:** Only selected repositories → selecciona `carlospalanca.es`
4. En **Repository permissions**, activa:
- **Contents:** Read and write
- **Pull requests:** Read and write
5. Clic en **"Generate token"**
6. **Copia el token** (empieza por `github_pat_...`) — solo lo verás una vez
> Guarda este token como `GITHUB_TOKEN` en el `.env` de los agentes.
## 4. Añadir GitHub Secrets
Estos secretos los usa el workflow de deploy.
1. Ve a tu repo → **Settings****Secrets and variables****Actions**
2. Clic en **"New repository secret"** y añade:
| Nombre | Valor |
|--------|-------|
| `VPS_SSH_PRIVATE_KEY` | Contenido completo de la clave privada SSH del usuario `deploy` |
| `VPS_HOST` | IP pública del VPS (ej: `123.456.789.0`) |
| `VPS_USER` | `deploy` |
> Para obtener la clave privada: ejecuta `cat ~/.ssh/id_ed25519` en el VPS (o la ruta donde generaste la keypair del usuario deploy).
## 5. Crear Labels
1. Ve a tu repo → **Issues****Labels**
2. Crea estos labels (o ve a la URL: `https://github.com/TU_USUARIO/carlospalanca.es/labels`):
| Label | Color | Descripción |
|-------|-------|-------------|
| `agent-created` | `#0075ca` | Contenido creado por un agente de IA |
| `needs-review` | `#e4e669` | Requiere revisión humana antes de mergear |
| `approved` | `#0e8a16` | Aprobado por Carlos, listo para mergear |
## 6. Activar Branch Protection en `main`
1. Ve a **Settings****Branches****Add branch ruleset** (o "Add rule")
2. Branch name pattern: `main`
3. Activa:
-**Require a pull request before merging**
- ✅ Require approvals: 0 (eres tú solo, no necesitas aprobador extra)
-**Require status checks to pass before merging**
- Busca y añade: `Build & Validate` (el job del ci.yml)
-**Do not allow bypassing the above settings**
4. Clic en **"Create"**
> Esto garantiza que ni tú ni los agentes puedan hacer push directo a `main`.
## 7. Verificar que el CI funciona
1. Crea un branch de prueba:
```bash
git checkout -b test/ci-check
echo "test" >> README.md
git add README.md
git commit -m "test: verify CI pipeline"
git push origin test/ci-check
```
2. Abre un PR en GitHub
3. Verifica que el workflow `CI - Build Check` se ejecuta y pasa
4. Verifica que comenta "✅ Build exitoso" en el PR
5. Puedes cerrar el PR sin mergear
## Checklist
- [ ] Repositorio creado en GitHub
- [ ] Push inicial completado
- [ ] Fine-Grained PAT creado y copiado
- [ ] 3 GitHub Secrets añadidos (VPS_SSH_PRIVATE_KEY, VPS_HOST, VPS_USER)
- [ ] Labels creados (agent-created, needs-review, approved)
- [ ] Branch protection activada en `main`
- [ ] CI workflow verificado en PR de prueba

View File

@@ -1,187 +0,0 @@
# Setup 02 — VPS
## 1. Contratar el VPS
**Recomendado:** Hetzner CX22 (~5€/mes)
- 2 vCPU, 4 GB RAM, 40 GB SSD
- Suficiente para: web estática + OpenWebUI + 9 bots Docker
**Alternativas:**
- DigitalOcean Droplet 2GB (~12$/mes)
- Linode Nanode 2GB (~12$/mes)
- Oracle Cloud Free Tier (gratuito, 4 vCPU ARM, 24 GB RAM)
**Durante la creación:**
- OS: Ubuntu 24.04 LTS
- Añade tu clave SSH pública para acceder sin contraseña
- Anota la IP pública del VPS
## 2. Acceder al VPS
```bash
ssh root@<IP_DEL_VPS>
```
## 3. Actualizar el sistema
```bash
apt-get update && apt-get upgrade -y
apt-get install -y curl wget git ufw fail2ban
```
## 4. Configurar el firewall (UFW)
```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
```
## 5. Crear el usuario `deploy`
El usuario `deploy` es el que GitHub Actions usará para subir la web.
```bash
# Crear usuario
useradd -m -s /bin/bash deploy
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
# Generar keypair para GitHub Actions
ssh-keygen -t ed25519 -f /home/deploy/.ssh/github_actions -N "" -C "github-actions@carlospalanca.es"
# Autorizar la clave pública en el VPS
cat /home/deploy/.ssh/github_actions.pub >> /home/deploy/.ssh/authorized_keys
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
# Ver la clave privada (cópiala como GitHub Secret VPS_SSH_PRIVATE_KEY)
cat /home/deploy/.ssh/github_actions
```
> **IMPORTANTE:** Copia el output del último comando completo (incluyendo `-----BEGIN OPENSSH PRIVATE KEY-----` y `-----END OPENSSH PRIVATE KEY-----`) y pégalo como el secret `VPS_SSH_PRIVATE_KEY` en GitHub.
## 6. Crear directorio de la web
```bash
mkdir -p /var/www/carlospalanca.es
chown deploy:deploy /var/www/carlospalanca.es
```
## 7. Instalar nginx
```bash
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
```
## 8. Instalar Docker y Docker Compose
```bash
# Instalar Docker
curl -fsSL https://get.docker.com | sh
# Añadir el usuario root y deploy al grupo docker
usermod -aG docker root
usermod -aG docker deploy
# Verificar
docker --version
docker compose version
```
## 9. Instalar Certbot (SSL)
```bash
apt-get install -y certbot python3-certbot-nginx
```
## 10. Configurar DNS
En tu proveedor de DNS (Cloudflare, Namecheap, etc.), añade estos registros:
| Tipo | Nombre | Valor |
|------|--------|-------|
| A | `carlospalanca.es` | `<IP_DEL_VPS>` |
| A | `www.carlospalanca.es` | `<IP_DEL_VPS>` |
| A | `ai.carlospalanca.es` | `<IP_DEL_VPS>` |
> Los DNS pueden tardar hasta 24h en propagarse. Verifica con: `ping carlospalanca.es`
## 11. Configurar nginx
```bash
# Copiar la config (desde tu máquina local)
scp vps/nginx/carlospalanca.conf root@<IP_DEL_VPS>:/etc/nginx/sites-available/carlospalanca.es
# En el VPS:
ln -sf /etc/nginx/sites-available/carlospalanca.es /etc/nginx/sites-enabled/
# Eliminar la config por defecto
rm -f /etc/nginx/sites-enabled/default
# Verificar sintaxis
nginx -t
# Recargar
systemctl reload nginx
```
## 12. Obtener certificados SSL
> Espera a que el DNS haya propagado antes de este paso.
```bash
certbot --nginx \
-d carlospalanca.es \
-d www.carlospalanca.es \
-d ai.carlospalanca.es \
--agree-tos \
--email tu@email.com
```
Certbot actualizará automáticamente la config de nginx con los certificados y configurará la renovación automática.
## 13. Verificar
```bash
# Comprobar que nginx está activo
systemctl status nginx
# Comprobar que el SSL funciona
curl -I https://carlospalanca.es
# Ver los logs de nginx si hay error
tail -f /var/log/nginx/error.log
```
## 14. Permitir a `deploy` recargar nginx sin contraseña
El workflow de deploy ejecuta `sudo systemctl reload nginx`. Necesitas configurar sudoers para esto:
```bash
echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx, /usr/sbin/nginx" \
>> /etc/sudoers.d/deploy-nginx
chmod 440 /etc/sudoers.d/deploy-nginx
```
## Checklist
- [ ] VPS contratado (Ubuntu 24.04)
- [ ] Acceso SSH verificado
- [ ] Sistema actualizado
- [ ] Firewall UFW configurado (80, 443, 22)
- [ ] Usuario `deploy` creado con SSH key
- [ ] Clave privada copiada como GitHub Secret
- [ ] Directorio `/var/www/carlospalanca.es/` creado
- [ ] nginx instalado
- [ ] Docker + Docker Compose instalados
- [ ] Certbot instalado
- [ ] DNS apuntando al VPS (carlospalanca.es, www, ai)
- [ ] nginx configurado y recargado
- [ ] Certificados SSL obtenidos
- [ ] `deploy` puede recargar nginx sin contraseña (sudoers)
- [ ] `https://carlospalanca.es` responde (aunque esté vacío)

View File

@@ -1,132 +0,0 @@
# Setup 03 — OpenWebUI
OpenWebUI es el gateway de LLM. Los 9 agentes lo llaman vía API en lugar de llamar directamente a OpenAI/Anthropic. Esto centraliza el modelo y permite cambiarlo sin redeployar los bots.
## 1. Subir el archivo de configuración al VPS
Desde tu máquina local:
```bash
# Crear el directorio en el VPS
ssh root@<IP_VPS> "mkdir -p /opt/openwebui"
# Subir el docker-compose
scp vps/docker-compose.openwebui.yml root@<IP_VPS>:/opt/openwebui/docker-compose.yml
```
## 2. Crear el archivo `.env` en el VPS
```bash
ssh root@<IP_VPS>
cd /opt/openwebui
```
Crea el archivo `/opt/openwebui/.env`:
```bash
cat > .env << 'EOF'
# Genera con: openssl rand -hex 32
WEBUI_SECRET_KEY=CAMBIA_ESTO_POR_STRING_ALEATORIO
# Modelo por defecto que verán los usuarios en la UI
DEFAULT_MODEL=gpt-4o
# Pon SOLO la clave del proveedor que vayas a usar:
# OpenAI (GPT-4o, GPT-4, etc.)
OPENAI_API_KEY=sk-...
# Anthropic (Claude 3.5 Sonnet, Claude 3 Opus, etc.)
# ANTHROPIC_API_KEY=sk-ant-...
EOF
```
**Para generar un WEBUI_SECRET_KEY seguro:**
```bash
openssl rand -hex 32
```
## 3. Levantar OpenWebUI
```bash
cd /opt/openwebui
docker compose up -d
# Verificar que está corriendo
docker compose ps
docker compose logs -f openwebui
```
Espera a que aparezca `Application startup complete` en los logs.
## 4. Verificar acceso
Abre en el navegador: `https://ai.carlospalanca.es`
Deberías ver la pantalla de login de OpenWebUI.
1. Crea una cuenta de administrador (la primera cuenta es admin automáticamente)
2. Verifica que puedes chatear (Settings → Models → selecciona un modelo)
## 5. Crear el API Key para los agentes
Los agentes necesitan un API key para autenticarse contra OpenWebUI.
1. Inicia sesión en `https://ai.carlospalanca.es`
2. Ve a **Settings** (icono de usuario arriba a la derecha)
3. **Account****API Keys**
4. Clic en **"Create new secret key"**
5. Nombre: `agents-key`
6. **Copia el token** (empieza por `sk-...`) — solo lo verás una vez
> Guarda este token como `OPENWEBUI_API_KEY` en el `.env` de los agentes.
## 6. Verificar el API con curl
```bash
curl -X POST https://ai.carlospalanca.es/api/chat/completions \
-H "Authorization: Bearer <TU_OPENWEBUI_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Responde solo: OK"}]
}'
```
Debería responder algo como: `{"choices":[{"message":{"content":"OK",...`
## 7. (Opcional) Añadir modelos de Ollama para uso local
Si quieres usar modelos locales (Llama 3.1, Mistral, etc.) además de los de OpenAI:
En `docker-compose.yml`, descomenta la sección de `ollama` y la variable `OLLAMA_BASE_URL`.
Luego:
```bash
docker compose up -d
# Descargar un modelo (ej: llama3.1:8b)
docker exec -it ollama ollama pull llama3.1:8b
```
> Los modelos locales requieren más RAM. Para el CX22 de Hetzner (4 GB), usa solo modelos de 7-8B.
## Cambiar el modelo que usan los agentes
Para cambiar de GPT-4o a Claude (sin tocar código):
1. En `/opt/agents/.env`, cambia:
```bash
OPENWEBUI_MODEL=claude-3-5-sonnet-20241022
```
2. `docker compose restart` en la carpeta de agentes
## Checklist
- [ ] `/opt/openwebui/` creado en el VPS
- [ ] `docker-compose.yml` subido
- [ ] `.env` creado con las API keys
- [ ] `docker compose up -d` ejecutado
- [ ] `https://ai.carlospalanca.es` accesible
- [ ] Cuenta de admin creada
- [ ] API Key creada y copiada
- [ ] Verificado con curl que el API responde

View File

@@ -1,125 +0,0 @@
# Setup 04 — Discord Apps y Bots
Necesitas crear **9 Discord Applications** separadas (una por agente). Cada una tiene su propio bot token.
## 1. Activar Developer Mode en Discord
1. Abre Discord → **Ajustes** (icono de rueda)
2. **Avanzado** → activa **"Modo desarrollador"**
Esto te permitirá hacer click derecho en canales y copiar sus IDs.
## 2. Crear los 9 canales en tu servidor Discord
Ve a tu servidor → crea estos canales de texto:
| Canal | Agente |
|-------|--------|
| `el-trono-de-hierro` | Tyrion (orquestador) |
| `el-pajarillo` | Varys |
| `la-ciudadela` | Samwell |
| `el-banco-de-hierro` | Bronn |
| `el-muro` | Bran |
| `desembarco-del-rey` | Davos |
| `cara-sin-nombre` | Arya |
| `poniente-en-llamas` | Daenerys |
| `la-guardia-de-la-noche` | Jon |
**Recomendado:** Crea una categoría llamada `LA MANO DEL REY` y agrupa todos los canales ahí.
## 3. Copiar los Channel IDs
Para cada canal:
1. Click derecho en el canal
2. Clic en **"Copiar ID del canal"**
3. Guárdalo — lo necesitarás para el `.env` de los agentes
## 4. Crear las 9 Discord Applications
Repite este proceso 9 veces (una por agente):
### Paso a paso para CADA bot:
1. Ve a [discord.com/developers/applications](https://discord.com/developers/applications)
2. Clic en **"New Application"**
3. Nombre según la tabla:
| Application Name | Variable del token |
|-----------------|-------------------|
| `Tyrion Lannister` | `DISCORD_TOKEN_TYRION` |
| `Varys` | `DISCORD_TOKEN_VARYS` |
| `Samwell Tarly` | `DISCORD_TOKEN_SAMWELL` |
| `Bronn` | `DISCORD_TOKEN_BRONN` |
| `Bran Stark` | `DISCORD_TOKEN_BRAN` |
| `Davos Seaworth` | `DISCORD_TOKEN_DAVOS` |
| `Arya Stark` | `DISCORD_TOKEN_ARYA` |
| `Daenerys Targaryen` | `DISCORD_TOKEN_DAENERYS` |
| `Jon Snow` | `DISCORD_TOKEN_JON` |
4. Acepta los Terms of Service → **"Create"**
5. Ve a la pestaña **"Bot"** (menú izquierdo)
6. Activa los **Privileged Gateway Intents**:
-**Message Content Intent** (OBLIGATORIO)
- Los otros son opcionales
7. Clic en **"Reset Token"** → **"Yes, do it"** → **Copia el token**
> Guárdalo inmediatamente, solo se muestra una vez.
8. Ve a **OAuth2****URL Generator**:
- Scopes: ✅ `bot`
- Bot permissions: ✅ `Read Messages/View Channels`, ✅ `Send Messages`, ✅ `Read Message History`
- Copia la URL generada
9. Abre la URL en el navegador → selecciona tu servidor → **"Autorizar"**
Repite los pasos 2-9 para los 9 agentes.
## 5. Verificar que los bots están en el servidor
En tu servidor Discord, en la lista de miembros o en los canales, deberías ver los 9 bots como miembros (aparecerán como offline hasta que los levantes).
## 6. (Opcional) Personalizar los bots
Para que cada bot tenga avatar y descripción:
1. En la app de Discord Developer → **General Information**
2. Sube una foto de perfil (imagen del personaje de GoT)
3. Añade descripción
## Checklist
- [ ] Developer Mode activado en Discord
- [ ] 9 canales creados en el servidor
- [ ] 9 Channel IDs copiados
- [ ] 9 Discord Applications creadas
- [ ] "Message Content Intent" activado en las 9
- [ ] 9 Bot Tokens copiados
- [ ] Los 9 bots invitados al servidor
## Resumen de variables `.env` de esta fase
```bash
# Tokens (de Discord Developer Portal)
DISCORD_TOKEN_TYRION=<token>
DISCORD_TOKEN_VARYS=<token>
DISCORD_TOKEN_SAMWELL=<token>
DISCORD_TOKEN_BRONN=<token>
DISCORD_TOKEN_BRAN=<token>
DISCORD_TOKEN_DAVOS=<token>
DISCORD_TOKEN_ARYA=<token>
DISCORD_TOKEN_DAENERYS=<token>
DISCORD_TOKEN_JON=<token>
# Channel IDs (click derecho en canal > Copiar ID)
DISCORD_CHANNEL_TRONO=<id> # #el-trono-de-hierro
DISCORD_CHANNEL_VARYS=<id> # #el-pajarillo
DISCORD_CHANNEL_SAMWELL=<id> # #la-ciudadela
DISCORD_CHANNEL_BRONN=<id> # #el-banco-de-hierro
DISCORD_CHANNEL_BRAN=<id> # #el-muro
DISCORD_CHANNEL_DAVOS=<id> # #desembarco-del-rey
DISCORD_CHANNEL_ARYA=<id> # #cara-sin-nombre
DISCORD_CHANNEL_DAENERYS=<id> # #poniente-en-llamas
DISCORD_CHANNEL_JON=<id> # #la-guardia-de-la-noche
```

View File

@@ -1,210 +0,0 @@
# Setup 05 — Despliegue de Agentes
Este es el último paso. Asegúrate de haber completado los setups 01-04 antes de continuar.
## Prerequisitos
Antes de empezar, debes tener:
- ✅ GitHub PAT creado (`GITHUB_TOKEN` y `GITHUB_REPO`)
- ✅ OpenWebUI funcionando (`OPENWEBUI_URL` y `OPENWEBUI_API_KEY`)
- ✅ 9 Discord bot tokens (`DISCORD_TOKEN_*`)
- ✅ 9 Discord channel IDs (`DISCORD_CHANNEL_*`)
## 1. Subir el código de agentes al VPS
Desde tu máquina local:
```bash
# Opción A: rsync (recomendado)
rsync -avz --exclude '.env' agents/ root@<IP_VPS>:/opt/agents/
# Opción B: desde el VPS, clonar el repo directamente
ssh root@<IP_VPS>
git clone https://github.com/TU_USUARIO/carlospalanca.es.git /tmp/repo
cp -r /tmp/repo/agents /opt/agents
```
## 2. Crear el archivo `.env` en el VPS
```bash
ssh root@<IP_VPS>
cp /opt/agents/.env.example /opt/agents/.env
nano /opt/agents/.env
```
Rellena TODOS los valores. El archivo debe quedar así:
```bash
# OpenWebUI
OPENWEBUI_URL=https://ai.carlospalanca.es
OPENWEBUI_API_KEY=sk-... # Del paso 03
OPENWEBUI_MODEL=gpt-4o # O: claude-3-5-sonnet-20241022
# GitHub
GITHUB_TOKEN=github_pat_... # Del paso 01
GITHUB_REPO=TU_USUARIO/carlospalanca.es
# Discord bot tokens (del paso 04)
DISCORD_TOKEN_TYRION=...
DISCORD_TOKEN_VARYS=...
DISCORD_TOKEN_SAMWELL=...
DISCORD_TOKEN_BRONN=...
DISCORD_TOKEN_BRAN=...
DISCORD_TOKEN_DAVOS=...
DISCORD_TOKEN_ARYA=...
DISCORD_TOKEN_DAENERYS=...
DISCORD_TOKEN_JON=...
# Discord channel IDs (del paso 04)
DISCORD_CHANNEL_TRONO=...
DISCORD_CHANNEL_VARYS=...
DISCORD_CHANNEL_SAMWELL=...
DISCORD_CHANNEL_BRONN=...
DISCORD_CHANNEL_BRAN=...
DISCORD_CHANNEL_DAVOS=...
DISCORD_CHANNEL_ARYA=...
DISCORD_CHANNEL_DAENERYS=...
DISCORD_CHANNEL_JON=...
```
## 3. Construir y levantar los agentes
```bash
cd /opt/agents
# Construir las imágenes (la primera vez tarda 2-5 minutos)
docker compose build
# Levantar todos los bots
docker compose up -d
# Verificar que todos están corriendo
docker compose ps
```
Deberías ver los 9 servicios con estado `running`.
## 4. Verificar logs de cada agente
```bash
# Ver logs de Tyrion
docker compose logs -f tyrion
# Ver logs de todos
docker compose logs -f
```
Deberías ver para cada bot:
```
[TYRION] Conectado como Tyrion Lannister#1234
[SAMWELL] Conectado como Samwell Tarly#5678
...
```
## 5. Verificar en Discord
En tu servidor Discord, los 9 bots deberían aparecer ahora como **online** (punto verde).
## 6. Test del flujo completo
### Test 1: Tyrion responde
Escribe en `#el-trono-de-hierro`:
```
Tyrion, ¿estás ahí?
```
Tyrion debería responder en el canal.
### Test 2: Enrutamiento a Varys
Escribe en `#el-trono-de-hierro`:
```
Tyrion, necesito los mejores títulos para un vídeo sobre Docker Compose
```
Deberías ver:
1. Tyrion responde en `#el-trono-de-hierro` confirmando que delega a Varys
2. En `#el-pajarillo` aparece el mensaje con la tarea
3. Varys procesa y responde con los títulos
### Test 3: Samwell crea un PR
Escribe en `#el-trono-de-hierro`:
```
Tyrion, pide a Samwell que cree un artículo de blog corto sobre qué es un contenedor Docker
```
Deberías ver:
1. Tyrion delega a Samwell
2. Samwell genera el artículo y crea un PR en GitHub
3. Samwell responde en `#la-ciudadela` con la URL del PR
4. En GitHub aparece el PR con etiquetas `agent-created` y `needs-review`
5. El workflow CI comenta en el PR si el build pasa
## 7. Comandos útiles de mantenimiento
```bash
cd /opt/agents
# Reiniciar un agente específico
docker compose restart tyrion
# Reiniciar todos
docker compose restart
# Ver logs en tiempo real de un agente
docker compose logs -f samwell
# Parar todos los agentes
docker compose stop
# Actualizar después de cambios en el código
git pull # En el repo clonado, o rsync de nuevo
docker compose build
docker compose up -d
# Ver uso de recursos
docker stats
```
## 8. Actualizar los system prompts sin rebuilder
Los `prompt.txt` se leen en el arranque del bot. Para actualizar un prompt:
```bash
# Editar el prompt
nano /opt/agents/samwell/prompt.txt
# Reiniciar solo ese bot (sin rebuild)
docker compose restart samwell
```
## Troubleshooting
**El bot no responde en Discord:**
```bash
docker compose logs tyrion
# Busca errores de autenticación Discord (token incorrecto)
# o errores de conexión a OpenWebUI
```
**Error "Message Content Intent not enabled":**
→ Ve a Discord Developer Portal → la app del bot → Bot → activa "Message Content Intent"
→ Reinicia: `docker compose restart <agente>`
**Error "401 Unauthorized" al llamar OpenWebUI:**
→ Verifica que `OPENWEBUI_API_KEY` en `.env` es correcto
→ Verifica que `OPENWEBUI_URL` no tiene barra al final
**Samwell no crea el PR:**
→ Verifica `GITHUB_TOKEN` y `GITHUB_REPO` en `.env`
→ Verifica que el token tiene permisos `Contents: Write` y `Pull requests: Write`
→ Verifica que el repositorio existe y tiene rama `main`
## Checklist
- [ ] Código de agentes subido a `/opt/agents/` en el VPS
- [ ] `.env` creado con todos los tokens y channel IDs
- [ ] `docker compose build` completado sin errores
- [ ] `docker compose up -d` ejecutado
- [ ] Los 9 servicios aparecen como `running` en `docker compose ps`
- [ ] Los 9 bots aparecen online en Discord
- [ ] Test 1: Tyrion responde en `#el-trono-de-hierro`
- [ ] Test 2: Tyrion enruta a Varys
- [ ] Test 3: Samwell crea un PR en GitHub
- [ ] CI workflow comenta en el PR

View File

View File

View File

@@ -1,245 +0,0 @@
# Qué es cada pieza del sistema y para qué sirve
Una guía rápida para entender por qué existe cada tecnología y qué pasaría si no la tuvieras.
---
## Astro
**¿Qué es?**
Un framework para construir sitios web. Genera HTML estático a partir de componentes y archivos Markdown/MDX.
**¿Para qué lo usamos?**
Es la web pública `carlospalanca.es`. Sirve el blog, los artículos y el contenido del canal.
**¿Por qué Astro y no WordPress/otro?**
- El output es HTML puro — se sirve desde nginx sin ningún proceso Node corriendo en el servidor
- Los agentes pueden crear contenido nuevo simplemente añadiendo archivos `.md` vía Pull Request
- Muy rápido y barato de hostear (solo archivos estáticos)
**¿Qué pasaría sin él?**
No tendrías web donde publicar el contenido que generan los agentes.
---
## GitHub
**¿Qué es?**
Plataforma de control de versiones y colaboración de código.
**¿Para qué lo usamos?**
- Almacena todo el código del proyecto
- Es donde los agentes crean Pull Requests con contenido nuevo
- Es el punto de partida del pipeline de despliegue (push a main → web actualizada)
- Sirve como historial y trazabilidad de todo lo que hacen los agentes
**¿Por qué es la "fuente de verdad"?**
Porque todo cambio pasa por aquí: el agente crea la rama, tú revisas el PR, lo mergeas, y la web se actualiza sola. Nada llega a producción sin pasar por GitHub.
**Fine-Grained PAT (token de GitHub):**
Es una llave con permisos muy limitados que usan los agentes para crear ramas y PRs. Al tener scope mínimo (solo este repo, solo contents + pull_requests), aunque un agente se comporte mal no puede tocar nada fuera de su scope.
**¿Qué pasaría sin él?**
Los agentes no tendrían dónde guardar el contenido que generan ni forma de enviártelo para revisión.
---
## GitHub Actions
**¿Qué es?**
El sistema de automatización de GitHub. Ejecuta scripts automáticamente cuando ocurren eventos (push, PR, merge).
**¿Para qué lo usamos?**
Dos workflows:
| Workflow | Cuándo se ejecuta | Qué hace |
|----------|------------------|----------|
| `ci.yml` | En cada PR | Construye el sitio y comenta si el build pasa o falla |
| `deploy.yml` | Al mergear a main | Construye el sitio y lo sube al VPS via rsync |
**¿Por qué es importante el CI en los PRs?**
Cuando Samwell crea un PR con un artículo de blog, puede tener un error en el frontmatter (YAML mal escrito, campo que falta) que rompa el build. El CI lo detecta antes de que llegue a producción y te lo avisa con un comentario en el PR.
**¿Qué pasaría sin él?**
Tendrías que hacer el build y el deploy manualmente cada vez, y podrías subir contenido roto a producción sin saberlo.
---
## VPS (Servidor Virtual Privado)
**¿Qué es?**
Un servidor Linux en la nube que está encendido 24/7 y es accesible desde internet.
**¿Para qué lo usamos?**
- Sirve la web estática de Astro (via nginx)
- Corre OpenWebUI (el gateway de los LLMs)
- Corre los 9 Discord bots (via Docker)
**¿Por qué no usar Vercel/Netlify para la web y nada más?**
Porque también necesitas hostear OpenWebUI y los 9 bots, que son procesos Docker de larga duración. Vercel solo sirve para webs estáticas. Con un VPS tienes todo en un sitio.
**¿Qué pasaría sin él?**
No tendrías dónde correr los bots ni OpenWebUI.
---
## nginx
**¿Qué es?**
Un servidor web y proxy inverso. Recibe las peticiones HTTP/HTTPS y las dirige al sitio correcto.
**¿Para qué lo usamos?**
Dos funciones en este proyecto:
1. **Servir la web:** cuando alguien abre `carlospalanca.es`, nginx lee los archivos de `/var/www/carlospalanca.es/` y los devuelve.
2. **Proxy a OpenWebUI:** cuando alguien abre `ai.carlospalanca.es`, nginx redirige la petición a `localhost:3000` donde corre OpenWebUI — sin exponer el puerto directamente a internet.
**¿Por qué no exponer OpenWebUI directamente en el puerto 3000?**
Porque nginx gestiona el SSL (HTTPS). Sin nginx, OpenWebUI estaría en `http://` sin cifrado. También permite tener múltiples servicios en el mismo servidor con distintos dominios.
**¿Qué pasaría sin él?**
La web no sería accesible y OpenWebUI no tendría HTTPS.
---
## Let's Encrypt / Certbot
**¿Qué es?**
Un servicio gratuito que emite certificados SSL. Certbot es la herramienta que los instala y renueva automáticamente.
**¿Para qué lo usamos?**
Para que la web use HTTPS (`https://carlospalanca.es`) en lugar de HTTP sin cifrado. Sin certificado, los navegadores muestran "Conexión no segura".
**¿Qué pasaría sin él?**
La web funcionaría solo en HTTP. Nadie confiaría en ella y Google la penalizaría en SEO.
---
## Docker y Docker Compose
**¿Qué es?**
Docker empaqueta aplicaciones en contenedores — entornos aislados con todo lo que necesitan para funcionar. Docker Compose orquesta múltiples contenedores.
**¿Para qué lo usamos?**
- Cada Discord bot es un contenedor Docker separado
- OpenWebUI corre como contenedor Docker
- Esto significa que puedes levantar los 9 bots con un solo comando: `docker compose up -d`
**Ventajas:**
- Si un bot falla, no afecta a los demás
- `restart: unless-stopped` los reinicia automáticamente si crashean
- Fácil de actualizar: `docker compose build && docker compose up -d`
- El entorno es idéntico en tu máquina y en el VPS
**¿Qué pasaría sin él?**
Tendrías que instalar Python, dependencias y gestionar 9 procesos manualmente en el VPS. Docker lo simplifica enormemente.
---
## OpenWebUI
**¿Qué es?**
Una interfaz web open-source para interactuar con modelos de lenguaje (LLMs). Tiene una API compatible con el formato de OpenAI.
**¿Para qué lo usamos?**
Es el **gateway centralizado de LLM**. Los 9 bots no llaman directamente a OpenAI o Anthropic — llaman a OpenWebUI, que a su vez llama al proveedor.
**¿Por qué este intermediario?**
| Sin OpenWebUI | Con OpenWebUI |
|---------------|---------------|
| Cambiar de GPT-4o a Claude requiere editar el código de los 9 bots y redeployar | Cambias `OPENWEBUI_MODEL=claude-3-5-sonnet` en `.env` y reinicias |
| No tienes visibilidad del consumo de tokens por agente | Dashboard web con historial de conversaciones y uso |
| No puedes chatear manualmente con los modelos | Tienes una UI en `ai.carlospalanca.es` para uso personal |
**¿Qué pasaría sin él?**
Seguiría funcionando si llamas directamente a la API de OpenAI, pero perderías la flexibilidad de cambiar de modelo y el dashboard de control.
---
## Discord
**¿Qué es?**
Plataforma de mensajería con soporte para bots programables.
**¿Para qué lo usamos?**
Es la **interfaz de control del sistema**. En lugar de tener una web especial para hablar con los agentes, usas Discord. Cada agente tiene su propio canal.
**¿Por qué Discord y no Telegram, Slack u otro?**
- Permite múltiples bots con identidades visuales separadas (cada bot tiene su nombre y avatar)
- Tyrion aparece como "Tyrion Lannister", Samwell como "Samwell Tarly", etc.
- Los canales organizan las conversaciones por agente de forma natural
- Gratis para este uso
**¿Por qué 9 aplicaciones Discord separadas y no una sola?**
Porque si fuera un solo bot, todos los mensajes aparecerían del mismo bot. Así cada agente tiene su propia identidad visual en Discord, lo que hace mucho más claro quién está haciendo qué.
**¿Qué pasaría sin él?**
Necesitarías otra interfaz para dar instrucciones a los agentes (una web propia, Telegram, etc.).
---
## discord.py
**¿Qué es?**
Librería de Python para crear bots de Discord.
**¿Para qué lo usamos?**
Es el núcleo de cada agente. Gestiona la conexión con Discord, escucha mensajes en el canal asignado y envía respuestas.
**¿Qué pasaría sin él?**
Habría que usar la API de Discord directamente con HTTP puro, mucho más complejo.
---
## PyGithub
**¿Qué es?**
Librería de Python que envuelve la API REST de GitHub.
**¿Para qué lo usamos?**
En `agents/shared/github_client.py` para crear ramas, hacer commits y abrir Pull Requests desde Python con pocas líneas de código.
**¿Qué pasaría sin él?**
Habría que hacer las llamadas HTTP a la API de GitHub manualmente.
---
## Resumen visual
```
TÚ (Carlos)
│ escribes en
Discord ──────────────────────────────────────────────────────┐
│ │
│ discord.py escucha │
▼ │
9 Bots Python (Docker en VPS) │
│ │
│ llaman vía HTTP │
▼ │
OpenWebUI (Docker en VPS, detrás de nginx con HTTPS) │
│ │
│ llama a │
▼ │
OpenAI / Anthropic / Ollama (LLM) │
│ │
│ respuesta │
▼ │
Bot crea PR con PyGithub │
│ │
▼ │
GitHub ◄──────────────────── tú revisas y mergeas ────────────┘
│ merge a main dispara
GitHub Actions
│ build + rsync
VPS (/var/www/carlospalanca.es/) ◄── nginx sirve
https://carlospalanca.es (web pública con Let's Encrypt)
```

View File

@@ -1,48 +0,0 @@
#!/bin/bash
# Script de despliegue manual para el VPS
# Ejecutar como: bash vps/deploy.sh
set -euo pipefail
echo "=== Despliegue manual de carlospalanca.es ==="
# 1. Instalar dependencias
echo "[1/4] Instalando dependencias..."
sudo apt-get update -q
sudo apt-get install -y -q nginx certbot python3-certbot-nginx docker.io docker-compose-plugin
# 2. Crear usuario deploy (si no existe)
if ! id "deploy" &>/dev/null; then
echo "[2/4] Creando usuario deploy..."
sudo useradd -m -s /bin/bash deploy
sudo mkdir -p /home/deploy/.ssh
echo "IMPORTANTE: Añade tu clave pública a /home/deploy/.ssh/authorized_keys"
else
echo "[2/4] Usuario deploy ya existe."
fi
# 3. Crear directorio web
echo "[3/4] Creando directorio web..."
sudo mkdir -p /var/www/carlospalanca.es
sudo chown deploy:deploy /var/www/carlospalanca.es
# 4. Configurar nginx
echo "[4/4] Configurando nginx..."
sudo cp vps/nginx/carlospalanca.conf /etc/nginx/sites-available/carlospalanca.es
sudo ln -sf /etc/nginx/sites-available/carlospalanca.es /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl enable nginx
sudo systemctl reload nginx
echo ""
echo "=== Setup completado ==="
echo "Próximos pasos:"
echo "1. Apunta los DNS de carlospalanca.es y ai.carlospalanca.es a esta IP"
echo "2. Ejecuta: sudo certbot --nginx -d carlospalanca.es -d www.carlospalanca.es -d ai.carlospalanca.es"
echo "3. Copia vps/docker-compose.openwebui.yml a /opt/openwebui/"
echo "4. Crea /opt/openwebui/.env con tus API keys"
echo "5. Ejecuta: cd /opt/openwebui && docker compose -f docker-compose.openwebui.yml up -d"
echo "6. Añade los GitHub Secrets en el repositorio:"
echo " - VPS_SSH_PRIVATE_KEY"
echo " - VPS_HOST (IP del VPS)"
echo " - VPS_USER (deploy)"

View File

@@ -1,40 +0,0 @@
services:
openwebui:
image: ghcr.io/open-webui/open-webui:main
container_name: openwebui
restart: unless-stopped
ports:
- "127.0.0.1:3000:8080" # Solo localhost — nginx proxifica con TLS
volumes:
- openwebui_data:/app/backend/data
environment:
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
- WEBUI_AUTH=true
- DEFAULT_MODELS=${DEFAULT_MODEL:-gpt-4o}
# Usa solo lo que necesites:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
# Descomenta si usas Ollama para modelos locales:
# - OLLAMA_BASE_URL=http://ollama:11434
# depends_on:
# - ollama
# Descomenta si quieres modelos locales con Ollama
# ollama:
# image: ollama/ollama:latest
# container_name: ollama
# restart: unless-stopped
# volumes:
# - ollama_data:/root/.ollama
# # GPU (requiere nvidia-container-toolkit):
# # deploy:
# # resources:
# # reservations:
# # devices:
# # - driver: nvidia
# # count: all
# # capabilities: [gpu]
volumes:
openwebui_data:
# ollama_data:

View File

@@ -1,66 +0,0 @@
# Redirect HTTP to HTTPS
server {
listen 80;
server_name carlospalanca.es www.carlospalanca.es;
return 301 https://carlospalanca.es$request_uri;
}
# Website
server {
listen 443 ssl http2;
server_name carlospalanca.es www.carlospalanca.es;
ssl_certificate /etc/letsencrypt/live/carlospalanca.es/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/carlospalanca.es/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/carlospalanca.es;
index index.html;
location / {
try_files $uri $uri/ $uri.html =404;
}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# OpenWebUI
server {
listen 80;
server_name ai.carlospalanca.es;
return 301 https://ai.carlospalanca.es$request_uri;
}
server {
listen 443 ssl http2;
server_name ai.carlospalanca.es;
ssl_certificate /etc/letsencrypt/live/ai.carlospalanca.es/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ai.carlospalanca.es/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}