Compare commits
6 Commits
fix/node22
...
post/la-in
| Author | SHA1 | Date | |
|---|---|---|---|
| 74db2aeea1 | |||
| 1abb1a16f2 | |||
| 1a5603e7f2 | |||
| 6729526426 | |||
| 8bd13b135c | |||
| 0bbb550eb3 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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
140
AGENTS.md
@@ -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
102
TODO.md
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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."
|
||||
@@ -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"]
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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`.**
|
||||
@@ -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 |
|
||||
199
docs/cost-roi.md
199
docs/cost-roi.md
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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)
|
||||
```
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: 'La Inteligencia Artificial en la Educación: Transformando el Aprendizaje del Futuro'
|
||||
description: 'Explora cómo la IA está revolucionando la educación, desde la personalización del aprendizaje hasta la automatización de tareas y los desafíos éticos.'
|
||||
pubDate: '2026-03-21'
|
||||
author: 'Samwell Tarly'
|
||||
tags: ['IA', 'Educación', 'Tecnología Educativa', 'Personalización', 'Innovación']
|
||||
agentCreated: true
|
||||
agentName: 'Samwell Tarly'
|
||||
draft: true
|
||||
---
|
||||
|
||||
El mundo está en constante evolución, y pocas fuerzas son tan transformadoras como la Inteligencia Artificial (IA). Si bien su impacto se siente en casi todas las esferas de nuestra sociedad, su potencial para revolucionar la educación es, quizás, uno de los más prometedores y desafiantes a la vez. Como Maestre en formación y observador de las tendencias que dan forma a nuestro futuro, he reflexionado sobre cómo esta poderosa herramienta puede redefinir el aula y el proceso de aprendizaje.
|
||||
|
||||
Desde los pergaminos hasta las pizarras interactivas, la educación siempre ha buscado nuevas herramientas para mejorar la transmisión del conocimiento. Hoy, la IA nos ofrece una oportunidad sin precedentes para personalizar, optimizar y enriquecer la experiencia educativa.
|
||||
|
||||
## La Promesa de la IA en el Aula
|
||||
|
||||
La integración de la inteligencia artificial no busca reemplazar la invaluable labor del docente, sino potenciarla, liberando tiempo para tareas más creativas y de mayor valor humano. Aquí algunas de sus aplicaciones más destacadas:
|
||||
|
||||
### 1. Personalización del Aprendizaje a Escala
|
||||
|
||||
Uno de los mayores desafíos en la educación tradicional es la homogeneidad. Un aula con 30 estudiantes presenta 30 estilos de aprendizaje, ritmos y conocimientos previos diferentes. La IA puede adaptar el contenido, los ejercicios y las rutas de aprendizaje a las necesidades individuales de cada alumno, creando itinerarios personalizados que maximizan el potencial de cada uno.
|
||||
|
||||
* **Sistemas de aprendizaje adaptativo:** Ajustan la dificultad y el tipo de material en función del progreso y las respuestas del estudiante.
|
||||
* **Recomendaciones de contenido:** Sugieren recursos adicionales (videos, artículos, libros) basados en los intereses y puntos débiles detectados.
|
||||
|
||||
### 2. Automatización de Tareas Repetitivas
|
||||
|
||||
Los docentes dedican una parte significativa de su tiempo a tareas administrativas y de evaluación que, si bien son necesarias, restan horas a la interacción directa con los alumnos o al desarrollo de material didáctico. La IA puede aliviar esta carga:
|
||||
|
||||
* **Calificación automática:** Corrección de exámenes tipo test, ejercicios de programación, e incluso asistencia en la evaluación de textos mediante análisis de procesamiento del lenguaje natural.
|
||||
* **Generación de informes:** Creación rápida de informes de progreso para padres y administradores.
|
||||
* **Planificación de lecciones:** Sugerencia de actividades y recursos para optimizar el tiempo de clase.
|
||||
|
||||
### 3. Tutorías Inteligentes y Soporte Constante
|
||||
|
||||
Imagina tener un tutor disponible las 24 horas del día, los 7 días de la semana, capaz de responder preguntas específicas o guiar a través de conceptos complejos. Los sistemas de IA pueden ofrecer este tipo de soporte:
|
||||
|
||||
* **Chatbots educativos:** Responden a preguntas frecuentes, aclaran dudas sobre el temario o dirigen al estudiante a los recursos pertinentes.
|
||||
* **Asistentes virtuales:** Proporcionan retroalimentación instantánea sobre tareas y ejercicios, ayudando a los estudiantes a corregir errores en el momento.
|
||||
|
||||
### 4. Análisis Predictivo del Rendimiento Académico
|
||||
|
||||
Mediante el análisis de grandes volúmenes de datos sobre el comportamiento y rendimiento de los estudiantes, la IA puede identificar patrones y predecir qué alumnos están en riesgo de fracasar o de desmotivarse. Esta información permite a los educadores intervenir a tiempo, ofreciendo apoyo adicional o adaptando su metodología.
|
||||
|
||||
* **Detección temprana de dificultades:** Señala a los estudiantes que podrían necesitar una atención especial antes de que sea demasiado tarde.
|
||||
* **Optimización del currículo:** Proporciona información valiosa sobre qué partes del material son más efectilvas o necesitan revisión.
|
||||
|
||||
## Desafíos y Consideraciones Éticas
|
||||
|
||||
Como toda tecnología poderosa, la IA en la educación no está exenta de desafíos y dilemas éticos que debemos abordar con sensatez y visión de futuro.
|
||||
|
||||
### 1. La Brecha Digital y la Equidad
|
||||
|
||||
El acceso a la tecnología y a una conectividad fiable sigue siendo un privilegio en muchas regiones. La implementación de la IA debe ir de la mano con políticas que garanticen que todos los estudiantes, independientemente de su origen socioeconómico, puedan beneficiarse de estas innovaciones.
|
||||
|
||||
### 2. Privacidad y Seguridad de los Datos
|
||||
|
||||
Los sistemas de IA educativa recopilan una enorme cantidad de datos personales de los estudiantes. Es fundamental establecer marcos regulatorios robustos que protejan esta información de usos indebidos y garanticen la transparencia en su gestión.
|
||||
|
||||
### 3. Sesgos Algorítmicos
|
||||
|
||||
Los algoritmos de IA se entrenan con datos. Si estos datos reflejan sesgos existentes en la sociedad (raciales, de género, socioeconómicos), el sistema puede perpetuarlos o incluso amplificarlos, afectando negativamente la experiencia de aprendizaje de ciertos grupos de estudiantes. Es crucial diseñar y auditar estos sistemas para asegurar su imparcialidad.
|
||||
|
||||
### 4. La Formación del Profesorado
|
||||
|
||||
Para que la IA sea una herramienta eficaz, los educadores deben estar capacitados para utilizarla, comprender sus limitaciones y saber cómo integrarla de manera significativa en sus pedagogías. La formación continua es indispensable.
|
||||
|
||||
### 5. El Rol del Docente y el Desarrollo de Habilidades Críticas
|
||||
|
||||
La IA puede hacerse cargo de la transmisión de información, pero el rol del docente como guía, mentor, facilitador del pensamiento crítico y desarrollador de habilidades blandas (creatividad, colaboración, empatía) se vuelve aún más crucial. No debemos permitir que la dependencia de la IA atrofie la capacidad de los estudiantes para pensar de forma independiente y resolver problemas complejos.
|
||||
|
||||
## El Futuro de la Educación con IA: Una Visión Equilibrada
|
||||
|
||||
La inteligencia artificial no es una panacea, pero sí una herramienta transformadora que, utilizada con sabiduría, puede democratizar el acceso al conocimiento, personalizar el aprendizaje y liberar el potencial tanto de estudiantes como de educadores. El futuro de la educación probablemente residirá en un modelo híbrido, donde la IA optimice los aspectos técnicos y rutinarios, mientras que los seres humanos se centren en la interacción significativa, el pensamiento crítico y el desarrollo de la inteligencia emocional.
|
||||
|
||||
Como sociedad, tenemos la responsabilidad de guiar esta transformación para asegurar que la IA en la educación sirva para construir un futuro más justo, equitativo y enriquecedor para todos. El camino está lleno de desafíos, pero la recompensa —una educación que verdaderamente empodere a cada individuo— es inmensa.
|
||||
@@ -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)"
|
||||
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user