Files
social-media-automation/app/models/post.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

159 lines
5.7 KiB
Python

"""
Modelo de Post - Posts generados y programados.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON, Enum
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm import relationship
import enum
from app.core.database import Base
class PostStatus(enum.Enum):
"""Estados posibles de un post."""
DRAFT = "draft"
PENDING_APPROVAL = "pending_approval"
APPROVED = "approved"
SCHEDULED = "scheduled"
PUBLISHING = "publishing"
PUBLISHED = "published"
FAILED = "failed"
CANCELLED = "cancelled"
class ContentType(enum.Enum):
"""Tipos de contenido."""
TIP_TECH = "tip_tech"
DATO_CURIOSO = "dato_curioso"
FRASE_MOTIVACIONAL = "frase_motivacional"
EFEMERIDE = "efemeride"
PRODUCTO = "producto"
SERVICIO = "servicio"
HILO_EDUCATIVO = "hilo_educativo"
CASO_EXITO = "caso_exito"
PROMOCION = "promocion"
ANUNCIO = "anuncio"
MANUAL = "manual"
class Post(Base):
"""Modelo para posts de redes sociales."""
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
# Contenido
content = Column(Text, nullable=False)
content_type = Column(String(50), nullable=False, index=True)
# Contenido adaptado por plataforma (opcional)
content_x = Column(Text, nullable=True) # Versión para X (280 chars)
content_threads = Column(Text, nullable=True)
content_instagram = Column(Text, nullable=True)
content_facebook = Column(Text, nullable=True)
# Plataformas destino
platforms = Column(ARRAY(String), nullable=False)
# Ejemplo: ["x", "threads", "instagram", "facebook"]
# Estado y programación
status = Column(String(50), default="draft", index=True)
scheduled_at = Column(DateTime, nullable=True, index=True)
published_at = Column(DateTime, nullable=True)
# Imagen
image_url = Column(String(500), nullable=True)
image_template_id = Column(Integer, ForeignKey("image_templates.id"), nullable=True)
# IDs de publicación en cada plataforma
platform_post_ids = Column(JSON, nullable=True)
# Ejemplo: {"x": "123456", "instagram": "789012", ...}
# Errores de publicación
error_message = Column(Text, nullable=True)
retry_count = Column(Integer, default=0)
# Aprobación
approval_required = Column(Boolean, default=False)
approved_by = Column(String(100), nullable=True)
approved_at = Column(DateTime, nullable=True)
# Relaciones con contenido fuente
product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
service_id = Column(Integer, ForeignKey("services.id"), nullable=True)
tip_template_id = Column(Integer, ForeignKey("tip_templates.id"), nullable=True)
# Metadatos
hashtags = Column(ARRAY(String), nullable=True)
mentions = Column(ARRAY(String), nullable=True)
# Métricas (actualizadas después de publicar)
metrics = Column(JSON, nullable=True)
# Ejemplo: {"likes": 10, "retweets": 5, "comments": 3}
# A/B Testing
ab_test_id = Column(Integer, ForeignKey("ab_tests.id"), nullable=True, index=True)
# Recycling
is_recyclable = Column(Boolean, default=True)
recycled_from_id = Column(Integer, ForeignKey("posts.id"), nullable=True)
recycle_count = Column(Integer, default=0) # Times this post has been recycled
# AI Generation Quality
quality_score = Column(Integer, nullable=True, index=True) # 0-100 score from validator
score_breakdown = Column(JSON, nullable=True) # Detailed scoring breakdown
generation_attempts = Column(Integer, default=1) # Times regenerated before acceptance
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
metrics_history = relationship("PostMetrics", back_populates="post", cascade="all, delete-orphan")
def __repr__(self):
return f"<Post {self.id} - {self.status}>"
def to_dict(self):
"""Convertir a diccionario."""
return {
"id": self.id,
"content": self.content,
"content_type": self.content_type,
"content_x": self.content_x,
"content_threads": self.content_threads,
"content_instagram": self.content_instagram,
"content_facebook": self.content_facebook,
"platforms": self.platforms,
"status": self.status,
"scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None,
"published_at": self.published_at.isoformat() if self.published_at else None,
"image_url": self.image_url,
"platform_post_ids": self.platform_post_ids,
"error_message": self.error_message,
"approval_required": self.approval_required,
"hashtags": self.hashtags,
"metrics": self.metrics,
"created_at": self.created_at.isoformat() if self.created_at else None,
"ab_test_id": self.ab_test_id,
"is_recyclable": self.is_recyclable,
"recycled_from_id": self.recycled_from_id,
"recycle_count": self.recycle_count,
"quality_score": self.quality_score,
"score_breakdown": self.score_breakdown,
"generation_attempts": self.generation_attempts
}
def get_content_for_platform(self, platform: str) -> str:
"""Obtener contenido adaptado para una plataforma específica."""
platform_content = {
"x": self.content_x,
"threads": self.content_threads,
"instagram": self.content_instagram,
"facebook": self.content_facebook
}
return platform_content.get(platform) or self.content