mirror of
https://github.com/nubenetes/awesome-kubernetes.git
synced 2026-05-23 09:33:33 +00:00
feat: optimize curation workflow with parallel URL expansion and repository consolidation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
26
src/main.py
26
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:
|
||||
|
||||
Reference in New Issue
Block a user