feat: optimize curation workflow with parallel URL expansion and repository consolidation

This commit is contained in:
Nubenetes Bot
2026-05-14 18:43:30 +02:00
parent 39e8f16bec
commit 9690bc501c
4 changed files with 93 additions and 9 deletions

View File

@@ -60,20 +60,39 @@ async def evaluate_extracted_assets(raw_assets: List[Dict]) -> Dict[str, Dict]:
log_event(f" [-] RECHAZADO: Dominio en lista negra ({domain})")
evaluations[asset["url"]] = {"status": "FILTERED", "reason": "Dominio en lista negra"}
continue
...
web_content = await _deep_fetch_content(asset['url'])
prompt = (
"Actúas como Ingeniero Curador Senior de 'nubenetes/awesome-kubernetes'.\n"
"Tu misión es catalogar contenido TÉCNICO sobre Kubernetes y Cloud Native compartido por el usuario.\n"
"REGLA DE ORO: Si el enlace está en el feed, es porque el usuario lo considera útil. NO lo descartes a menos que sea ruido total (publicidad agresiva, error 404, o contenido no técnico).\n\n"
f"Categorías válidas: {', '.join(NUBENETES_CATEGORIES)}.\n\n"
"INSTRUCCIONES:\n"
"1. YOUTUBE: Acepta videos técnicos o tutoriales. Categorízalos según su temática.\n"
"2. RESUMEN: Crea un resumen conciso (1 frase). Usa prioritariamente el 'Contexto' (que es el post de X) ya que suele explicar por qué se compartió.\n"
"3. ASIGNACIÓN: Si es sobre Model Context Protocol (MCP), asígnalo a 'ai-agents-mcp'.\n\n"
f"URL: {asset['url']}\nContexto de X: {context}\nContenido Web Extraído: {web_content[:2000]}\n\n"
"Evalúa el IMPACTO TÉCNICO (1-100):\n"
"- >80: Recurso excepcional (🌟).\n"
"- >5: Aceptar (si encaja en alguna categoría).\n"
"- <5: Descartar (Ruido absoluto).\n\n"
"Responde SOLAMENTE un JSON: {\"impact_score\": int, \"categories\": [\"cat1\"], \"title\": \"...\", \"desc\": \"...\", \"reasoning\": \"Breve explicación de por qué esta categoría y score\", \"rejection_reason\": \"... (si aplica)\"}"
)
try:
data = await call_gemini_with_retry(prompt)
score = data.get("impact_score", 50)
valid_cats = [c for c in data.get("categories", []) if c in NUBENETES_CATEGORIES]
reasoning = data.get("reasoning", "Sin motivo especificado")
if score < 20:
if score < 5:
reason = data.get("rejection_reason", "Bajo impacto técnico")
evaluations[asset["url"]] = {"status": "FILTERED", "reason": reason}
log_event(f" [-] RECHAZADO: {reason} (Score: {score})")
log_event(f" Motivo IA: {reasoning}")
if score < 10 and domain not in domain_blacklist:
if score < 1 and domain not in domain_blacklist:
domain_blacklist.add(domain)
log_event(f" [!] Dominio {domain} añadido a lista negra.")
elif not valid_cats:
@@ -174,7 +193,3 @@ class AgenticCurator:
def validate_changes(self) -> bool:
return True
def validate_changes(self) -> bool:
return True

View File

@@ -31,6 +31,52 @@ class GeminiDiagnostics:
report += "\n"
return report
async def resolve_url(url: str) -> str:
"""Sigue las redirecciones para obtener la URL larga final y consolida repositorios si fallan."""
shorteners = ['t.co', 'bit.ly', 'buff.ly', 'goo.gl', 'tinyurl.com', 't.ly', 'rb.gy', 'is.gd', 'drp.li', 't.me']
try:
domain = url.split("//")[-1].split("/")[0].lower()
except:
return url
# 1. Expansión inicial
final_url = url
if domain in shorteners or url.endswith(''):
try:
async with httpx.AsyncClient(follow_redirects=True) as client:
resp = await client.head(url, timeout=5)
final_url = str(resp.url)
if final_url != url:
log_event(f" [🔗] URL Expandida: {url} -> {final_url}")
except:
pass
# 2. Consolidación de Repositorios (GitHub/GitLab)
repo_domains = ['github.com', 'gitlab.com']
current_domain = final_url.split("//")[-1].split("/")[0].lower()
if any(d in current_domain for d in repo_domains):
# Intentar validar si el enlace profundo funciona
try:
async with httpx.AsyncClient(follow_redirects=True) as client:
resp = await client.head(final_url, timeout=5)
if resp.status_code == 200:
return final_url
# Si falla, intentar consolidar a la raíz del repo
# Formato esperado: https://github.com/user/repo/...
parts = final_url.split('/')
if len(parts) > 4: # https: , , domain, user, repo
root_repo = "/".join(parts[:5])
resp_root = await client.head(root_repo, timeout=5)
if resp_root.status_code == 200:
log_event(f" [📦] Consolidación: {final_url} -> {root_repo} (Raíz validada)")
return root_repo
except:
pass
return final_url
async def call_gemini_with_retry(prompt: str, response_format: str = "json", max_retries: int = 3):
"""
Llama a la API de Gemini con rotación exhaustiva y REINTENTO REAL en 429.

View File

@@ -12,7 +12,7 @@ from src.autonomous_discovery import discover_trending_assets
from src.gitops_manager import RepositoryController
from src.logger import log_event
from src.state_manager import get_last_date, save_state
from src.gemini_utils import call_gemini_with_retry, resolve_url
async def master_orchestrator():
git_controller = RepositoryController(GH_TOKEN, TARGET_REPO)
@@ -82,7 +82,29 @@ async def master_orchestrator():
log_event("[!] No se encontraron nuevos enlaces para procesar.")
return
# 3. Evaluación y Registro (Deduplicación Global Robusta)
# 3. Expansión y Deduplicación Inicial
log_event(f"[*] Expandiendo y deduplicando {len(all_raw_assets)} enlaces brutos...")
async def process_asset(asset):
expanded_url = await resolve_url(asset["url"])
asset["url"] = expanded_url
return asset
all_raw_assets = await asyncio.gather(*[process_asset(a) for a in all_raw_assets])
unique_assets_map = {}
for asset in all_raw_assets:
clean_url = asset["url"].split('#')[0].rstrip('/').lower()
if clean_url not in unique_assets_map:
unique_assets_map[clean_url] = asset
else:
if len(asset.get("context", "")) > len(unique_assets_map[clean_url].get("context", "")):
unique_assets_map[clean_url] = asset
all_raw_assets = list(unique_assets_map.values())
log_event(f"[*] Total tras deduplicación inicial: {len(all_raw_assets)} enlaces únicos.")
# 4. Evaluación y Registro (Deduplicación Global Robusta)
existing_urls = set()
for root, dirs, files in os.walk("docs"):
for file in files: