feat: Claude Haiku for content generation, Ollama fallback
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
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>
This commit is contained in:
+88
-63
@@ -1,9 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
@@ -42,95 +43,95 @@ THREAD_SYSTEM = (
|
|||||||
|
|
||||||
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]
|
[Contexto — qué, 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 DISTINTO — sin 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 concise — no filler
|
- Sé preciso y conciso — sin 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):"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -170,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)
|
||||||
@@ -191,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user