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:
551
app/api/routes/generate.py
Normal file
551
app/api/routes/generate.py
Normal 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
|
||||
Reference in New Issue
Block a user