feat: Ghost EN — /generate blog en publica en inglés en theexclusionzone.com
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m19s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m19s
This commit is contained in:
+6
-3
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user