From 964e38564a6a2da0d1974c47dab52a98d2009836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Consultor=C3=ADa=20AS?= Date: Wed, 28 Jan 2026 01:49:49 +0000 Subject: [PATCH] feat(phase-3): Complete AI content generation system - Add /api/generate endpoints for AI content generation - Integrate DeepSeek AI into dashboard compose page - Add content templates for tips, products, services, engagement - Implement batch generation for weekly/monthly calendars - Add cost estimation endpoint New endpoints: - POST /api/generate/tip - Generate tech tips - POST /api/generate/product - Generate product posts - POST /api/generate/service - Generate service posts - POST /api/generate/thread - Generate educational threads - POST /api/generate/response - Generate response suggestions - POST /api/generate/adapt - Adapt content to platform - POST /api/generate/improve - Improve existing content - POST /api/generate/batch - Batch generate for calendar - GET /api/generate/batch/estimate - Estimate batch costs - GET /api/generate/status - Check AI connection Co-Authored-By: Claude Opus 4.5 --- app/api/routes/generate.py | 551 +++++++++++++++++++++++++++++++ app/data/__init__.py | 31 ++ app/data/content_templates.py | 269 +++++++++++++++ app/main.py | 3 +- app/services/batch_generator.py | 326 ++++++++++++++++++ dashboard/templates/compose.html | 211 +++++++++++- 6 files changed, 1376 insertions(+), 15 deletions(-) create mode 100644 app/api/routes/generate.py create mode 100644 app/data/__init__.py create mode 100644 app/data/content_templates.py create mode 100644 app/services/batch_generator.py diff --git a/app/api/routes/generate.py b/app/api/routes/generate.py new file mode 100644 index 0000000..378134c --- /dev/null +++ b/app/api/routes/generate.py @@ -0,0 +1,551 @@ +""" +API endpoints para generación de contenido con IA. +""" + +from typing import Optional, List +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from app.services.content_generator import content_generator +from app.core.config import settings + + +router = APIRouter() + + +# ============================================================ +# Schemas +# ============================================================ + +class GenerateTipRequest(BaseModel): + """Solicitud para generar tip tech.""" + category: str = "general" + platform: str = "x" + template: Optional[str] = None + + +class GenerateProductPostRequest(BaseModel): + """Solicitud para generar post de producto.""" + product: dict # name, description, price, category, specs, highlights + platform: str = "x" + + +class GenerateServicePostRequest(BaseModel): + """Solicitud para generar post de servicio.""" + service: dict # name, description, category, target_sectors, benefits, call_to_action + platform: str = "x" + + +class GenerateThreadRequest(BaseModel): + """Solicitud para generar hilo educativo.""" + topic: str + num_posts: int = 5 + + +class GenerateResponseRequest(BaseModel): + """Solicitud para generar sugerencias de respuesta.""" + interaction_content: str + interaction_type: str = "comment" # comment, mention, dm + context: Optional[str] = None + + +class AdaptContentRequest(BaseModel): + """Solicitud para adaptar contenido a plataforma.""" + content: str + target_platform: str + + +class ImproveContentRequest(BaseModel): + """Solicitud para mejorar contenido existente.""" + content: str + platform: str = "x" + style: str = "engaging" # engaging, professional, casual, educational + + +class GenerateResponse(BaseModel): + """Respuesta de generación.""" + success: bool + content: Optional[str] = None + contents: Optional[List[str]] = None + error: Optional[str] = None + tokens_used: Optional[int] = None + + +# ============================================================ +# Helper +# ============================================================ + +def check_api_configured(): + """Verificar que la API de DeepSeek esté configurada.""" + if not settings.DEEPSEEK_API_KEY: + raise HTTPException( + status_code=503, + detail="DeepSeek API no configurada. Agrega DEEPSEEK_API_KEY en .env" + ) + + +# ============================================================ +# Endpoints +# ============================================================ + +@router.post("/tip", response_model=GenerateResponse) +async def generate_tip(request: GenerateTipRequest): + """ + Generar un tip de tecnología. + + Categorías sugeridas: + - productividad, seguridad, ia, programacion, hardware, + - redes, cloud, automatizacion, impresion3d, general + """ + check_api_configured() + + try: + content = await content_generator.generate_tip_tech( + category=request.category, + platform=request.platform, + template=request.template + ) + + return GenerateResponse( + success=True, + content=content + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.post("/product", response_model=GenerateResponse) +async def generate_product_post(request: GenerateProductPostRequest): + """ + Generar post promocional de producto. + + Campos del producto: + - name (requerido): Nombre del producto + - description: Descripción + - price (requerido): Precio en MXN + - category: Categoría (computadoras, impresoras3d, accesorios) + - specs: Dict con especificaciones técnicas + - highlights: Lista de puntos destacados + """ + check_api_configured() + + # Validar campos mínimos + if "name" not in request.product or "price" not in request.product: + raise HTTPException( + status_code=400, + detail="El producto debe tener 'name' y 'price'" + ) + + try: + content = await content_generator.generate_product_post( + product=request.product, + platform=request.platform + ) + + return GenerateResponse( + success=True, + content=content + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.post("/service", response_model=GenerateResponse) +async def generate_service_post(request: GenerateServicePostRequest): + """ + Generar post promocional de servicio. + + Campos del servicio: + - name (requerido): Nombre del servicio + - description: Descripción + - category: Categoría (consultoria, desarrollo, soporte, capacitacion) + - target_sectors: Lista de sectores objetivo + - benefits: Lista de beneficios + - call_to_action: CTA personalizado + """ + check_api_configured() + + if "name" not in request.service: + raise HTTPException( + status_code=400, + detail="El servicio debe tener 'name'" + ) + + try: + content = await content_generator.generate_service_post( + service=request.service, + platform=request.platform + ) + + return GenerateResponse( + success=True, + content=content + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.post("/thread", response_model=GenerateResponse) +async def generate_thread(request: GenerateThreadRequest): + """ + Generar hilo educativo. + + - topic: Tema del hilo (ej: "5 tips para proteger tu PC") + - num_posts: Cantidad de posts en el hilo (3-10) + """ + check_api_configured() + + if request.num_posts < 3 or request.num_posts > 10: + raise HTTPException( + status_code=400, + detail="num_posts debe estar entre 3 y 10" + ) + + try: + posts = await content_generator.generate_thread( + topic=request.topic, + num_posts=request.num_posts + ) + + return GenerateResponse( + success=True, + contents=posts + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.post("/response", response_model=GenerateResponse) +async def generate_response_suggestions(request: GenerateResponseRequest): + """ + Generar sugerencias de respuesta para una interacción. + + Devuelve 3 opciones de respuesta diferentes. + """ + check_api_configured() + + try: + suggestions = await content_generator.generate_response_suggestion( + interaction_content=request.interaction_content, + interaction_type=request.interaction_type, + context=request.context + ) + + return GenerateResponse( + success=True, + contents=suggestions + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.post("/adapt", response_model=GenerateResponse) +async def adapt_content(request: AdaptContentRequest): + """ + Adaptar contenido existente a una plataforma específica. + + Útil para tomar contenido de una plataforma y adaptarlo + a los límites y estilo de otra. + """ + check_api_configured() + + valid_platforms = ["x", "threads", "instagram", "facebook"] + if request.target_platform.lower() not in valid_platforms: + raise HTTPException( + status_code=400, + detail=f"Plataforma no válida. Opciones: {valid_platforms}" + ) + + try: + content = await content_generator.adapt_content_for_platform( + content=request.content, + target_platform=request.target_platform.lower() + ) + + return GenerateResponse( + success=True, + content=content + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.post("/improve", response_model=GenerateResponse) +async def improve_content(request: ImproveContentRequest): + """ + Mejorar contenido existente. + + Estilos disponibles: + - engaging: Más atractivo y con gancho + - professional: Más formal y corporativo + - casual: Más cercano y amigable + - educational: Más informativo y didáctico + """ + check_api_configured() + + try: + # Usar el generador para mejorar + from openai import OpenAI + + client = OpenAI( + api_key=settings.DEEPSEEK_API_KEY, + base_url=settings.DEEPSEEK_BASE_URL + ) + + style_prompts = { + "engaging": "Hazlo más atractivo, con un gancho inicial que capture atención", + "professional": "Hazlo más profesional y corporativo, manteniendo credibilidad", + "casual": "Hazlo más cercano y amigable, como si hablaras con un amigo", + "educational": "Hazlo más informativo y didáctico, que enseñe algo útil" + } + + char_limits = { + "x": 280, + "threads": 500, + "instagram": 2200, + "facebook": 1000 + } + + prompt = f"""Mejora este contenido para {request.platform}: + +CONTENIDO ORIGINAL: +{request.content} + +ESTILO DESEADO: {style_prompts.get(request.style, style_prompts['engaging'])} + +LÍMITE DE CARACTERES: {char_limits.get(request.platform, 500)} + +REQUISITOS: +- Mantén el mensaje principal +- Mejora la redacción y el impacto +- Incluye emojis relevantes +- Termina con hashtags apropiados + +Responde SOLO con el contenido mejorado.""" + + response = client.chat.completions.create( + model="deepseek-chat", + messages=[ + {"role": "system", "content": content_generator._get_system_prompt()}, + {"role": "user", "content": prompt} + ], + max_tokens=400, + temperature=0.7 + ) + + improved = response.choices[0].message.content.strip() + + return GenerateResponse( + success=True, + content=improved, + tokens_used=response.usage.total_tokens + ) + + except Exception as e: + return GenerateResponse( + success=False, + error=str(e) + ) + + +@router.get("/categories") +async def get_tip_categories(): + """Obtener categorías disponibles para tips.""" + return { + "categories": [ + {"id": "productividad", "name": "Productividad", "description": "Tips para ser más eficiente"}, + {"id": "seguridad", "name": "Seguridad", "description": "Ciberseguridad y protección"}, + {"id": "ia", "name": "Inteligencia Artificial", "description": "IA y machine learning"}, + {"id": "programacion", "name": "Programación", "description": "Desarrollo de software"}, + {"id": "hardware", "name": "Hardware", "description": "Equipos y componentes"}, + {"id": "redes", "name": "Redes", "description": "Networking y conectividad"}, + {"id": "cloud", "name": "Cloud", "description": "Servicios en la nube"}, + {"id": "automatizacion", "name": "Automatización", "description": "Automatizar procesos"}, + {"id": "impresion3d", "name": "Impresión 3D", "description": "Impresoras y diseño 3D"}, + {"id": "general", "name": "General", "description": "Tips variados de tecnología"}, + ] + } + + +class BatchGenerateRequest(BaseModel): + """Solicitud de generación por lotes.""" + platforms: List[str] + days: int = 7 # 7 para semana, 30 para mes + tip_categories: Optional[List[str]] = None + + +@router.post("/batch") +async def generate_batch(request: BatchGenerateRequest): + """ + Generar contenido por lotes para el calendario. + + - **platforms**: Lista de plataformas ["x", "threads", "instagram"] + - **days**: Cantidad de días (7 para semana, 30 para mes) + - **tip_categories**: Categorías de tips a usar + """ + check_api_configured() + + if request.days < 1 or request.days > 30: + raise HTTPException( + status_code=400, + detail="days debe estar entre 1 y 30" + ) + + from app.services.batch_generator import batch_generator + + try: + result = await batch_generator._generate_batch( + platforms=request.platforms, + start_date=None, # Usa mañana por defecto + days=request.days, + tip_categories=request.tip_categories + ) + + return { + "success": result.success, + "total_requested": result.total_requested, + "total_generated": result.total_generated, + "posts": [ + { + "content": p.content, + "content_type": p.content_type.value, + "platform": p.platform, + "scheduled_at": p.scheduled_at.isoformat(), + "metadata": p.metadata + } + for p in result.posts + ], + "errors": result.errors + } + + except Exception as e: + raise HTTPException( + status_code=500, + detail=str(e) + ) + + +@router.get("/batch/estimate") +async def estimate_batch_cost( + platforms: str, # comma-separated + days: int = 7 +): + """ + Estimar costo de generación por lotes. + + Calcula tokens y costo estimado sin generar contenido. + """ + from app.services.batch_generator import batch_generator + + platform_list = [p.strip() for p in platforms.split(",")] + estimate = batch_generator.calculate_costs(platform_list, days) + + return estimate + + +@router.post("/tips/batch") +async def generate_tips_batch( + count: int = 10, + platform: str = "x", + categories: Optional[str] = None # comma-separated +): + """ + Generar múltiples tips de una vez. + + - **count**: Cantidad de tips (1-20) + - **platform**: Plataforma destino + - **categories**: Categorías separadas por coma + """ + check_api_configured() + + if count < 1 or count > 20: + raise HTTPException( + status_code=400, + detail="count debe estar entre 1 y 20" + ) + + from app.services.batch_generator import batch_generator + + category_list = None + if categories: + category_list = [c.strip() for c in categories.split(",")] + + try: + tips = await batch_generator.generate_tips_batch( + count=count, + platform=platform, + categories=category_list + ) + + return { + "success": True, + "count": len(tips), + "tips": tips + } + + except Exception as e: + return { + "success": False, + "error": str(e), + "tips": [] + } + + +@router.get("/status") +async def get_ai_status(): + """Verificar estado de la API de IA.""" + configured = bool(settings.DEEPSEEK_API_KEY) + + result = { + "configured": configured, + "provider": "DeepSeek", + "model": "deepseek-chat", + "base_url": settings.DEEPSEEK_BASE_URL + } + + if configured: + try: + # Test rápido + from openai import OpenAI + client = OpenAI( + api_key=settings.DEEPSEEK_API_KEY, + base_url=settings.DEEPSEEK_BASE_URL + ) + response = client.chat.completions.create( + model="deepseek-chat", + messages=[{"role": "user", "content": "test"}], + max_tokens=5 + ) + result["status"] = "connected" + result["test"] = "OK" + except Exception as e: + result["status"] = "error" + result["error"] = str(e) + else: + result["status"] = "not_configured" + + return result diff --git a/app/data/__init__.py b/app/data/__init__.py new file mode 100644 index 0000000..c901865 --- /dev/null +++ b/app/data/__init__.py @@ -0,0 +1,31 @@ +""" +Data module - templates and static data for content generation. +""" + +from app.data.content_templates import ( + TIP_TEMPLATES, + PRODUCT_TEMPLATES, + SERVICE_TEMPLATES, + ENGAGEMENT_TEMPLATES, + THREAD_TEMPLATES, + OPTIMAL_POSTING_TIMES, + get_tip_template, + get_product_template, + get_service_template, + get_engagement_template, + get_optimal_times, +) + +__all__ = [ + "TIP_TEMPLATES", + "PRODUCT_TEMPLATES", + "SERVICE_TEMPLATES", + "ENGAGEMENT_TEMPLATES", + "THREAD_TEMPLATES", + "OPTIMAL_POSTING_TIMES", + "get_tip_template", + "get_product_template", + "get_service_template", + "get_engagement_template", + "get_optimal_times", +] diff --git a/app/data/content_templates.py b/app/data/content_templates.py new file mode 100644 index 0000000..8e048c7 --- /dev/null +++ b/app/data/content_templates.py @@ -0,0 +1,269 @@ +""" +Plantillas predefinidas para generación de contenido. +Estas plantillas guían a la IA para generar contenido consistente. +""" + +# ============================================================ +# TIPS TECH +# ============================================================ + +TIP_TEMPLATES = { + "productividad": [ + "Tip de productividad: [herramienta/técnica] puede ahorrarte [tiempo/esfuerzo] al [tarea]. #Productividad", + "¿Sabías que [atajo/función] en [app/sistema] puede [beneficio]? #TipTech", + "3 segundos que te ahorran 30 minutos: [tip rápido]. #Eficiencia", + ], + "seguridad": [ + "Tip de seguridad: [acción] para proteger [recurso]. Nunca [error común]. #Ciberseguridad", + "¿Tu [dispositivo/cuenta] está protegido? Verifica que tengas [medida de seguridad]. #SeguridadDigital", + "Error común de seguridad: [error]. Solución: [solución]. #InfoSec", + ], + "ia": [ + "Prompt tip: Para mejores resultados con IA, [técnica]. #IAparaNegocios", + "Herramienta IA del día: [herramienta] te ayuda a [beneficio]. #InteligenciaArtificial", + "Cómo uso IA para [tarea]: [paso breve]. #ProductividadIA", + ], + "programacion": [ + "Code tip: [técnica/patrón] mejora [aspecto] en tu código. #DevTips", + "Atajo en [IDE/lenguaje]: [atajo] → [resultado]. #ProgrammingTips", + "Evita este error común en [tecnología]: [error] → [solución]. #CodeQuality", + ], + "hardware": [ + "Tip de hardware: [acción] para [beneficio] en tu [equipo]. #TechTips", + "¿Tu PC está lenta? Prueba [solución]. #Mantenimiento", + "Antes de comprar [componente], verifica [aspecto]. #CompraTech", + ], + "impresion3d": [ + "Tip 3D: [configuración/técnica] mejora [aspecto] de tus impresiones. #Impresion3D", + "Material del día: [filamento] es ideal para [uso]. #3DPrinting", + "Error común en 3D: [problema] → Solución: [solución]. #Makers", + ], + "general": [ + "Tip tech del día: [consejo general]. #Tecnologia", + "¿Conocías este truco? [tip]. #TechTips", + "Tecnología que facilita tu día: [herramienta/técnica]. #Tech", + ] +} + +# ============================================================ +# PRODUCTOS +# ============================================================ + +PRODUCT_TEMPLATES = { + "computadoras": """ +🖥️ {name} + +{highlight_1} +{highlight_2} +{highlight_3} + +💰 ${price:,.0f} MXN + +{call_to_action} + +#Computadoras #Tech #Tijuana +""", + "laptops": """ +💻 {name} + +✅ {highlight_1} +✅ {highlight_2} +✅ {highlight_3} + +Precio: ${price:,.0f} MXN + +{call_to_action} + +#Laptops #Portátiles #Tech +""", + "impresoras3d": """ +🔧 {name} + +Lo que puedes crear: +• Prototipos +• Piezas personalizadas +• Figuras y modelos + +{specs_summary} + +💰 ${price:,.0f} MXN + +{call_to_action} + +#Impresion3D #3DPrinting #Makers +""", + "accesorios": """ +{emoji} {name} + +{description} + +${price:,.0f} MXN + +{call_to_action} + +#Accesorios #Tech +""", +} + +# ============================================================ +# SERVICIOS +# ============================================================ + +SERVICE_TEMPLATES = { + "consultoria": """ +🎯 {name} + +¿{pain_point}? + +Te ayudamos a: +✅ {benefit_1} +✅ {benefit_2} +✅ {benefit_3} + +{call_to_action} + +#ConsultoríaTech #TransformaciónDigital +""", + "desarrollo": """ +💻 {name} + +Desarrollamos soluciones a medida: +• {feature_1} +• {feature_2} +• {feature_3} + +{call_to_action} + +#DesarrolloSoftware #Automatización +""", + "soporte": """ +🛠️ {name} + +Problemas técnicos resueltos: +✓ {issue_1} +✓ {issue_2} +✓ {issue_3} + +{call_to_action} + +#SoporteTécnico #IT #Tijuana +""", + "capacitacion": """ +📚 {name} + +Aprende: +🎓 {topic_1} +🎓 {topic_2} +🎓 {topic_3} + +{call_to_action} + +#Capacitación #TechTraining +""", +} + +# ============================================================ +# ENGAGEMENT +# ============================================================ + +ENGAGEMENT_TEMPLATES = { + "pregunta": [ + "🤔 ¿Cuál es tu mayor reto con [tema]? Cuéntanos en los comentarios.", + "Pregunta del día: ¿[pregunta relevante]? 👇", + "Si pudieras automatizar UNA tarea de tu trabajo, ¿cuál sería? 🤖", + ], + "encuesta": [ + "¿Qué prefieres?\n\n🅰️ [Opción A]\n🅱️ [Opción B]\n\nResponde con el emoji.", + "Tu herramienta favorita para [tarea]:\n\n1️⃣ [Op1]\n2️⃣ [Op2]\n3️⃣ [Op3]\n4️⃣ Otra (comenta)", + ], + "behind_scenes": [ + "Un día normal en @ConsultoriaAS: [actividad]. ¿Qué les gustaría ver? 👀", + "Trabajando en [proyecto]. Pronto más detalles... 🔜", + ], + "celebracion": [ + "🎉 [Logro/Milestone]. ¡Gracias a todos los que confían en nosotros!", + "Otro [proyecto/cliente] satisfecho. Esto es lo que nos motiva 💪", + ], +} + +# ============================================================ +# HILOS EDUCATIVOS +# ============================================================ + +THREAD_TEMPLATES = { + "tutorial": { + "hook": "🧵 [Título llamativo en forma de promesa]\n\nEn este hilo te explico paso a paso 👇", + "content_format": "{number}/ {step_title}\n\n{explanation}", + "closing": "📌 Guarda este hilo para cuando lo necesites.\n\n¿Te fue útil? RT para que llegue a más personas.\n\n#[hashtag1] #[hashtag2]" + }, + "tips_list": { + "hook": "🧵 {count} tips de {topic} que ojalá hubiera sabido antes:\n\n👇", + "content_format": "{number}/ {tip}", + "closing": "¿Cuál agregarías tú?\n\nSíguenos para más tips como estos.\n\n#[hashtag1] #[hashtag2]" + }, + "comparison": { + "hook": "🧵 {option_a} vs {option_b}: ¿Cuál elegir?\n\nTe lo explico 👇", + "content_format": "{number}/ {aspect}:\n\n{option_a}: {value_a}\n{option_b}: {value_b}", + "closing": "Mi recomendación: {recommendation}\n\n¿Con cuál te quedas?\n\n#[hashtag1] #[hashtag2]" + }, +} + +# ============================================================ +# HORARIOS ÓPTIMOS +# ============================================================ + +OPTIMAL_POSTING_TIMES = { + "x": { + "weekday": ["09:00", "12:00", "17:00", "20:00"], + "weekend": ["10:00", "14:00", "19:00"], + "best": "12:00" + }, + "threads": { + "weekday": ["08:00", "12:00", "18:00"], + "weekend": ["10:00", "15:00"], + "best": "18:00" + }, + "instagram": { + "weekday": ["11:00", "14:00", "19:00"], + "weekend": ["10:00", "14:00", "20:00"], + "best": "19:00" + }, + "facebook": { + "weekday": ["09:00", "13:00", "16:00"], + "weekend": ["12:00", "15:00"], + "best": "13:00" + }, +} + +# ============================================================ +# HELPERS +# ============================================================ + +def get_tip_template(category: str) -> str: + """Obtener plantilla aleatoria para un tip.""" + import random + templates = TIP_TEMPLATES.get(category, TIP_TEMPLATES["general"]) + return random.choice(templates) + + +def get_product_template(category: str) -> str: + """Obtener plantilla para producto.""" + return PRODUCT_TEMPLATES.get(category, PRODUCT_TEMPLATES["accesorios"]) + + +def get_service_template(category: str) -> str: + """Obtener plantilla para servicio.""" + return SERVICE_TEMPLATES.get(category, SERVICE_TEMPLATES["consultoria"]) + + +def get_engagement_template(template_type: str) -> str: + """Obtener plantilla de engagement.""" + import random + templates = ENGAGEMENT_TEMPLATES.get(template_type, ENGAGEMENT_TEMPLATES["pregunta"]) + return random.choice(templates) + + +def get_optimal_times(platform: str, is_weekend: bool = False) -> list: + """Obtener horarios óptimos para publicar.""" + times = OPTIMAL_POSTING_TIMES.get(platform, OPTIMAL_POSTING_TIMES["x"]) + return times["weekend"] if is_weekend else times["weekday"] diff --git a/app/main.py b/app/main.py index 4b055b4..9cfd8db 100644 --- a/app/main.py +++ b/app/main.py @@ -11,7 +11,7 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware -from app.api.routes import posts, products, services, calendar, dashboard, interactions, auth, publish +from app.api.routes import posts, products, services, calendar, dashboard, interactions, auth, publish, generate from app.core.config import settings from app.core.database import engine from app.models import Base @@ -63,6 +63,7 @@ app.include_router(services.router, prefix="/api/services", tags=["Services"]) app.include_router(calendar.router, prefix="/api/calendar", tags=["Calendar"]) app.include_router(interactions.router, prefix="/api/interactions", tags=["Interactions"]) app.include_router(publish.router, prefix="/api/publish", tags=["Publish"]) +app.include_router(generate.router, prefix="/api/generate", tags=["AI Generation"]) @app.get("/api/health") diff --git a/app/services/batch_generator.py b/app/services/batch_generator.py new file mode 100644 index 0000000..75dc7af --- /dev/null +++ b/app/services/batch_generator.py @@ -0,0 +1,326 @@ +""" +Servicio de generación de contenido por lotes. +Genera múltiples posts para el calendario de contenido. +""" + +import asyncio +from datetime import datetime, timedelta +from typing import List, Dict, Optional +from dataclasses import dataclass +from enum import Enum + +from app.services.content_generator import content_generator +from app.data.content_templates import ( + get_tip_template, + get_optimal_times, + OPTIMAL_POSTING_TIMES +) +from app.core.config import settings + + +class ContentType(str, Enum): + TIP = "tip" + PRODUCT = "product" + SERVICE = "service" + ENGAGEMENT = "engagement" + THREAD = "thread" + + +@dataclass +class GeneratedPost: + """Post generado para el calendario.""" + content: str + content_type: ContentType + platform: str + scheduled_at: datetime + metadata: Dict = None + + +@dataclass +class BatchResult: + """Resultado de generación por lotes.""" + success: bool + posts: List[GeneratedPost] + errors: List[str] + total_requested: int + total_generated: int + + +class BatchGenerator: + """Generador de contenido por lotes.""" + + # Distribución de contenido por defecto (porcentaje) + DEFAULT_DISTRIBUTION = { + ContentType.TIP: 60, + ContentType.PRODUCT: 20, + ContentType.SERVICE: 15, + ContentType.ENGAGEMENT: 5, + } + + # Frecuencia por plataforma (posts por día) + DEFAULT_FREQUENCY = { + "x": 4, + "threads": 3, + "instagram": 2, + "facebook": 1, + } + + def __init__(self): + self.generator = content_generator + + async def generate_week( + self, + platforms: List[str], + start_date: Optional[datetime] = None, + distribution: Optional[Dict[ContentType, int]] = None, + frequency: Optional[Dict[str, int]] = None, + tip_categories: Optional[List[str]] = None, + ) -> BatchResult: + """ + Generar contenido para una semana. + + Args: + platforms: Lista de plataformas + start_date: Fecha de inicio (default: mañana) + distribution: Distribución de tipos de contenido + frequency: Posts por día por plataforma + tip_categories: Categorías para tips + + Returns: + BatchResult con posts generados + """ + if start_date is None: + start_date = datetime.now().replace( + hour=0, minute=0, second=0, microsecond=0 + ) + timedelta(days=1) + + return await self._generate_batch( + platforms=platforms, + start_date=start_date, + days=7, + distribution=distribution, + frequency=frequency, + tip_categories=tip_categories + ) + + async def generate_month( + self, + platforms: List[str], + start_date: Optional[datetime] = None, + distribution: Optional[Dict[ContentType, int]] = None, + frequency: Optional[Dict[str, int]] = None, + tip_categories: Optional[List[str]] = None, + ) -> BatchResult: + """Generar contenido para un mes (30 días).""" + if start_date is None: + start_date = datetime.now().replace( + hour=0, minute=0, second=0, microsecond=0 + ) + timedelta(days=1) + + return await self._generate_batch( + platforms=platforms, + start_date=start_date, + days=30, + distribution=distribution, + frequency=frequency, + tip_categories=tip_categories + ) + + async def _generate_batch( + self, + platforms: List[str], + start_date: datetime, + days: int, + distribution: Optional[Dict[ContentType, int]] = None, + frequency: Optional[Dict[str, int]] = None, + tip_categories: Optional[List[str]] = None, + ) -> BatchResult: + """Generar lote de posts.""" + distribution = distribution or self.DEFAULT_DISTRIBUTION + frequency = frequency or self.DEFAULT_FREQUENCY + tip_categories = tip_categories or [ + "productividad", "seguridad", "ia", "programacion", + "hardware", "impresion3d", "general" + ] + + posts = [] + errors = [] + total_requested = 0 + + # Calcular posts necesarios por plataforma + for platform in platforms: + posts_per_day = frequency.get(platform, 2) + total_posts = posts_per_day * days + total_requested += total_posts + + # Distribuir por tipo + posts_by_type = {} + for content_type, percentage in distribution.items(): + count = int(total_posts * percentage / 100) + posts_by_type[content_type] = count + + # Generar posts para cada tipo + current_date = start_date + day_counter = 0 + + for content_type, count in posts_by_type.items(): + for i in range(count): + # Calcular fecha y hora + day_offset = (i * days) // count + post_date = start_date + timedelta(days=day_offset) + is_weekend = post_date.weekday() >= 5 + + # Obtener hora óptima + times = get_optimal_times(platform, is_weekend) + time_index = i % len(times) + hour, minute = map(int, times[time_index].split(":")) + scheduled_at = post_date.replace(hour=hour, minute=minute) + + try: + # Generar contenido + content = await self._generate_content( + content_type=content_type, + platform=platform, + tip_categories=tip_categories + ) + + if content: + posts.append(GeneratedPost( + content=content, + content_type=content_type, + platform=platform, + scheduled_at=scheduled_at, + metadata={ + "batch_generated": True, + "category": tip_categories[i % len(tip_categories)] + if content_type == ContentType.TIP else None + } + )) + + except Exception as e: + errors.append(f"{platform}/{content_type}: {str(e)}") + + return BatchResult( + success=len(posts) > 0, + posts=posts, + errors=errors, + total_requested=total_requested, + total_generated=len(posts) + ) + + async def _generate_content( + self, + content_type: ContentType, + platform: str, + tip_categories: List[str] + ) -> Optional[str]: + """Generar contenido individual.""" + import random + + if content_type == ContentType.TIP: + category = random.choice(tip_categories) + template = get_tip_template(category) + return await self.generator.generate_tip_tech( + category=category, + platform=platform, + template=template + ) + + elif content_type == ContentType.ENGAGEMENT: + # Generar pregunta de engagement + prompts = [ + "¿Cuál es tu mayor reto tecnológico esta semana?", + "¿Qué herramienta de productividad no podrías dejar de usar?", + "¿Prefieres trabajar en oficina o remoto? ¿Por qué?", + "¿Qué tecnología te gustaría aprender este año?", + "¿Cuál es tu consejo #1 para mantenerte productivo?", + ] + return random.choice(prompts) + + # Para productos y servicios, devolver None + # (requieren datos específicos) + return None + + async def generate_tips_batch( + self, + count: int, + platform: str, + categories: Optional[List[str]] = None + ) -> List[str]: + """ + Generar múltiples tips de una vez. + + Args: + count: Cantidad de tips a generar + platform: Plataforma destino + categories: Categorías a usar (se rotarán) + + Returns: + Lista de tips generados + """ + categories = categories or [ + "productividad", "seguridad", "ia", "programacion", + "hardware", "general" + ] + + tips = [] + for i in range(count): + category = categories[i % len(categories)] + try: + tip = await self.generator.generate_tip_tech( + category=category, + platform=platform + ) + tips.append(tip) + except Exception: + pass # Ignorar errores individuales + + # Pequeña pausa para no saturar la API + if i < count - 1: + await asyncio.sleep(0.5) + + return tips + + def calculate_costs( + self, + platforms: List[str], + days: int, + frequency: Optional[Dict[str, int]] = None + ) -> Dict: + """ + Calcular costos estimados para un lote. + + Returns: + Dict con estimaciones de tokens y costos + """ + frequency = frequency or self.DEFAULT_FREQUENCY + + total_posts = sum( + frequency.get(p, 2) * days + for p in platforms + ) + + # Estimaciones basadas en análisis previo + avg_input_tokens = 650 + avg_output_tokens = 250 + + total_input = total_posts * avg_input_tokens + total_output = total_posts * avg_output_tokens + + # Precios DeepSeek + input_cost = total_input * 0.14 / 1_000_000 + output_cost = total_output * 0.28 / 1_000_000 + + return { + "total_posts": total_posts, + "estimated_input_tokens": total_input, + "estimated_output_tokens": total_output, + "estimated_cost_usd": round(input_cost + output_cost, 4), + "platforms": platforms, + "days": days, + "frequency": frequency + } + + +# Instancia global +batch_generator = BatchGenerator() diff --git a/dashboard/templates/compose.html b/dashboard/templates/compose.html index 5356da6..5890ef7 100644 --- a/dashboard/templates/compose.html +++ b/dashboard/templates/compose.html @@ -107,23 +107,23 @@
-

Asistente IA

+

Asistente IA (DeepSeek)

-

- Usa IA para generar o mejorar el contenido +

+ Verificando estado de IA...

@@ -402,17 +402,183 @@ } }); - // AI functions (placeholder - will connect to backend) + // AI functions + let aiLoading = false; + + function setAiLoading(loading) { + aiLoading = loading; + document.querySelectorAll('.ai-btn').forEach(btn => { + btn.disabled = loading; + if (loading) { + btn.classList.add('opacity-50', 'cursor-wait'); + } else { + btn.classList.remove('opacity-50', 'cursor-wait'); + } + }); + } + + async function checkAiStatus() { + try { + const response = await fetch('/api/generate/status'); + const data = await response.json(); + return data.configured && data.status === 'connected'; + } catch { + return false; + } + } + async function generateTip() { - alert('Función de generación con IA - Requiere configurar DEEPSEEK_API_KEY'); + if (aiLoading) return; + + // Show category selector + const categories = [ + 'productividad', 'seguridad', 'ia', 'programacion', + 'hardware', 'redes', 'cloud', 'automatizacion', 'impresion3d', 'general' + ]; + + const category = prompt( + 'Categoría del tip:\n\n' + categories.join(', ') + '\n\n(Enter para "general")' + ) || 'general'; + + const platform = selectedPlatforms[0] || 'x'; + + setAiLoading(true); + + try { + const response = await fetch('/api/generate/tip', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ category, platform }) + }); + + const data = await response.json(); + + if (data.success) { + document.getElementById('content').value = data.content; + updateCharCount(); + } else { + alert('Error: ' + (data.error || 'No se pudo generar')); + } + } catch (error) { + alert('Error de conexión: ' + error.message); + } finally { + setAiLoading(false); + } } async function improveContent() { - alert('Función de mejora con IA - Requiere configurar DEEPSEEK_API_KEY'); + if (aiLoading) return; + + const content = document.getElementById('content').value; + if (!content.trim()) { + alert('Primero escribe algo de contenido para mejorar'); + return; + } + + const styles = ['engaging', 'professional', 'casual', 'educational']; + const style = prompt( + 'Estilo de mejora:\n\n' + + '- engaging: Más atractivo\n' + + '- professional: Más formal\n' + + '- casual: Más cercano\n' + + '- educational: Más didáctico\n\n' + + '(Enter para "engaging")' + ) || 'engaging'; + + const platform = selectedPlatforms[0] || 'x'; + + setAiLoading(true); + + try { + const response = await fetch('/api/generate/improve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, platform, style }) + }); + + const data = await response.json(); + + if (data.success) { + document.getElementById('content').value = data.content; + updateCharCount(); + } else { + alert('Error: ' + (data.error || 'No se pudo mejorar')); + } + } catch (error) { + alert('Error de conexión: ' + error.message); + } finally { + setAiLoading(false); + } } async function adaptContent() { - alert('Función de adaptación con IA - Requiere configurar DEEPSEEK_API_KEY'); + if (aiLoading) return; + + const content = document.getElementById('content').value; + if (!content.trim()) { + alert('Primero escribe algo de contenido para adaptar'); + return; + } + + if (selectedPlatforms.length < 2) { + alert('Selecciona al menos 2 plataformas para adaptar el contenido'); + return; + } + + setAiLoading(true); + + try { + // Adaptar a cada plataforma excepto la primera (que es el contenido original) + const adaptedContent = { [selectedPlatforms[0]]: content }; + + for (let i = 1; i < selectedPlatforms.length; i++) { + const platform = selectedPlatforms[i]; + const response = await fetch('/api/generate/adapt', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, target_platform: platform }) + }); + + const data = await response.json(); + if (data.success) { + adaptedContent[platform] = data.content; + } + } + + // Mostrar resultados en preview + const previewDiv = document.getElementById('preview-content'); + let html = '

Contenido adaptado por plataforma:

'; + + for (const [platform, text] of Object.entries(adaptedContent)) { + html += ` +
+
${platform.toUpperCase()}
+

${text}

+ +
+ `; + } + + previewDiv.innerHTML = html; + document.getElementById('preview-modal').classList.remove('hidden'); + document.getElementById('preview-modal').classList.add('flex'); + + } catch (error) { + alert('Error de conexión: ' + error.message); + } finally { + setAiLoading(false); + } + } + + function useAdaptedContent(platform, btn) { + const content = decodeURIComponent(btn.dataset.content); + document.getElementById('content').value = content; + updateCharCount(); + closePreview(); } function saveDraft() { @@ -423,7 +589,7 @@ } // Load draft on page load - window.addEventListener('load', () => { + window.addEventListener('load', async () => { const draftContent = localStorage.getItem('draft_content'); const draftPlatforms = localStorage.getItem('draft_platforms'); @@ -437,6 +603,23 @@ } updateCharCount(); + + // Check AI status + const aiStatus = document.getElementById('ai-status'); + try { + const response = await fetch('/api/generate/status'); + const data = await response.json(); + + if (data.configured && data.status === 'connected') { + aiStatus.innerHTML = 'IA conectada y lista'; + } else if (data.configured) { + aiStatus.innerHTML = 'IA configurada pero con error: ' + (data.error || 'desconocido') + ''; + } else { + aiStatus.innerHTML = 'IA no configurada. Agrega DEEPSEEK_API_KEY en .env'; + } + } catch (error) { + aiStatus.innerHTML = 'No se pudo verificar estado de IA'; + } });