diff --git a/src/gitops_manager.py b/src/gitops_manager.py index ecdf1fe6..3fd243b3 100644 --- a/src/gitops_manager.py +++ b/src/gitops_manager.py @@ -11,19 +11,45 @@ class RepositoryController: base_branch = self.repository.get_branch(self.default_branch_name) self.repository.create_git_ref(ref=f"refs/heads/{branch_name}", sha=base_branch.commit.sha) - def apply_state_changes(self, target_file_path: str, new_document_state: str, metrics: dict) -> None: + def apply_multi_file_changes(self, updates: dict, metrics: dict) -> None: + """ + updates: dict { "path/to/file": "new_content" } + """ + if not updates: + return + timestamp_slug = datetime.now().strftime("%Y%m%d-%H%M") branch_name = f"bot/agentic-curation-{timestamp_slug}" self._create_feature_branch(branch_name) - file_meta = self.repository.get_contents(target_file_path, ref=self.default_branch_name) - commit_signature = f"chore(docs): automatización agéntica de curaduría [{timestamp_slug}]" - self.repository.update_file(path=target_file_path, message=commit_signature, content=new_document_state, sha=file_meta.sha, branch=branch_name) + + for file_path, content in updates.items(): + try: + file_meta = self.repository.get_contents(file_path, ref=self.default_branch_name) + commit_signature = f"chore(docs): curaduría agéntica en {file_path} [{timestamp_slug}]" + self.repository.update_file( + path=file_path, + message=commit_signature, + content=content, + sha=file_meta.sha, + branch=branch_name + ) + except Exception as e: + print(f"Error actualizando {file_path}: {e}") + pr_narrative = ( "## 🤖 Ejecución del Agente Curador de Nubenetes\n\n" - "Este Pull Request ha sido ensamblado de manera autónoma.\n\n" - "### Resumen:\n" - f"- Nuevos Enlaces (Redes): {metrics.get('social_injections', 0)}\n" - f"- Nuevos Enlaces (Descubrimiento): {metrics.get('autonomous_injections', 0)}\n" - "- Purga de enlaces caídos y duplicados completada." + "Este Pull Request ha sido ensamblado de manera autónoma con **Gemini**.\n\n" + "### Resumen de Inyecciones:\n" + f"- **Vía Redes Sociales (@nubenetes):** {metrics.get('social_injections', 0)}\n" + f"- **Vía Descubrimiento Autónomo (Trending):** {metrics.get('autonomous_injections', 0)}\n\n" + "### Labores de Mantenimiento:\n" + "- Purga global de hipervínculos caídos (404/500).\n" + "- Eliminación de duplicados y optimización de Markdown en múltiples archivos." + ) + + self.repository.create_pull( + title=f"Automated Knowledge Curation: {datetime.now().strftime('%d %b %Y')}", + body=pr_narrative, + head=branch_name, + base=self.default_branch_name ) - self.repository.create_pull(title=f"Curation: {datetime.now().strftime('%d %b %Y')}", body=pr_narrative, head=branch_name, base=self.default_branch_name) diff --git a/src/main.py b/src/main.py index 876ee678..89056635 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,6 @@ import asyncio from datetime import datetime, timedelta -from src.config import TARGET_REPO, MAIN_DOC_FILE, MADRID_TZ, GH_TOKEN +from src.config import TARGET_REPO, MADRID_TZ, GH_TOKEN, NUBENETES_CATEGORIES from src.ingestion_twikit import SocialDataExtractor from src.markdown_ast import MarkdownSanitizer from src.agentic_curator import evaluate_extracted_assets @@ -10,8 +10,6 @@ from src.gitops_manager import RepositoryController async def master_orchestrator(): git_controller = RepositoryController(GH_TOKEN, TARGET_REPO) - # DINÁMICO: Obtener la fecha del último commit en la carpeta docs/ - # Si no hay commits previos o hay error, usamos la fecha de seguridad 2024-10-05 try: commits = git_controller.repository.get_commits(path="docs") last_commit_date = commits[0].commit.committer.date.replace(tzinfo=MADRID_TZ) @@ -19,49 +17,56 @@ async def master_orchestrator(): except: time_horizon = datetime(2024, 10, 5, 18, 36, tzinfo=MADRID_TZ) - print(f"Buscando actualizaciones desde: {time_horizon}") + print(f">>> Iniciando curaduría desde: {time_horizon}") - # 1. Extracción de X (Twitter) + # 1. Obtención de datos twitter_client = SocialDataExtractor() raw_social_links = await twitter_client.fetch_links_since(time_horizon) - - # 2. Descubrimiento proactivo en GitHub autonomous_links = await discover_trending_assets() - # 3. Evaluación con IA (Gemini) + # 2. Evaluación con IA curated_social_links = await evaluate_extracted_assets(raw_social_links) - total_new_assets = curated_social_links + autonomous_links - # 4. Procesamiento de Markdown + # 3. Preparar cambios markdown_sanitizer = MarkdownSanitizer() - repo_file_data = git_controller.repository.get_contents(MAIN_DOC_FILE) - document_state = repo_file_data.decoded_content.decode("utf-8") + file_updates = {} - # 5. Limpieza de enlaces rotos y duplicados (Higiene) - purified_document_state = await markdown_sanitizer.sanitize_document(document_state) + # Identificar qué archivos necesitan ser procesados + categories_to_update = set([a["category"] for asset in total_new_assets]) - # 6. Inyección de nuevos recursos - final_document_state = purified_document_state - for asset in total_new_assets: - final_document_state = markdown_sanitizer.inject_curated_link( - final_document_state, - asset["category"], - asset["title"], - asset["url"], - asset["description"] - ) - - # 7. Persistencia vía Pull Request si hay cambios - if final_document_state.strip() != document_state.strip(): + # 4. Procesar inyecciones y saneamiento + for category in NUBENETES_CATEGORIES: + file_path = f"docs/{category}.md" + try: + repo_file = git_controller.repository.get_contents(file_path) + content = repo_file.decoded_content.decode("utf-8") + + # Saneamiento (siempre lo hacemos para mantener la salud) + new_content = await markdown_sanitizer.sanitize_document(content) + + # Inyección si hay activos para esta categoría + for asset in total_new_assets: + if asset["category"] == category: + new_content = markdown_sanitizer.inject_curated_link( + new_content, category, asset["title"], asset["url"], asset["description"] + ) + + if new_content.strip() != content.strip(): + file_updates[file_path] = new_content + except: + continue + + # 5. Aplicar cambios vía GitOps + if file_updates: metrics = { "social_injections": len(curated_social_links), "autonomous_injections": len(autonomous_links) } - git_controller.apply_state_changes(MAIN_DOC_FILE, final_document_state, metrics) - print(f"Cambios detectados. PR abierta con {len(total_new_assets)} nuevos recursos.") + git_controller.apply_multi_file_changes(file_updates, metrics) + print(f">>> Éxito: PR abierta con cambios en {len(file_updates)} archivos.") else: - print("El repositorio ya está actualizado. Sin cambios pendientes.") + print(">>> Sin cambios necesarios.") if __name__ == "__main__": asyncio.run(master_orchestrator())