From 9690bc501c548bef66cca52bb2fb02de13c0adeb Mon Sep 17 00:00:00 2001 From: Nubenetes Bot Date: Thu, 14 May 2026 18:43:30 +0200 Subject: [PATCH] feat: optimize curation workflow with parallel URL expansion and repository consolidation --- GEMINI.md | 1 + src/agentic_curator.py | 29 +++++++++++++++++++------- src/gemini_utils.py | 46 ++++++++++++++++++++++++++++++++++++++++++ src/main.py | 26 ++++++++++++++++++++++-- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 93de3a46..7edff875 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -8,6 +8,7 @@ Este archivo contiene las instrucciones acumuladas y la visión de largo plazo p 2. **Aprendizaje Persistente**: Utiliza `src/memory/health_learning.json` para almacenar el conocimiento sobre dominios (bloqueos anti-bot, estrategias exitosas) y patrones de navegación. 3. **Resiliencia Total**: El workflow debe ser capaz de continuar incluso si hay errores individuales en validaciones de links o archivos. Prioriza generar un resultado (PR) aunque sea parcial. 4. **Consolidación de Repositorios**: Ante un fallo en un enlace profundo de GitHub/GitLab, intenta siempre validar la raíz del repositorio antes de darlo por muerto. Preferimos enlaces estables a raíces de repositorios que deep-links volátiles. +5. **Expansión de URLs**: Todos los enlaces acortados (t.co, bit.ly, buff.ly, etc.) DEBEN ser expandidos a su versión larga original antes de ser evaluados o inyectados. Esto garantiza la homogeneidad del inventario y mejora la precisión de la deduplicación global. ## 🛠️ Evolución Estructural (Progressive Reorganization) diff --git a/src/agentic_curator.py b/src/agentic_curator.py index b3cabb1c..dd61409c 100644 --- a/src/agentic_curator.py +++ b/src/agentic_curator.py @@ -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 diff --git a/src/gemini_utils.py b/src/gemini_utils.py index 6125d7a4..2ec8bc06 100644 --- a/src/gemini_utils.py +++ b/src/gemini_utils.py @@ -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. diff --git a/src/main.py b/src/main.py index ea7263d5..10614977 100644 --- a/src/main.py +++ b/src/main.py @@ -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: