Files
social-media-automation/app/services/ai/platform_adapter.py
Consultoría AS 11b0ba46fa feat: Add Content Generation Engine v2 with quality scoring
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>
2026-01-28 20:55:28 +00:00

375 lines
11 KiB
Python

"""
PlatformAdapter - Adapta contenido según reglas de cada plataforma.
Este módulo maneja:
- Transformación de contenido base a formato específico de plataforma
- Ajuste de longitud, tono, hashtags y formato
- Validación de límites de plataforma
"""
from typing import Dict, Optional, Any, List
from dataclasses import dataclass
from app.services.ai.prompt_library import prompt_library
@dataclass
class AdaptedContent:
"""Contenido adaptado para una plataforma."""
content: str
platform: str
original_content: str
truncated: bool
hashtags_adjusted: bool
changes_made: List[str]
class PlatformAdapter:
"""
Adaptador de contenido por plataforma.
Transforma contenido generado según las reglas específicas
de cada red social (X, Threads, Instagram, Facebook).
"""
def __init__(self):
"""Inicializar el adaptador."""
self.prompt_lib = prompt_library
# Límites rápidos (fallback si no hay YAML)
self._default_limits = {
"x": {"max_characters": 280, "max_hashtags": 2},
"threads": {"max_characters": 500, "max_hashtags": 5},
"instagram": {"max_characters": 2200, "max_hashtags": 10},
"facebook": {"max_characters": 2000, "max_hashtags": 3},
}
def get_limits(self, platform: str) -> Dict[str, int]:
"""
Obtener límites de una plataforma.
Args:
platform: Nombre de la plataforma
Returns:
Dict con límites
"""
try:
return self.prompt_lib.get_platform_limits(platform)
except FileNotFoundError:
return self._default_limits.get(platform, self._default_limits["x"])
def get_config(self, platform: str) -> Dict:
"""
Obtener configuración completa de una plataforma.
Args:
platform: Nombre de la plataforma
Returns:
Dict con configuración
"""
try:
return self.prompt_lib.get_platform_config(platform)
except FileNotFoundError:
return {"platform": platform, "limits": self.get_limits(platform)}
# === Adaptación Principal ===
def adapt(
self,
content: str,
platform: str,
preserve_hashtags: bool = True
) -> AdaptedContent:
"""
Adaptar contenido a una plataforma específica.
Esta es adaptación basada en reglas (sin IA).
Para adaptación con IA, usar adapt_with_ai().
Args:
content: Contenido a adaptar
platform: Plataforma destino
preserve_hashtags: Si preservar hashtags existentes
Returns:
AdaptedContent con el contenido adaptado
"""
limits = self.get_limits(platform)
config = self.get_config(platform)
changes = []
adapted = content
truncated = False
hashtags_adjusted = False
# 1. Extraer y procesar hashtags
main_content, hashtags = self._extract_hashtags(adapted)
# 2. Ajustar hashtags según plataforma
max_hashtags = limits.get("max_hashtags", 5)
if len(hashtags) > max_hashtags:
hashtags = hashtags[:max_hashtags]
hashtags_adjusted = True
changes.append(f"Hashtags reducidos a {max_hashtags}")
# 3. Verificar y ajustar longitud
max_chars = limits.get("max_characters", 2000)
# Calcular espacio para hashtags
hashtag_space = len(" ".join(hashtags)) + 2 if hashtags else 0
available_for_content = max_chars - hashtag_space
if len(main_content) > available_for_content:
main_content = self._smart_truncate(main_content, available_for_content)
truncated = True
changes.append(f"Contenido truncado a {available_for_content} caracteres")
# 4. Aplicar formato de plataforma
main_content = self._apply_platform_formatting(main_content, platform, config)
if main_content != content.replace("#", "").strip():
changes.append("Formato ajustado para plataforma")
# 5. Recombinar con hashtags
if hashtags and preserve_hashtags:
adapted = main_content + "\n\n" + " ".join(hashtags)
else:
adapted = main_content
return AdaptedContent(
content=adapted.strip(),
platform=platform,
original_content=content,
truncated=truncated,
hashtags_adjusted=hashtags_adjusted,
changes_made=changes,
)
def adapt_for_all_platforms(
self,
content: str,
platforms: List[str]
) -> Dict[str, AdaptedContent]:
"""
Adaptar contenido para múltiples plataformas.
Args:
content: Contenido base
platforms: Lista de plataformas
Returns:
Dict de plataforma -> AdaptedContent
"""
return {
platform: self.adapt(content, platform)
for platform in platforms
}
# === Helpers ===
def _extract_hashtags(self, content: str) -> tuple[str, List[str]]:
"""
Extraer hashtags del contenido.
Returns:
Tuple de (contenido sin hashtags, lista de hashtags)
"""
import re
hashtags = re.findall(r"#\w+", content)
# Remover hashtags del contenido
main_content = re.sub(r"\s*#\w+", "", content).strip()
return main_content, hashtags
def _smart_truncate(self, content: str, max_length: int) -> str:
"""
Truncar contenido de forma inteligente.
Intenta cortar en un punto natural (oración, párrafo).
Args:
content: Contenido a truncar
max_length: Longitud máxima
Returns:
Contenido truncado
"""
if len(content) <= max_length:
return content
# Reservar espacio para "..."
target_length = max_length - 3
# Intentar cortar en punto/salto de línea
truncated = content[:target_length]
# Buscar último punto o salto de línea
last_period = truncated.rfind(".")
last_newline = truncated.rfind("\n")
cut_point = max(last_period, last_newline)
if cut_point > target_length * 0.5: # Solo si no perdemos mucho
return content[:cut_point + 1].strip()
# Si no hay buen punto de corte, cortar en última palabra completa
last_space = truncated.rfind(" ")
if last_space > target_length * 0.7:
return content[:last_space].strip() + "..."
return truncated + "..."
def _apply_platform_formatting(
self,
content: str,
platform: str,
config: Dict
) -> str:
"""
Aplicar formato específico de plataforma.
Args:
content: Contenido a formatear
platform: Plataforma
config: Configuración de la plataforma
Returns:
Contenido formateado
"""
formatting = config.get("formatting", {})
# Aplicar estilo de bullets si está configurado
bullet_style = formatting.get("bullet_style", "")
# Reemplazar bullets genéricos
content = content.replace("", f"{bullet_style} ")
content = content.replace("- ", f"{bullet_style} ")
# Ajustar saltos de línea según plataforma
if platform == "x":
# X: menos saltos de línea, más compacto
content = self._compact_line_breaks(content)
elif platform == "instagram":
# Instagram: más saltos para legibilidad
content = self._expand_line_breaks(content)
return content
def _compact_line_breaks(self, content: str) -> str:
"""Reducir saltos de línea múltiples a uno."""
import re
return re.sub(r"\n{3,}", "\n\n", content)
def _expand_line_breaks(self, content: str) -> str:
"""Asegurar separación entre párrafos."""
import re
# Reemplazar un salto por dos donde hay oraciones
return re.sub(r"\.(\n)(?=[A-Z])", ".\n\n", content)
# === Validación ===
def validate_for_platform(
self,
content: str,
platform: str
) -> Dict[str, Any]:
"""
Validar que contenido cumple con límites de plataforma.
Args:
content: Contenido a validar
platform: Plataforma
Returns:
Dict con resultado de validación
"""
limits = self.get_limits(platform)
_, hashtags = self._extract_hashtags(content)
issues = []
# Verificar longitud
max_chars = limits.get("max_characters", 2000)
if len(content) > max_chars:
issues.append({
"type": "length",
"message": f"Contenido excede límite ({len(content)}/{max_chars})",
"severity": "error"
})
# Verificar hashtags
max_hashtags = limits.get("max_hashtags", 10)
if len(hashtags) > max_hashtags:
issues.append({
"type": "hashtags",
"message": f"Demasiados hashtags ({len(hashtags)}/{max_hashtags})",
"severity": "warning"
})
return {
"valid": len([i for i in issues if i["severity"] == "error"]) == 0,
"issues": issues,
"stats": {
"characters": len(content),
"max_characters": max_chars,
"hashtags": len(hashtags),
"max_hashtags": max_hashtags,
}
}
# === Generación de Prompts de Adaptación ===
def get_adaptation_prompt(
self,
content: str,
source_platform: str,
target_platform: str
) -> str:
"""
Generar prompt para adaptar contenido con IA.
Args:
content: Contenido original
source_platform: Plataforma de origen
target_platform: Plataforma destino
Returns:
Prompt para enviar a la IA
"""
target_config = self.get_config(target_platform)
limits = self.get_limits(target_platform)
adaptation_rules = target_config.get("adaptation_rules", "")
tone_style = target_config.get("tone", {}).get("style", "neutral")
prompt = f"""Adapta este contenido de {source_platform} para {target_platform}:
CONTENIDO ORIGINAL:
{content}
LÍMITES DE {target_platform.upper()}:
- Máximo caracteres: {limits.get('max_characters', 2000)}
- Máximo hashtags: {limits.get('max_hashtags', 5)}
TONO PARA {target_platform.upper()}: {tone_style}
REGLAS DE ADAPTACIÓN:
{adaptation_rules}
IMPORTANTE:
- Mantén la esencia y mensaje principal
- Adapta el tono según la plataforma
- Ajusta hashtags apropiadamente
- NO inventes información nueva
Responde SOLO con el contenido adaptado, sin explicaciones."""
return prompt
# Instancia global
platform_adapter = PlatformAdapter()