2 Commits

Author SHA1 Message Date
ChemaVX 65b1739943 feat: Claude Haiku for content generation, Ollama fallback
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Use Claude Haiku (via ANTHROPIC_API_KEY) for all output generation.
Falls back to Ollama qwen2.5:3b if no API key is set.
Also translates all user-turn prompts to Spanish for consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 09:06:06 +00:00
ChemaVX 54b3841d32 feat: generate all outputs in Spanish
Add "Escribe SIEMPRE en español" at the start of all system prompts
(podcast, blog, report, thread) so Ollama generates content in Spanish.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:40:38 +00:00
+106 -77
View File
@@ -1,132 +1,137 @@
""" """
ResearchOwl Generators ResearchOwl Generators
Produces structured outputs from processed research using Ollama Produces structured outputs from processed research using Claude or Ollama
""" """
import structlog import structlog
from src.config import settings
from src.processor.processor import OllamaClient, ContentProcessor from src.processor.processor import OllamaClient, ContentProcessor
from src.db.database import ResearchDB, OutputType from src.db.database import ResearchDB, OutputType
logger = structlog.get_logger() logger = structlog.get_logger()
PODCAST_SYSTEM = ( PODCAST_SYSTEM = (
"You are a podcast scriptwriter. Write exactly as a host SPEAKS — contractions, " "Escribe SIEMPRE en español. "
"incomplete sentences, natural pauses, rhetorical questions. " "Eres un guionista de podcast. Escribe exactamente como un presentador HABLA — contracciones, "
"NEVER repeat a sentence, phrase, or idea you already wrote. " "frases naturales, pausas, preguntas retóricas. "
"Each paragraph must introduce NEW information. " "NUNCA repitas una frase o idea que ya escribiste. "
"Use [PAUSE], [EMPHASIS], [MUSIC CUE] markers sparingly." "Cada párrafo debe introducir información NUEVA. "
"Usa marcadores [PAUSA], [ÉNFASIS], [MÚSICA] con moderación."
) )
BLOG_SYSTEM = ( BLOG_SYSTEM = (
"You are a journalist writing a blog post. Use clear markdown headings. " "Escribe SIEMPRE en español. "
"NEVER repeat the same fact or phrase twice — if you said something, move on. " "Eres un periodista escribiendo un artículo de blog. Usa encabezados markdown claros. "
"Each section must add new information not covered in previous sections." "NUNCA repitas el mismo dato o frase dos veces — si ya lo dijiste, avanza. "
"Cada sección debe añadir información nueva no cubierta en secciones anteriores."
) )
REPORT_SYSTEM = ( REPORT_SYSTEM = (
"You are a research analyst. Write a structured factual report. " "Escribe SIEMPRE en español. "
"Be concise — do NOT pad with redundant summaries. " "Eres un analista de investigación. Escribe un informe estructurado y factual. "
"NEVER restate a finding already listed. Each numbered finding must be distinct." "Sé conciso — NO rellenes con resúmenes redundantes. "
"NUNCA repitas un hallazgo ya listado. Cada hallazgo numerado debe ser distinto."
) )
THREAD_SYSTEM = ( THREAD_SYSTEM = (
"You write Twitter/X threads. Each tweet must be under 280 chars. " "Escribe SIEMPRE en español. "
"NEVER repeat information from a previous tweet. " "Escribes hilos de Twitter/X. Cada tweet debe tener menos de 280 caracteres. "
"Each tweet must reveal something NEW. Number them 1/N, 2/N..." "NUNCA repitas información de un tweet anterior. "
"Cada tweet debe revelar algo NUEVO. Numéralos 1/N, 2/N..."
) )
PROMPTS = { PROMPTS = {
OutputType.PODCAST: """\ OutputType.PODCAST: """\
Write a podcast script about: "{topic}" Escribe un guion de podcast sobre: "{topic}"
RULES — follow strictly: REGLAS — sigue estrictamente:
- Write as SPOKEN WORD: contractions, natural rhythm, as if talking to a friend - Escribe como PALABRA HABLADA: contracciones, ritmo natural, como si hablaras con un amigo
- DO NOT use formal headings like "SEGMENT 1:"just flow naturally - NO uses encabezados formales como "SEGMENTO 1:"fluye de forma natural
- Each paragraph must introduce a NEW fact or angle — never restate something already said - Cada párrafo debe introducir un NUEVO hecho o ángulo — nunca repitas algo ya dicho
- If you find yourself repeating, stop and jump to the next new point - Si te encuentras repitiendo, para y salta al siguiente punto nuevo
- Aim for 800-1200 words of actual spoken content - Objetivo: 800-1200 palabras de contenido hablado real
STRUCTURE (use natural transitions, not headers): ESTRUCTURA (usa transiciones naturales, no encabezados):
1. Hook: open with the most surprising or dramatic fact 1. Gancho: abre con el hecho más sorprendente o dramático
2. Background: how did we get here? 2. Contexto: ¿cómo llegamos aquí?
3. The key evidence or events (pick the 3 most interesting) 3. Las evidencias o eventos clave (elige los 3 más interesantes)
4. Controversy or debate around the topic 4. La controversia o debate sobre el tema
5. What does this mean / what happened next 5. ¿Qué significa esto / qué pasó después?
RESEARCH MATERIAL: MATERIAL DE INVESTIGACIÓN:
{context} {context}
Write the script now (spoken word only, no stage directions except occasional [PAUSE]):""", Escribe el guion ahora (solo palabra hablada, sin acotaciones excepto [PAUSA] ocasional):""",
OutputType.BLOG: """\ OutputType.BLOG: """\
Write a blog post about: "{topic}" Escribe un artículo de blog sobre: "{topic}"
RULES — follow strictly: REGLAS — sigue estrictamente:
- Each section under a heading must add NEW information not covered elsewhere - Cada sección bajo un encabezado debe añadir información NUEVA no cubierta en otro lugar
- Do NOT summarize previous sections at the start of each new section - NO resumas secciones anteriores al inicio de cada nueva sección
- Do NOT repeat facts — if a fact appears once, do not mention it again - NO repitas hechos — si un hecho aparece una vez, no lo menciones de nuevo
- Use concrete details, numbers, names — avoid vague generalities - Usa detalles concretos, números, nombres — evita generalidades vagas
- Target 1000-1500 words - Objetivo: 1000-1500 palabras
STRUCTURE: ESTRUCTURA:
# [Compelling headline] # [Titular impactante]
[Hook paragraph — the most surprising fact] [Párrafo gancho — el hecho más sorprendente]
## Background ## Contexto
[Context — what, when, who — only facts not covered elsewhere] [Contextoqué, cuándo, quién — solo hechos no cubiertos en otro lugar]
## Key Facts ## Hechos Clave
[The most significant findings — each bullet must be distinct] [Los hallazgos más significativos — cada punto debe ser distinto]
## Analysis / Significance ## Análisis / Importancia
[What this means — no repetition of Key Facts section] [Qué significa esto — sin repetir la sección de Hechos Clave]
## Conclusion ## Conclusión
[Takeaway — no more than 2 sentences summarizing, then a forward-looking statement] [Conclusión — no más de 2 oraciones resumiendo, luego una declaración prospectiva]
RESEARCH MATERIAL: MATERIAL DE INVESTIGACIÓN:
{context} {context}
Write the complete blog post in markdown:""", Escribe el artículo completo en markdown:""",
OutputType.REPORT: """\ OutputType.REPORT: """\
Write a research report about: "{topic}" Escribe un informe de investigación sobre: "{topic}"
RULES — follow strictly: REGLAS — sigue estrictamente:
- Each numbered finding must be DISTINCT — no overlapping content - Cada hallazgo numerado debe ser DISTINTOsin contenido que se superponga
- The Executive Summary must NOT repeat findings verbatim — only the 2-3 most critical points - El Resumen Ejecutivo NO debe repetir los hallazgos literalmente — solo los 2-3 puntos más críticos
- Source quality and contradictions must reference specific claims, not generic statements - La calidad de las fuentes y contradicciones deben referenciar afirmaciones específicas, no declaraciones genéricas
- Be precise and conciseno filler - preciso y concisosin relleno
STRUCTURE: ESTRUCTURA:
1. Executive Summary (3-4 sentences, key takeaways only) 1. Resumen Ejecutivo (3-4 oraciones, solo puntos clave)
2. Key Findings (5-10 numbered, each completely distinct) 2. Hallazgos Clave (5-10 numerados, cada uno completamente distinto)
3. Evidence Analysis (what the sources show, with any contradictions) 3. Análisis de Evidencia (lo que muestran las fuentes, con cualquier contradicción)
4. Timeline (if applicable — specific dates/events) 4. Cronología (si aplica — fechas/eventos específicos)
5. Conclusions & Open Questions 5. Conclusiones y Preguntas Abiertas
RESEARCH MATERIAL: MATERIAL DE INVESTIGACIÓN:
{context} {context}
Write the complete report in markdown:""", Escribe el informe completo en markdown:""",
OutputType.THREAD: """\ OutputType.THREAD: """\
Write a Twitter/X thread about: "{topic}" Escribe un hilo de Twitter/X sobre: "{topic}"
RULES — follow strictly: REGLAS — sigue estrictamente:
- Each tweet must reveal ONE new fact or idea — never restate a previous tweet - Cada tweet debe revelar UN nuevo hecho o idea — nunca repetir un tweet anterior
- Max 280 characters per tweet (count carefully) - Máximo 280 caracteres por tweet (cuenta cuidadosamente)
- Number format: 1/ 2/ 3/ ... N/ - Formato de numeración: 1/ 2/ 3/ ... N/
- Hook tweet must be the most surprising/provocative fact - El tweet gancho debe ser el hecho más sorprendente/provocador
- Build toward a conclusion — do not repeat the hook at the end - Avanza hacia una conclusión — no repitas el gancho al final
- 12-18 tweets total - 12-18 tweets en total
RESEARCH MATERIAL: MATERIAL DE INVESTIGACIÓN:
{context} {context}
Write the thread (one tweet per line, nothing else):""" Escribe el hilo (un tweet por línea, nada más):"""
} }
@@ -166,15 +171,15 @@ class OutputGenerator:
if len(context_words) > 6000: if len(context_words) > 6000:
context = " ".join(context_words[:6000]) + "\n\n[... additional material truncated ...]" context = " ".join(context_words[:6000]) + "\n\n[... additional material truncated ...]"
backend = "Claude Haiku" if settings.anthropic_api_key else "Ollama"
if progress_callback: if progress_callback:
await progress_callback(f"✍️ Generating {output_type} with Ollama... (this takes 2-5 min)") await progress_callback(f"✍️ Generando {output_type} con {backend}... (2-5 min)")
# Build prompt # Build prompt
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)
# Generate — temperature=0.7 reduces repetition in small models output = await self._generate(prompt, system, output_type)
output = await self.ollama.generate(prompt, system=system, timeout=300, temperature=0.7)
# Add metadata header # Add metadata header
stats = await self.db.get_session_stats(session_id) stats = await self.db.get_session_stats(session_id)
@@ -187,6 +192,30 @@ class OutputGenerator:
logger.info("Output generated", type=output_type, length=len(full_output)) logger.info("Output generated", type=output_type, length=len(full_output))
return full_output return full_output
async def _generate(self, prompt: str, system: str, output_type: OutputType) -> str:
if settings.anthropic_api_key:
return await self._generate_with_claude(prompt, system, output_type)
return await self._generate_with_ollama(prompt, system)
async def _generate_with_claude(self, prompt: str, system: str, output_type: OutputType) -> str:
import anthropic
max_tokens = 4096 if output_type == OutputType.THREAD else 8192
try:
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
msg = await client.messages.create(
model=settings.claude_model,
max_tokens=max_tokens,
system=system,
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text.strip()
except Exception as e:
logger.warning("Claude generation failed, falling back to Ollama", error=str(e))
return await self._generate_with_ollama(prompt, system)
async def _generate_with_ollama(self, prompt: str, system: str) -> str:
return await self.ollama.generate(prompt, system=system, timeout=300, temperature=0.7)
def _get_rag_query(self, output_type: OutputType, topic: str) -> str: def _get_rag_query(self, output_type: OutputType, topic: str) -> str:
queries = { queries = {
OutputType.PODCAST: f"{topic} story narrative facts interesting", OutputType.PODCAST: f"{topic} story narrative facts interesting",