Major improvements to AI content generation: ## New Components (app/services/ai/) - PromptLibrary: YAML-based prompt templates with inheritance - ContextEngine: Anti-repetition and best performers tracking - ContentGeneratorV2: Enhanced generation with dynamic parameters - PlatformAdapter: Platform-specific content adaptation - ContentValidator: AI-powered quality scoring (0-100) ## Prompt Library (app/prompts/) - 3 personalities: default, educational, promotional - 5 templates: tip_tech, product_post, service_post, thread, response - 4 platform configs: x, threads, instagram, facebook - Few-shot examples by category: ia, productividad, seguridad ## Database Changes - New table: content_memory (tracks generated content) - New columns in posts: quality_score, score_breakdown, generation_attempts ## New API Endpoints (/api/v2/generate/) - POST /generate - Generation with quality check - POST /generate/batch - Batch generation - POST /quality/evaluate - Evaluate content quality - GET /templates, /personalities, /platforms - List configs ## Celery Tasks - update_engagement_scores (every 6h) - cleanup_old_memory (monthly) - refresh_best_posts_yaml (weekly) ## Tests - Comprehensive tests for all AI engine components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
494 lines
15 KiB
Python
494 lines
15 KiB
Python
"""
|
|
ContentGeneratorV2 - Generador de contenido mejorado con DeepSeek.
|
|
|
|
Este módulo maneja:
|
|
- Generación de contenido usando prompts de la biblioteca
|
|
- Integración con Context Engine para anti-repetición
|
|
- Soporte para few-shot learning
|
|
- Parámetros dinámicos por tipo de contenido
|
|
"""
|
|
|
|
import json
|
|
from typing import Optional, List, Dict, Any
|
|
from openai import OpenAI
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from app.services.ai.prompt_library import prompt_library, PromptLibrary
|
|
from app.services.ai.context_engine import context_engine, ContextEngine
|
|
from app.services.ai.platform_adapter import platform_adapter, PlatformAdapter
|
|
|
|
|
|
class ContentGeneratorV2:
|
|
"""
|
|
Generador de contenido v2.0 con motor modular.
|
|
|
|
Mejoras sobre v1:
|
|
- Prompts externalizados en YAML
|
|
- Anti-repetición via Context Engine
|
|
- Few-shot learning con posts exitosos
|
|
- Adaptación automática por plataforma
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
prompt_lib: Optional[PromptLibrary] = None,
|
|
ctx_engine: Optional[ContextEngine] = None,
|
|
plt_adapter: Optional[PlatformAdapter] = None
|
|
):
|
|
"""
|
|
Inicializar el generador.
|
|
|
|
Args:
|
|
prompt_lib: PromptLibrary (usa global si no se especifica)
|
|
ctx_engine: ContextEngine (usa global si no se especifica)
|
|
plt_adapter: PlatformAdapter (usa global si no se especifica)
|
|
"""
|
|
self._client = None
|
|
self.model = "deepseek-chat"
|
|
|
|
self.prompt_lib = prompt_lib or prompt_library
|
|
self.ctx_engine = ctx_engine or context_engine
|
|
self.plt_adapter = plt_adapter or platform_adapter
|
|
|
|
@property
|
|
def client(self) -> OpenAI:
|
|
"""Lazy initialization del cliente OpenAI/DeepSeek."""
|
|
if self._client is None:
|
|
if not settings.DEEPSEEK_API_KEY:
|
|
raise ValueError(
|
|
"DEEPSEEK_API_KEY no configurada. "
|
|
"Configura la variable de entorno."
|
|
)
|
|
self._client = OpenAI(
|
|
api_key=settings.DEEPSEEK_API_KEY,
|
|
base_url=settings.DEEPSEEK_BASE_URL
|
|
)
|
|
return self._client
|
|
|
|
# === Generación Principal ===
|
|
|
|
async def generate(
|
|
self,
|
|
template_name: str,
|
|
variables: Dict[str, Any],
|
|
platform: str,
|
|
db: Optional[Session] = None,
|
|
use_context: bool = True,
|
|
use_few_shot: bool = True,
|
|
personality: Optional[str] = None,
|
|
temperature_override: Optional[float] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Generar contenido usando un template.
|
|
|
|
Args:
|
|
template_name: Nombre del template (tip_tech, product_post, etc.)
|
|
variables: Variables para el template
|
|
platform: Plataforma destino
|
|
db: Sesión de DB (requerida para context)
|
|
use_context: Si usar anti-repetición
|
|
use_few_shot: Si usar ejemplos de posts exitosos
|
|
personality: Override de personalidad
|
|
temperature_override: Override de temperature
|
|
|
|
Returns:
|
|
Dict con:
|
|
- content: Contenido generado
|
|
- adapted_content: Contenido adaptado a plataforma
|
|
- metadata: Info de generación
|
|
"""
|
|
# 1. Renderizar template
|
|
rendered = self.prompt_lib.render_template(
|
|
template_name,
|
|
variables,
|
|
personality or "default"
|
|
)
|
|
|
|
system_prompt = rendered["system_prompt"]
|
|
user_prompt = rendered["user_prompt"]
|
|
params = rendered["parameters"]
|
|
|
|
# 2. Agregar contexto de exclusión si hay DB
|
|
if db and use_context:
|
|
exclusion_context = self.ctx_engine.build_exclusion_context(
|
|
db,
|
|
template_name,
|
|
variables.get("category")
|
|
)
|
|
if exclusion_context:
|
|
user_prompt += f"\n\n{exclusion_context}"
|
|
|
|
# 3. Agregar few-shot examples si hay DB
|
|
if db and use_few_shot:
|
|
few_shot_context = self.ctx_engine.build_few_shot_context(
|
|
db,
|
|
template_name,
|
|
platform
|
|
)
|
|
if few_shot_context:
|
|
user_prompt += f"\n\n{few_shot_context}"
|
|
|
|
# También agregar ejemplos del YAML si hay categoría
|
|
category = variables.get("category")
|
|
if category:
|
|
yaml_examples = self.prompt_lib.get_few_shot_examples(
|
|
template_name,
|
|
category,
|
|
max_examples=2
|
|
)
|
|
if yaml_examples:
|
|
user_prompt += "\n\nEJEMPLOS DE REFERENCIA:\n"
|
|
for ex in yaml_examples:
|
|
user_prompt += f"\n---\n{ex}\n---"
|
|
|
|
# 4. Agregar límites de plataforma
|
|
limits = self.plt_adapter.get_limits(platform)
|
|
user_prompt += f"\n\nPLATAFORMA: {platform}"
|
|
user_prompt += f"\nLÍMITE DE CARACTERES: {limits.get('max_characters', 500)}"
|
|
user_prompt += f"\nMÁXIMO HASHTAGS: {limits.get('max_hashtags', 5)}"
|
|
|
|
# 5. Llamar a DeepSeek
|
|
temperature = temperature_override or params.get("temperature", 0.7)
|
|
max_tokens = params.get("max_tokens", 400)
|
|
|
|
response = self.client.chat.completions.create(
|
|
model=self.model,
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
max_tokens=max_tokens,
|
|
temperature=temperature
|
|
)
|
|
|
|
content = response.choices[0].message.content.strip()
|
|
|
|
# 6. Adaptar a plataforma
|
|
adapted = self.plt_adapter.adapt(content, platform)
|
|
|
|
return {
|
|
"content": content,
|
|
"adapted_content": adapted.content,
|
|
"truncated": adapted.truncated,
|
|
"metadata": {
|
|
"template": template_name,
|
|
"personality": rendered["personality"],
|
|
"platform": platform,
|
|
"temperature": temperature,
|
|
"tokens_used": response.usage.total_tokens if response.usage else None,
|
|
"changes_made": adapted.changes_made,
|
|
}
|
|
}
|
|
|
|
# === Métodos de Conveniencia ===
|
|
|
|
async def generate_tip(
|
|
self,
|
|
category: str,
|
|
platform: str,
|
|
db: Optional[Session] = None,
|
|
difficulty: str = "principiante",
|
|
**kwargs
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Generar un tip de tecnología.
|
|
|
|
Args:
|
|
category: Categoría del tip (ia, productividad, seguridad, etc.)
|
|
platform: Plataforma destino
|
|
db: Sesión de DB
|
|
difficulty: Nivel de dificultad
|
|
**kwargs: Argumentos adicionales para generate()
|
|
|
|
Returns:
|
|
Dict con contenido generado
|
|
"""
|
|
return await self.generate(
|
|
template_name="tip_tech",
|
|
variables={
|
|
"category": category,
|
|
"difficulty_level": difficulty,
|
|
"target_audience": "profesionales tech"
|
|
},
|
|
platform=platform,
|
|
db=db,
|
|
**kwargs
|
|
)
|
|
|
|
async def generate_product_post(
|
|
self,
|
|
product: Dict[str, Any],
|
|
platform: str,
|
|
db: Optional[Session] = None,
|
|
**kwargs
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Generar post para un producto.
|
|
|
|
Args:
|
|
product: Dict con datos del producto
|
|
platform: Plataforma destino
|
|
db: Sesión de DB
|
|
**kwargs: Argumentos adicionales
|
|
|
|
Returns:
|
|
Dict con contenido generado
|
|
"""
|
|
return await self.generate(
|
|
template_name="product_post",
|
|
variables={
|
|
"product_name": product["name"],
|
|
"product_description": product.get("description", ""),
|
|
"price": product["price"],
|
|
"category": product["category"],
|
|
"specs": json.dumps(product.get("specs", {}), ensure_ascii=False),
|
|
"highlights": ", ".join(product.get("highlights", [])),
|
|
},
|
|
platform=platform,
|
|
db=db,
|
|
personality="promotional",
|
|
**kwargs
|
|
)
|
|
|
|
async def generate_service_post(
|
|
self,
|
|
service: Dict[str, Any],
|
|
platform: str,
|
|
db: Optional[Session] = None,
|
|
**kwargs
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Generar post para un servicio.
|
|
|
|
Args:
|
|
service: Dict con datos del servicio
|
|
platform: Plataforma destino
|
|
db: Sesión de DB
|
|
**kwargs: Argumentos adicionales
|
|
|
|
Returns:
|
|
Dict con contenido generado
|
|
"""
|
|
return await self.generate(
|
|
template_name="service_post",
|
|
variables={
|
|
"service_name": service["name"],
|
|
"service_description": service.get("description", ""),
|
|
"category": service["category"],
|
|
"target_sectors": ", ".join(service.get("target_sectors", [])),
|
|
"benefits": ", ".join(service.get("benefits", [])),
|
|
"call_to_action": service.get("call_to_action", "Contáctanos"),
|
|
},
|
|
platform=platform,
|
|
db=db,
|
|
personality="promotional",
|
|
**kwargs
|
|
)
|
|
|
|
async def generate_thread(
|
|
self,
|
|
topic: str,
|
|
num_posts: int = 5,
|
|
db: Optional[Session] = None,
|
|
**kwargs
|
|
) -> List[str]:
|
|
"""
|
|
Generar un hilo educativo.
|
|
|
|
Args:
|
|
topic: Tema del hilo
|
|
num_posts: Número de posts en el hilo
|
|
db: Sesión de DB
|
|
**kwargs: Argumentos adicionales
|
|
|
|
Returns:
|
|
Lista de posts del hilo
|
|
"""
|
|
result = await self.generate(
|
|
template_name="thread",
|
|
variables={
|
|
"topic": topic,
|
|
"num_posts": num_posts,
|
|
"depth": "intermedio",
|
|
},
|
|
platform="x", # Threads son para X principalmente
|
|
db=db,
|
|
personality="educational",
|
|
**kwargs
|
|
)
|
|
|
|
# Parsear posts del hilo
|
|
content = result["content"]
|
|
posts = [p.strip() for p in content.split("\n\n") if p.strip()]
|
|
|
|
# Si no se separaron bien, intentar por números
|
|
if len(posts) < num_posts - 1:
|
|
import re
|
|
posts = re.split(r"\n(?=\d+[/\)])", content)
|
|
posts = [p.strip() for p in posts if p.strip()]
|
|
|
|
return posts
|
|
|
|
async def generate_response(
|
|
self,
|
|
interaction_content: str,
|
|
interaction_type: str,
|
|
sentiment: str = "neutral",
|
|
context: Optional[str] = None,
|
|
**kwargs
|
|
) -> List[str]:
|
|
"""
|
|
Generar sugerencias de respuesta.
|
|
|
|
Args:
|
|
interaction_content: Contenido de la interacción
|
|
interaction_type: Tipo (comment, mention, reply, dm)
|
|
sentiment: Sentimiento detectado
|
|
context: Contexto adicional
|
|
**kwargs: Argumentos adicionales
|
|
|
|
Returns:
|
|
Lista de 3 opciones de respuesta
|
|
"""
|
|
result = await self.generate(
|
|
template_name="response",
|
|
variables={
|
|
"interaction_content": interaction_content,
|
|
"interaction_type": interaction_type,
|
|
"sentiment": sentiment,
|
|
"context": context or "Sin contexto adicional",
|
|
},
|
|
platform="x",
|
|
use_context=False,
|
|
use_few_shot=False,
|
|
**kwargs
|
|
)
|
|
|
|
# Parsear las 3 opciones
|
|
content = result["content"]
|
|
lines = [l.strip() for l in content.split("\n") if l.strip()]
|
|
|
|
# Limpiar numeración
|
|
responses = []
|
|
for line in lines:
|
|
if line and line[0].isdigit() and len(line) > 2:
|
|
# Remover "1.", "1)", "1:" etc.
|
|
clean = line[2:].strip() if line[1] in ".):,-" else line
|
|
if clean:
|
|
responses.append(clean)
|
|
elif line and not line[0].isdigit():
|
|
responses.append(line)
|
|
|
|
return responses[:3]
|
|
|
|
async def adapt_content(
|
|
self,
|
|
content: str,
|
|
source_platform: str,
|
|
target_platform: str
|
|
) -> str:
|
|
"""
|
|
Adaptar contenido existente a otra plataforma usando IA.
|
|
|
|
Args:
|
|
content: Contenido original
|
|
source_platform: Plataforma de origen
|
|
target_platform: Plataforma destino
|
|
|
|
Returns:
|
|
Contenido adaptado
|
|
"""
|
|
# Generar prompt de adaptación
|
|
adaptation_prompt = self.plt_adapter.get_adaptation_prompt(
|
|
content, source_platform, target_platform
|
|
)
|
|
|
|
system_prompt = self.prompt_lib.get_system_prompt()
|
|
|
|
response = self.client.chat.completions.create(
|
|
model=self.model,
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": adaptation_prompt}
|
|
],
|
|
max_tokens=500,
|
|
temperature=0.6
|
|
)
|
|
|
|
return response.choices[0].message.content.strip()
|
|
|
|
async def generate_for_all_platforms(
|
|
self,
|
|
template_name: str,
|
|
variables: Dict[str, Any],
|
|
platforms: List[str],
|
|
db: Optional[Session] = None,
|
|
**kwargs
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
Generar contenido para múltiples plataformas.
|
|
|
|
Genera una versión base y la adapta para cada plataforma.
|
|
|
|
Args:
|
|
template_name: Nombre del template
|
|
variables: Variables del template
|
|
platforms: Lista de plataformas
|
|
db: Sesión de DB
|
|
**kwargs: Argumentos adicionales
|
|
|
|
Returns:
|
|
Dict de plataforma -> resultado de generación
|
|
"""
|
|
results = {}
|
|
|
|
# Generar versión base (para la plataforma con más espacio)
|
|
base_platform = "instagram" # Más espacio = más contexto
|
|
if "facebook" in platforms:
|
|
base_platform = "facebook"
|
|
|
|
base_result = await self.generate(
|
|
template_name=template_name,
|
|
variables=variables,
|
|
platform=base_platform,
|
|
db=db,
|
|
**kwargs
|
|
)
|
|
|
|
base_content = base_result["content"]
|
|
results[base_platform] = base_result
|
|
|
|
# Adaptar para otras plataformas
|
|
for platform in platforms:
|
|
if platform == base_platform:
|
|
continue
|
|
|
|
# Usar adaptación con IA para mejor calidad
|
|
adapted_content = await self.adapt_content(
|
|
base_content,
|
|
base_platform,
|
|
platform
|
|
)
|
|
|
|
# Validar adaptación
|
|
adapted = self.plt_adapter.adapt(adapted_content, platform)
|
|
|
|
results[platform] = {
|
|
"content": adapted_content,
|
|
"adapted_content": adapted.content,
|
|
"truncated": adapted.truncated,
|
|
"metadata": {
|
|
"template": template_name,
|
|
"platform": platform,
|
|
"adapted_from": base_platform,
|
|
"changes_made": adapted.changes_made,
|
|
}
|
|
}
|
|
|
|
return results
|
|
|
|
|
|
# Instancia global
|
|
content_generator_v2 = ContentGeneratorV2()
|