Spaces:
Running
Running
| # 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 | |
| } |