feat: initial project setup
This commit is contained in:
15
agents/varys/Dockerfile
Normal file
15
agents/varys/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
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"]
|
||||
96
agents/varys/main.py
Normal file
96
agents/varys/main.py
Normal file
@@ -0,0 +1,96 @@
|
||||
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"])
|
||||
45
agents/varys/prompt.txt
Normal file
45
agents/varys/prompt.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
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
|
||||
Reference in New Issue
Block a user