# agents/specialized.py import os from core.llm_client import llm from utils.prompts import get_today_prefix, get_chat_style from agents.registry import AGENT_REGISTRY from core.file_builder import build_docx, build_excel import re import json import asyncio import requests import base64 from pathlib import Path from datetime import datetime AGENT_ROLES = { "manager": """ Eres el gerente principal. Cuando no haya delegación o la tarea sea simple, responde de forma natural, amigable y útil al usuario. No necesitas generar JSON aquí, solo contesta directamente. Mantén un tono profesional pero cercano. """, "image_agent": """ Eres el agente de imágenes. Cuando se te pida generar, crear, mostrar o visualizar una imagen, responde SOLO con un JSON: {"image_queries": ["término en inglés 1", "término en inglés 2", "término en inglés 3"]} Los términos deben ser específicos, detallados y en inglés para obtener mejores resultados. Si la tarea no es sobre imágenes, responde: {"skip":"no image task"} """, "backend_dev": """ Eres programador backend senior. REGLAS ABSOLUTAS: 1. Entrega SOLO el código pedido, sin explicaciones innecesarias. 2. Para excel/planilla → responde con EXCEL_TEMPLATE:{"title":"...","sheet_name":"...","headers":[...],"sample_rows":[[...],...]} 3. Python → entrega código Python puro y funcional. 4. Groovy/Jenkins → entrega el script completo. 5. Si hay frontend_dev en el equipo, TÚ haces servidor/backend, él hace HTML. 6. Si la tarea no requiere backend → responde: {"skip":"no backend needed"} """, "writer": """ Eres redactor experto. Escribe SOLO contenido real y extenso (500+ palabras). Sin placeholders. Usa ## para secciones y ### para subsecciones. Secciones: ## Resumen Ejecutivo, ### Introducción, ### Desarrollo, ### Hallazgos, ### Conclusiones, ### Recomendaciones """, "analyst": """ Eres analista de negocios. REGLAS: 1. Solo haz lo que el manager delegó: revisar documentos, evaluar viabilidad, analizar riesgos. 2. NUNCA describas imágenes ni hagas trabajo de otros agentes. 3. Si la tarea no requiere análisis → responde: {"skip":"no analysis needed"} """, "frontend_dev": """ Eres desarrollador frontend senior. REGLAS ABSOLUTAS: 1. Entrega SOLO código HTML/CSS/JS pedido, sin explicaciones innecesarias. 2. Si hay backend_dev, TÚ haces HTML/interfaz, él hace servidor/lógica. 3. Si la tarea NO requiere frontend → responde: {"skip":"no frontend needed"} 4. Entrega siempre HTML completo y funcional con los estilos incluidos. """ } """ async def generate_real_image(prompts: list, model: str = "black-forest-labs/FLUX.1-schnell"): Genera una o más imágenes reales usando HF Inference API results = [] for i, prompt in enumerate(prompts[:3]): # límite de 3 imágenes por seguridad try: from huggingface_hub import InferenceClient client = InferenceClient(model=model, token=os.getenv("HF_API_TOKEN")) enhanced_prompt = f"{prompt}, highly detailed, cinematic lighting, 8k, masterpiece, best quality" image = await asyncio.to_thread( client.text_to_image, enhanced_prompt, num_inference_steps=20, guidance_scale=7.5, height=768, width=768 ) DOCS_DIR = Path("data/docs") DOCS_DIR.mkdir(exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") file_name = f"generated_image_{ts}_{i+1}.png" file_path = DOCS_DIR / file_name image.save(file_path) results.append({ "success": True, "image_url": f"/docs/{file_name}", "prompt_used": enhanced_prompt }) except Exception as e: results.append({"success": False, "error": str(e)}) return results """ # 🔵 FUNCIONES AUXILIARES (AQUÍ VAN) async def generate_with_gemini(queries, model_name): # ⚠️ por ahora fallback return [] async def generate_with_hf(queries): urls = [] for i, prompt in enumerate(queries[:2]): response = requests.post( "https://router.huggingface.co/v1/images/generations", headers={ "Authorization": f"Bearer {os.getenv('HF_API_TOKEN')}" }, json={ "model": "black-forest-labs/FLUX.1-schnell", "prompt": prompt, "size": "1024x1024" } ) if response.status_code == 200: data = response.json() image_base64 = data["data"][0]["b64_json"] image_bytes = base64.b64decode(image_base64) DOCS_DIR = Path("data/docs") DOCS_DIR.mkdir(exist_ok=True) file_name = f"hf_image_{datetime.now().timestamp()}.png" file_path = DOCS_DIR / file_name with open(file_path, "wb") as f: f.write(image_bytes) urls.append(f"/docs/{file_name}") else: print("HF error:", response.text) return urls # 🟢 FUNCIÓN PRINCIPAL async def generate_real_image(prompts: list): results = [] for prompt in prompts[:3]: try: response = requests.post( "https://router.huggingface.co/v1/images/generations", headers={ "Authorization": f"Bearer {os.getenv('HF_API_TOKEN')}", "Content-Type": "application/json" }, json={ "model": "black-forest-labs/FLUX.1-schnell", "prompt": prompt, "size": "1024x1024" } ) # 🔥 DEBUG print("STATUS:", response.status_code) print("RAW:", response.text[:200]) if response.status_code != 200: raise Exception(f"HTTP {response.status_code}: {response.text}") # intentar parsear JSON try: data = response.json() except: raise Exception("Respuesta no es JSON válida") if "data" not in data: raise Exception(f"Formato inesperado: {data}") image_url = data["data"][0].get("url") if not image_url: raise Exception("No se encontró URL de imagen") results.append({ "success": True, "image_url": image_url }) except Exception as e: results.append({ "success": False, "error": str(e) }) return results async def run_specialized_agent(agent_key: str, task: str, context: dict = None) -> dict: if agent_key not in AGENT_ROLES: return {"error": f"Agente {agent_key} no implementado aún", "success": False} agent = AGENT_REGISTRY.get(agent_key, {}) today = get_today_prefix() role = AGENT_ROLES[agent_key] full_prompt = today + role + get_chat_style() + f"\n\nTarea asignada: {task}" if context: full_prompt += "\n\nContexto de otros agentes:\n" + "\n".join([f"[{k.upper()}] {v[:300]}..." for k, v in context.items()]) messages = [{"role": "user", "content": full_prompt}] try: response = await llm.call(agent_key, messages, temperature=0.45) result = { "agent": agent_key, "response": response, "success": True, "file_path": None, "file_type": None, "image_urls": [], # ahora lista para múltiples imágenes "queries": None } # Generar archivo según agente if agent_key == "writer" and len(response.strip()) > 300: title = task[:60].strip('"') or "Informe generado" try: file_path = build_docx(title, response, images=None) if file_path: result["file_path"] = file_path.name result["file_type"] = "docx" except Exception as e: result["file_error"] = str(e) elif agent_key == "backend_dev": match = re.search(r'EXCEL_TEMPLATE:\s*(\{.*\})', response, re.DOTALL | re.IGNORECASE) if match: try: excel_data = json.loads(match.group(1)) file_path = build_excel(task, excel_data) if file_path: result["file_path"] = file_path.name result["file_type"] = "xlsx" result["response"] = response.split("EXCEL_TEMPLATE:")[0].strip() except Exception as e: result["file_error"] = str(e) elif agent_key == "image_agent": agent_config = AGENT_REGISTRY.get(agent_key, {}) provider = agent_config.get("provider") model_name = agent_config.get("models", [None])[0] print("USANDO PROVIDER:", provider) print("USANDO MODELO:", model_name) match = re.search(r'\{.*"image_queries".*\}', response, re.DOTALL) if match: try: data = json.loads(match.group(0)) queries = data.get("image_queries", []) result["queries"] = queries # 🧠 respuesta base result["response"] = "Ideas de imágenes generadas:\n" + "\n".join(queries) image_urls = [] # 🔥 intento 1: provider principal (registry) try: if provider == "gemini": image_urls = await generate_with_gemini(queries, model_name) elif provider == "huggingface": image_urls = await generate_with_hf(queries) except Exception as e: print("Provider principal falló:", str(e)) # 🔥 fallback automático if not image_urls: try: print("Intentando fallback HF...") image_urls = await generate_with_hf(queries) except Exception as e: print("Fallback falló:", str(e)) # 🔥 fallback final visual if not image_urls: image_urls = ["https://picsum.photos/400/300"] result["image_urls"] = image_urls if image_urls: result["response"] += "\n\nImagen generada ✔" else: result["response"] += "\n\n⚠️ No se pudo generar imagen" except Exception as e: result["response"] = f"Error procesando JSON: {str(e)}" else: result["response"] = "No se detectaron instrucciones claras para imágenes." return result except Exception as e: return { "agent": agent_key, "error": str(e), "success": False }