feat: Ghost EN — /generate blog en publica en inglés en theexclusionzone.com
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m19s

This commit is contained in:
ChemaVX
2026-05-18 16:49:09 +00:00
parent 747b9605c0
commit f577ac4712
3 changed files with 69 additions and 13 deletions
+6 -3
View File
@@ -277,6 +277,7 @@ async def cmd_generate(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id chat_id = update.effective_chat.id
output_arg = ctx.args[0].lower() if ctx.args else "" output_arg = ctx.args[0].lower() if ctx.args else ""
lang = "en" if len(ctx.args) > 1 and ctx.args[1].lower() == "en" else "es"
type_map = { type_map = {
"podcast": OutputType.PODCAST, "podcast": OutputType.PODCAST,
@@ -326,9 +327,11 @@ async def cmd_generate(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
session = dict(row) session = dict(row)
session_id = session["id"] session_id = session["id"]
backend = "Claude Haiku" if settings.anthropic_api_key else f"Ollama ({settings.ollama_model})"
lang_label = " (EN)" if lang == "en" else ""
msg = await update.message.reply_text( msg = await update.message.reply_text(
f"⚙️ Generating *{output_type}* for: `{session['topic']}`\n" f"⚙️ Generating *{output_type}{lang_label}* for: `{session['topic']}`\n"
f"Using Ollama ({settings.ollama_model})...\n" f"Using {backend}...\n"
f"This may take 2-5 minutes ☕", f"This may take 2-5 minutes ☕",
parse_mode=ParseMode.MARKDOWN parse_mode=ParseMode.MARKDOWN
) )
@@ -343,7 +346,7 @@ async def cmd_generate(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
processor = ContentProcessor(db, ollama) processor = ContentProcessor(db, ollama)
generator = OutputGenerator(db, ollama, processor) generator = OutputGenerator(db, ollama, processor)
output = await generator.generate(session_id, output_type, gen_progress) output = await generator.generate(session_id, output_type, gen_progress, lang=lang)
# Send as file if very long # Send as file if very long
if len(output) > 8000: if len(output) > 8000:
+2
View File
@@ -36,6 +36,8 @@ class Settings(BaseSettings):
# Ghost CMS # Ghost CMS
ghost_url: Optional[str] = Field(None, env="GHOST_URL") ghost_url: Optional[str] = Field(None, env="GHOST_URL")
ghost_api_key: Optional[str] = Field(None, env="GHOST_API_KEY") ghost_api_key: Optional[str] = Field(None, env="GHOST_API_KEY")
ghost_url_en: str = Field("", env="GHOST_URL_EN")
ghost_api_key_en: str = Field("", env="GHOST_API_KEY_EN")
# Alerts # Alerts
cost_alert_threshold: float = Field(0.15, env="COST_ALERT_THRESHOLD") cost_alert_threshold: float = Field(0.15, env="COST_ALERT_THRESHOLD")
+61 -10
View File
@@ -33,6 +33,45 @@ BLOG_SYSTEM = (
"Cada sección debe añadir información nueva no cubierta en secciones anteriores." "Cada sección debe añadir información nueva no cubierta en secciones anteriores."
) )
BLOG_SYSTEM_EN = (
"You write ALWAYS in English. "
"You are a journalist writing a blog article. Use clear markdown headers. "
"NEVER repeat the same fact or phrase twice — if you said it, move on. "
"Each section must add new information not covered in other sections."
)
BLOG_PROMPT_EN = """\
Write a blog article about: "{topic}"
RULES follow strictly:
- Each section under a heading must add NEW information not covered elsewhere
- Do NOT summarize previous sections at the start of each new section
- Do NOT repeat facts if a fact appears once, do not mention it again
- Use concrete details, numbers, names avoid vague generalities
- Target: 1000-1500 words
STRUCTURE:
# [Impactful headline]
[Hook paragraph the most surprising fact]
## Background
[Context what, when, who only facts not covered elsewhere]
## Key Facts
[Most significant findings each point must be distinct]
## Analysis / Significance
[What this means without repeating the Key Facts section]
## Conclusion
[No more than 2 sentences summarizing, then a forward-looking statement]
RESEARCH MATERIAL:
{context}
Write the complete article in markdown:"""
REPORT_SYSTEM = ( REPORT_SYSTEM = (
"Escribe SIEMPRE en español. " "Escribe SIEMPRE en español. "
"Eres un analista de investigación. Escribe un informe estructurado y factual. " "Eres un analista de investigación. Escribe un informe estructurado y factual. "
@@ -239,9 +278,13 @@ def _strip_researchowl_header(content: str) -> str:
class GhostPublisher: class GhostPublisher:
def __init__(self): def __init__(self, lang: str = "es"):
self.url = (settings.ghost_url or "").rstrip("/") if lang == "en":
self.api_key = settings.ghost_api_key or "" self.url = (settings.ghost_url_en or "").rstrip("/")
self.api_key = settings.ghost_api_key_en or ""
else:
self.url = (settings.ghost_url or "").rstrip("/")
self.api_key = settings.ghost_api_key or ""
def is_configured(self) -> bool: def is_configured(self) -> bool:
return bool(self.url and self.api_key) return bool(self.url and self.api_key)
@@ -318,12 +361,13 @@ class OutputGenerator:
self.processor = processor self.processor = processor
async def generate(self, session_id: int, output_type: OutputType, async def generate(self, session_id: int, output_type: OutputType,
progress_callback=None) -> str: progress_callback=None, lang: str = "es") -> str:
"""Generate an output for a research session""" """Generate an output for a research session"""
if output_type in (OutputType.REPORT_EXTENDED, if output_type in (OutputType.REPORT_EXTENDED,
OutputType.BLOG_EXTENDED, OutputType.BLOG_EXTENDED,
OutputType.PODCAST_EXTENDED): OutputType.PODCAST_EXTENDED):
return await self.generate_extended(session_id, output_type, progress_callback) return await self.generate_extended(session_id, output_type, progress_callback,
lang=lang)
session = await self.db.get_session(session_id) session = await self.db.get_session(session_id)
if not session: if not session:
@@ -355,6 +399,10 @@ class OutputGenerator:
system = self._get_system(output_type) system = self._get_system(output_type)
prompt = PROMPTS[output_type].format(topic=topic, context=context) prompt = PROMPTS[output_type].format(topic=topic, context=context)
if lang == "en" and output_type == OutputType.BLOG:
system = BLOG_SYSTEM_EN
prompt = BLOG_PROMPT_EN.format(topic=topic, context=context)
output = await self._generate(prompt, system, output_type, session_id) output = await self._generate(prompt, system, output_type, session_id)
# Add metadata header # Add metadata header
@@ -368,7 +416,7 @@ class OutputGenerator:
# Auto-publish to Ghost for blog outputs # Auto-publish to Ghost for blog outputs
ghost_notice = "" ghost_notice = ""
if output_type in (OutputType.BLOG, OutputType.BLOG_EXTENDED): if output_type in (OutputType.BLOG, OutputType.BLOG_EXTENDED):
ghost = GhostPublisher() ghost = GhostPublisher(lang=lang)
if ghost.is_configured(): if ghost.is_configured():
try: try:
title = _extract_title(full_output) or topic title = _extract_title(full_output) or topic
@@ -439,7 +487,7 @@ class OutputGenerator:
return systems.get(output_type, "You are a helpful research assistant.") return systems.get(output_type, "You are a helpful research assistant.")
async def generate_extended(self, session_id: int, output_type: OutputType, async def generate_extended(self, session_id: int, output_type: OutputType,
progress_callback=None) -> str: progress_callback=None, lang: str = "es") -> str:
""" """
Generación por secciones para outputs exhaustivos. Generación por secciones para outputs exhaustivos.
1. Recupera muestra de contexto para el outline 1. Recupera muestra de contexto para el outline
@@ -496,6 +544,8 @@ class OutputGenerator:
# Paso 3: generar cada sección # Paso 3: generar cada sección
base_output_type = OutputType(base_type) base_output_type = OutputType(base_type)
system = self._get_system(base_output_type) system = self._get_system(base_output_type)
if lang == "en" and output_type == OutputType.BLOG_EXTENDED:
system = BLOG_SYSTEM_EN
sections_text = [] sections_text = []
for i, section in enumerate(sections, 1): for i, section in enumerate(sections, 1):
@@ -512,6 +562,7 @@ class OutputGenerator:
if not section_context: if not section_context:
section_context = context_summary section_context = context_summary
lang_rule = "- Write in English\n" if lang == "en" else "- Escribe en español\n"
section_prompt = ( section_prompt = (
f"Escribe la sección '{title}' del {base_type} sobre: '{topic}'\n\n" f"Escribe la sección '{title}' del {base_type} sobre: '{topic}'\n\n"
f"REGLAS:\n" f"REGLAS:\n"
@@ -520,8 +571,8 @@ class OutputGenerator:
f"- No incluyas encabezados del documento completo, solo el contenido de esta sección\n" f"- No incluyas encabezados del documento completo, solo el contenido de esta sección\n"
f"- Objetivo: aproximadamente {target_words} palabras\n" f"- Objetivo: aproximadamente {target_words} palabras\n"
f"- Usa SOLO información del material siguiente — no inventes datos\n" f"- Usa SOLO información del material siguiente — no inventes datos\n"
f"- Escribe en español\n\n" f"{lang_rule}"
f"MATERIAL:\n{section_context}" f"\nMATERIAL:\n{section_context}"
) )
section_text = await self._generate( section_text = await self._generate(
@@ -540,7 +591,7 @@ class OutputGenerator:
# Auto-publish to Ghost for extended blog outputs # Auto-publish to Ghost for extended blog outputs
ghost_notice = "" ghost_notice = ""
if output_type == OutputType.BLOG_EXTENDED: if output_type == OutputType.BLOG_EXTENDED:
ghost = GhostPublisher() ghost = GhostPublisher(lang=lang)
if ghost.is_configured(): if ghost.is_configured():
try: try:
title = _extract_title(full_output) or topic title = _extract_title(full_output) or topic