Curador automatizado de pauta periodística para La Chispa Sur, medio digital independiente de izquierda, crítico del modelo neoliberal.
El agente recolecta noticias desde canales RSS y scraping web, las filtra por una ventana temporal de 7 días (168 horas), enriquece los resúmenes cortos extrayendo el contenido completo de los artículos con trafilatura, y utiliza el modelo DeepSeek-V4-Pro (vía API compatible con OpenAI SDK) para sintetizar tres propuestas de pauta editorial semanal con profundidad analítica, tono incisivo y narrativa ágil. También permite escribir artículos completos (~1000 palabras) a partir de cualquiera de las propuestas generadas, con un sistema de referencias deterministas a fuentes que asegura que el redactor reciba el contenido completo de los artículos fuente correctos para cada propuesta.
Flujo automatizado que:
- Ingesta de noticias — Obtiene artículos desde múltiples fuentes configuradas en
rss_feeds.json:- RSS: Usa
feedparserpara canales RSS/Atom tradicionales. - Scraping web: Usa
requests+BeautifulSoupcon selectores CSS configurables por sitio, para medios sin feed RSS. - Tolerancia a fallos individuales: si un feed o scraping falla, continúa con el siguiente.
- RSS: Usa
- Enriquecimiento de contenido — Cuando el resumen RSS de un artículo es demasiado corto para un análisis de calidad (< 150 caracteres), el agente extrae el texto completo del artículo desde su URL usando
trafilatura. Procesamiento paralelo con rate limiting por dominio y caché en archivo JSON para no re-descargar URLs ya visitadas. El enriquecimiento es best-effort: si falla, se conserva el resumen RSS original sin interrumpir el pipeline. - Filtrado temporal — Descarta noticias con más de 7 días (168 horas) de antigüedad, cubriendo la ventana semanal de lunes a domingo. Configurable.
- Limpieza HTML — Elimina etiquetas, comentarios, scripts y decodifica entidades HTML de los resúmenes.
- Truncado inteligente — Recorta resúmenes a 700 caracteres sin cortar palabras a la mitad.
- Guardado intermedio de depuración (opcional) — Con el flag
--debug, guarda un archivo JSON con los artículos procesados (resumen RSS original, contenido extraído y resumen final) para comparar y ajustar el prompt. - Análisis con IA — Envía los artículos filtrados a DeepSeek-V4-Pro con un system prompt que define la identidad editorial de La Chispa Sur (izquierda independiente, rigor periodístico, enfoque chileno, foco territorial en Villarrica y La Araucanía).
- Reporte Markdown — Genera un archivo
pauta_semanal_AAAA_MM_DD.mdcon tres propuestas estructuradas: título gancho, enfoque editorial, puntos clave a desarrollar y fuentes sugeridas para ampliar. La cabecera (fecha y cantidad de notas) se genera automáticamente desde el código para garantizar precisión. - Companion JSON de fuentes — Extrae las fuentes sugeridas desde el texto de la pauta, las empareja determinísticamente con los artículos del pipeline por nombre de medio y similitud temática (keywords), enriquece los artículos emparejados con contenido completo y guarda un archivo
pauta_semanal_AAAA_MM_DD_companion.json. Este archivo permite que el redactor de artículos reciba el contenido completo de las fuentes correctas.
Toma una propuesta específica de la pauta (1, 2 o 3) y la expande a un artículo de ~1000 palabras con material de origen real y verificado:
- Parseo de la pauta — Extrae título, enfoque editorial, puntos clave y fuentes sugeridas desde el archivo markdown generado.
- Emparejamiento determinista de fuentes — En lugar de depender de números de artículo auto-reportados por el LLM (poco fiables), el sistema:
- Extrae los nombres de medios y descripciones temáticas de la sección «Fuentes Sugeridas para Ampliar» de cada propuesta.
- Empareja cada fuente con los artículos del pipeline por nombre del medio (comparación flexible: case-insensitive, parcial, por primera palabra) y similitud temática (solapamiento de keywords entre la descripción de la fuente y el título + contenido del artículo, usando índice Jaccard con bonus por keywords en el título).
- Enriquece los artículos emparejados con contenido completo forzando extracción vía
trafilaturasi es necesario. - Guarda un archivo companion JSON (
pauta_semanal_AAAA_MM_DD_companion.json) con el contenido completo de cada artículo fuente.
- Prompt enriquecido — El redactor recibe una sección
## Material de origen disponiblecon el contenido completo de los artículos fuente correctos para su propuesta, permitiéndole escribir con datos verificables en lugar de inventar o depender de memoria. - Redacción del artículo — Usa un system prompt de redactor periodístico (identidad La Chispa Sur) que exige trazabilidad de cada dato a las fuentes proporcionadas.
- Artículo final — Lead, desarrollo por secciones con subtítulos, y fuentes citadas al final. Guardado como
articulo_N_slug-del-titulo.md.
¿Por qué matching determinista y no números de artículo? En pruebas reales, el LLM que genera la pauta frecuentemente asigna números de artículo incorrectos en el bloque de referencias (ej: mapear una propuesta sobre tala de bosque nativo en Villarrica a artículos sobre salmonicultura o conflictos en Líbano). El matching por nombre de medio + keywords extrae las fuentes directamente del texto de la pauta —que el LLM escribe de forma natural y confiable— y las cruza con los artículos del pipeline sin depender del auto-reporte del modelo.
El punto de entrada python -m news_agent ofrece dos modos:
| Modo | Comando |
|---|---|
| Generar pauta | python -m news_agent --feeds rss_feeds.json --output ./reportes |
| Generar pauta + debug | python -m news_agent --feeds rss_feeds.json --output ./reportes --debug |
| Escribir artículo | python -m news_agent --write-article reportes/pauta_semanal_AAAA_MM_DD.md --article 1 --output ./articulos |
Flags adicionales:
--verbose: Activa logging nivel DEBUG para diagnóstico detallado.--output: Directorio donde guardar los archivos generados.--debug: Guarda un archivo JSON intermedio endebug/articulos_procesados_YYYY_MM_DD.jsoncon los datos completos de cada artículo (resumen RSS, contenido extraído y resumen final enviado al LLM) para depuración y ajuste de prompts.
news_agent/
├── __main__.py # Punto de entrada CLI (argparse)
├── orchestrator.py # Orquestador del pipeline completo
├── config.py # Carga de .env, validación de API key, feeds JSON
├── rss_fetcher.py # Ingesta RSS + despacho a scraping según método
├── scraper.py # Scraping web con requests + BeautifulSoup (selectores CSS)
├── content_enricher.py # Extracción de texto completo con trafilatura + caché
├── news_filter.py # Ventana temporal (168h / 7 días), limpieza HTML, truncado
├── intermediate_writer.py # Escritura de archivo JSON intermedio para depuración
├── prompt_builder.py # Construcción de system/user prompts editoriales (~545 líneas)
├── llm_client.py # Cliente DeepSeek vía SDK OpenAI (modo compatible)
├── report_writer.py # Escritura de reportes .md y artículos
├── article_writer.py # Parseo de pauta + escritura de artículo completo
└── source_references.py # Emparejamiento determinista de fuentes y companion JSON
| Capa | Tecnología | Propósito |
|---|---|---|
| Ingesta RSS | feedparser |
Parseo de canales RSS/Atom |
| Scraping web | requests + beautifulsoup4 |
Extracción de artículos desde sitios sin RSS con selectores CSS configurables |
| Enriquecimiento | trafilatura |
Extracción del texto completo de artículos con resúmenes RSS insuficientes |
| Cliente LLM | openai (SDK) |
Conexión con DeepSeek API en modo compatible |
| Lenguaje | Python ≥ 3.10 | stdlib + 5 dependencias |
| Configuración | .env + rss_feeds.json |
Separación de credenciales y fuentes |
| Testing | pytest, pytest-mock, freezegun |
Tests unitarios para todos los módulos |
| Parámetro | Valor | Descripción |
|---|---|---|
model |
deepseek-v4-pro |
Modelo principal (límite de salida: 384K tokens) |
temperature |
0.1 |
Baja temperatura para maximizar rigurosidad factual y minimizar alucinaciones |
PAUTA_MAX_TOKENS |
16384 |
Presupuesto compartido entre razonamiento y contenido para ~1000+ noticias semanales |
ARTICLE_MAX_TOKENS |
8192 |
Suficiente para ~1000 palabras en español (~2500 tokens) + razonamiento |
REASONING_EFFORT |
high |
Razonamiento profundo para la pauta semanal (high o max) |
ARTICLE_REASONING_EFFORT |
high |
Razonamiento independiente para redacción de artículos |
base_url |
https://api.deepseek.com/v1 |
Endpoint compatible OpenAI |
DeepSeek-V4-Pro opera con thinking mode activado (reasoning_effort: high). En este modo, los tokens de salida se dividen en dos campos:
reasoning_content: cadena de razonamiento interna (CoT) que el modelo usa para estructurar el análisis.content: respuesta final visible que se escribe en el archivo de salida.
El parámetro max_tokens es el presupuesto total compartido entre ambos campos. El razonamiento típicamente consume el 60-80% del presupuesto. Para la pauta semanal se usan 16K tokens (PAUTA_MAX_TOKENS) para dar espacio al razonamiento sobre grandes volúmenes de noticias (~1000+ artículos). Para la escritura de artículos se usan ~8K tokens (ARTICLE_MAX_TOKENS), ya que el modelo solo necesita razonar sobre una propuesta concreta.
Si el modelo agota el presupuesto en razonamiento y content queda vacío, el cliente tiene un fallback automático que rescata reasoning_content como último recurso para no perder la ejecución.
- Python 3.10 o superior
- Una API key de DeepSeek
git clone <repo-url> && cd news_agentpython -m venv .venv
source .venv/bin/activate
pip install -e .cp .env.example .env
# Editar .env y reemplazar con tu key real:
# DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEdita rss_feeds.json para agregar, quitar o modificar las fuentes de noticias. Se soportan dos métodos de ingesta:
[
{
"name": "Nombre del Medio",
"url": "https://ejemplo.com/rss.xml"
}
]Para medios que no disponen de feed RSS, usa "method": "scraping" con selectores CSS:
[
{
"name": "Nombre del Medio",
"url": "https://www.medio.cl",
"method": "scraping",
"enrich": false,
"selectors": {
"article": "h2:has(a[href]), h3:has(a[href])",
"title": "a[href]",
"link": "a[href]",
"summary": ".excerpt, .summary",
"date": "time, .date"
},
"date_regex": "/(\\d{4})/(\\d{2})/(\\d{2})/",
"date_format": "%Y/%m/%d",
"link_prefix": "https://www.medio.cl",
"request_headers": {}
}
]| Campo | Obligatorio | Descripción |
|---|---|---|
name |
✅ | Nombre descriptivo del medio |
url |
✅ | URL del sitio web a scrapear |
method |
✅ | Debe ser "scraping" para activar scraping |
enrich |
❌ | Controla si se extrae el contenido completo de artículos con resúmenes cortos. Por defecto true. Usar false para medios donde el scraping ya obtiene resúmenes completos y no se justifica una petición HTTP adicional (ej: El Mostrador) |
min_summary_length |
❌ | Umbral de longitud mínima de resumen (en caracteres) por debajo del cual se intenta enriquecer. Si no se especifica, usa el default global (150) |
selectors.article |
✅ | Selector CSS para cada contenedor de artículo |
selectors.title |
✅ | Selector CSS para el título (dentro del contenedor) |
selectors.link |
❌ | Selector CSS para el enlace (por defecto usa el mismo que title) |
selectors.summary |
❌ | Selector CSS para el resumen/extracto |
selectors.date |
❌ | Selector CSS para la fecha de publicación |
date_regex |
❌ | Regex para extraer la fecha desde la URL del artículo (ej: /(\\d{4})/(\\d{2})/(\\d{2})/) |
date_format |
❌ | Formato strptime para parsear la fecha extraída |
link_prefix |
❌ | Prefijo para resolver URLs relativas |
request_headers |
❌ | Headers HTTP adicionales para la petición |
Nota: Los selectores CSS deben coincidir con la estructura HTML real del sitio. Usa las herramientas de desarrollador del navegador para identificar los selectores correctos. Si el sitio cambia su estructura, solo necesitas actualizar los selectores en el JSON, sin tocar código.
# Generar pauta editorial semanal
python -m news_agent --feeds rss_feeds.json --output ./reportes
# Generar pauta con archivo de depuración intermedio
python -m news_agent --feeds rss_feeds.json --output ./reportes --debug
# Escribir artículo completo desde propuesta #1
python -m news_agent --write-article reportes/pauta_semanal_2026_07_04.md --article 1 --output ./articulospip install -e ".[dev]"
pytestLas constantes principales se encuentran en news_agent/config.py y pueden ajustarse según necesidades:
| Constante | Valor por defecto | Descripción |
|---|---|---|
DEEPSEEK_MODEL |
"deepseek-v4-pro" |
Modelo a utilizar |
TEMPERATURE |
0.1 |
Temperatura de sampling (0.0–2.0). Valor bajo para privilegiar precisión factual |
PAUTA_MAX_TOKENS |
16384 |
Límite de tokens para generación de pauta semanal |
ARTICLE_MAX_TOKENS |
8192 |
Límite de tokens para escritura de artículo (~1000 palabras + razonamiento) |
REASONING_EFFORT |
"high" |
Esfuerzo de razonamiento para la pauta ("high", "max", o None para deshabilitar) |
ARTICLE_REASONING_EFFORT |
"high" |
Esfuerzo de razonamiento independiente para redacción de artículos |
TIME_WINDOW_HOURS |
168 |
Ventana de análisis en horas (7 días, lunes a domingo) |
SUMMARY_MAX_CHARS |
700 |
Caracteres máximos por resumen. Amplio para preservar leads, cifras y atribuciones necesarias para la verificación factual |
FULL_CONTENT_FETCH_ENABLED |
True |
Control global de enriquecimiento de contenido |
MIN_SUMMARY_LENGTH |
150 |
Si el resumen RSS tiene menos de esto, se intenta extraer el texto completo |
FULL_CONTENT_TIMEOUT |
15 |
Timeout HTTP (segundos) para cada extracción de artículo |
FULL_CONTENT_DELAY |
1.0 |
Pausa entre peticiones al mismo dominio (segundos) |
FULL_CONTENT_MAX_WORKERS |
4 |
Hilos paralelos para extracción de contenido |
FULL_CONTENT_CACHE_DIR |
Ruta absoluta a cache/ |
Directorio para caché de contenido extraído (resuelto relativo al paquete, cron-safe) |
SOURCE_ARTICLE_MAX_CHARS |
2000 |
Caracteres máximos de contenido fuente por artículo en el prompt del redactor |
COMPANION_MAX_ARTICLES_PER_SOURCE |
3 |
Máximo de artículos del mismo medio incluidos en el companion JSON de fuentes |
Para ejecutar el agente de forma semanal (recomendado: domingo a las 23:00 o lunes a las 07:00):
# Ejemplo: todos los lunes a las 07:00 hrs
0 7 * * 1 cd /ruta/al/news_agent && /ruta/al/.venv/bin/python -m news_agent --feeds rss_feeds.json --output ./reportesEl agente implementa fail-safe en cada etapa del pipeline:
- Feed caído o scraping fallido → registra el error, continúa con el siguiente medio.
- Extracción de contenido fallida → conserva el resumen RSS original. El enriquecimiento es best-effort y nunca interrumpe el pipeline.
- Timeout en trafilatura → timeout configurable por hilo; si se excede, se descarta la extracción y se usa el resumen RSS.
- 0 noticias en ventana → aborta antes de llamar a la API (ahorro de tokens).
- API key ausente → error explícito de configuración al inicio.
- API devuelve vacío → aborta con mensaje claro. Si
contentesNonepero existereasoning_content, se usa este último como fallback automático. - Directorio de salida inexistente →
IOErrorantes de escribir. - Configuración de scraping inválida → error descriptivo al cargar
rss_feeds.jsonsi faltan selectores obligatorios.
# ⚡ Pauta Editorial Sugerida - La Chispa Sur
**Fecha de Generación:** 2026-07-08
**Notas Procesadas:** 200
---
## 1. [TÍTULO GANCHO DEL ARTÍCULO 1]
* **Enfoque Editorial:** [3-5 líneas explicando el valor agregado...]
* **Puntos Clave a Desarrollar:**
1. [Arista de investigación 1]
2. [Arista de investigación 2]
3. [Ángulo crítico, proyección o pregunta abierta]
* **Fuentes Sugeridas para Ampliar:**
* [Medio 1]: [Qué aporta]
* [Medio 2]: [Qué aporta]
## 2. [TÍTULO GANCHO DEL ARTÍCULO 2]
...Al usar --debug, se genera un JSON con cada artículo procesado, permitiendo comparar:
summary_raw: resumen RSS originalfull_content: contenido completo extraído (si se logró enriquecer)summary_clean: resumen final enviado al LLM
Se genera automáticamente junto con la pauta. Contiene, para cada propuesta, los artículos fuente emparejados con su contenido completo:
{
"metadata": {
"generated_at": "2026-07-09T12:00:00",
"total_articles_in_pipeline": 200
},
"proposal_1": {
"articles": [
{
"title": "Condenan a empresa por tala de bosque nativo en Villarrica",
"source": "CIPER Chile",
"link": "https://...",
"summary": "Resumen limpio del artículo...",
"content": "Contenido completo extraído con trafilatura (truncado a 2000 chars)..."
}
]
},
"proposal_2": { "articles": [...] },
"proposal_3": { "articles": [...] }
}Este archivo es leído automáticamente por article_writer.py al redactar un artículo, inyectando el contenido como ## Material de origen disponible en el prompt del redactor.
Artículo de ~1000 palabras con lead periodístico, desarrollo en 3 secciones con subtítulos, y fuentes citadas al final. Ver ejemplos reales en articulos/.
# Instalar dependencias de desarrollo
pip install -e ".[dev]"
# Ejecutar tests
pytest
# Ejecutar tests con cobertura
pytest --cov=news_agent