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 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:49:49 +00:00
parent 3caf2a67fb
commit 964e38564a
6 changed files with 1376 additions and 15 deletions

551
app/api/routes/generate.py Normal file
View File

@@ -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