feat: Auto-adapt content to platform limits when creating posts
- Add adapt_with_ai() method to PlatformAdapter that uses DeepSeek to condense content when it exceeds platform character limits - Add adapt_for_all_platforms_smart() for batch adaptation - Modify create_post endpoint to auto-generate content_x, content_threads, content_instagram, content_facebook with adapted versions - Modify update_post to re-adapt content when main content changes - If content fits within limit, use as-is (no AI call) - If content exceeds limit, AI condenses while preserving message - Fallback to rule-based truncation if DeepSeek API unavailable Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.models.post import Post
|
from app.models.post import Post
|
||||||
|
from app.services.ai.platform_adapter import platform_adapter
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -122,7 +123,14 @@ async def get_post(post_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.post("/", response_model=PostResponse)
|
@router.post("/", response_model=PostResponse)
|
||||||
async def create_post(post_data: PostCreate, db: Session = Depends(get_db)):
|
async def create_post(post_data: PostCreate, db: Session = Depends(get_db)):
|
||||||
"""Crear un nuevo post."""
|
"""Crear un nuevo post con auto-adaptación de contenido por plataforma."""
|
||||||
|
|
||||||
|
# Adaptar contenido para cada plataforma
|
||||||
|
adapted_content = await platform_adapter.adapt_for_all_platforms_smart(
|
||||||
|
post_data.content,
|
||||||
|
post_data.platforms
|
||||||
|
)
|
||||||
|
|
||||||
post = Post(
|
post = Post(
|
||||||
content=post_data.content,
|
content=post_data.content,
|
||||||
content_type=post_data.content_type,
|
content_type=post_data.content_type,
|
||||||
@@ -131,7 +139,12 @@ async def create_post(post_data: PostCreate, db: Session = Depends(get_db)):
|
|||||||
image_url=post_data.image_url,
|
image_url=post_data.image_url,
|
||||||
approval_required=post_data.approval_required,
|
approval_required=post_data.approval_required,
|
||||||
hashtags=post_data.hashtags,
|
hashtags=post_data.hashtags,
|
||||||
status="pending_approval" if post_data.approval_required else "scheduled"
|
status="pending_approval" if post_data.approval_required else "scheduled",
|
||||||
|
# Contenido adaptado por plataforma
|
||||||
|
content_x=adapted_content.get("x"),
|
||||||
|
content_threads=adapted_content.get("threads"),
|
||||||
|
content_instagram=adapted_content.get("instagram"),
|
||||||
|
content_facebook=adapted_content.get("facebook"),
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(post)
|
db.add(post)
|
||||||
@@ -143,12 +156,30 @@ async def create_post(post_data: PostCreate, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.put("/{post_id}")
|
@router.put("/{post_id}")
|
||||||
async def update_post(post_id: int, post_data: PostUpdate, db: Session = Depends(get_db)):
|
async def update_post(post_id: int, post_data: PostUpdate, db: Session = Depends(get_db)):
|
||||||
"""Actualizar un post."""
|
"""Actualizar un post con re-adaptación automática si cambia el contenido."""
|
||||||
post = db.query(Post).filter(Post.id == post_id).first()
|
post = db.query(Post).filter(Post.id == post_id).first()
|
||||||
if not post:
|
if not post:
|
||||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||||
|
|
||||||
update_data = post_data.dict(exclude_unset=True)
|
update_data = post_data.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
# Si cambia el contenido principal, re-adaptar para todas las plataformas
|
||||||
|
if "content" in update_data:
|
||||||
|
platforms = update_data.get("platforms", post.platforms)
|
||||||
|
adapted_content = await platform_adapter.adapt_for_all_platforms_smart(
|
||||||
|
update_data["content"],
|
||||||
|
platforms
|
||||||
|
)
|
||||||
|
# Solo actualizar campos de plataforma si no vienen explícitos en la request
|
||||||
|
if "content_x" not in update_data and "x" in platforms:
|
||||||
|
update_data["content_x"] = adapted_content.get("x")
|
||||||
|
if "content_threads" not in update_data and "threads" in platforms:
|
||||||
|
update_data["content_threads"] = adapted_content.get("threads")
|
||||||
|
if "content_instagram" not in update_data and "instagram" in platforms:
|
||||||
|
update_data["content_instagram"] = adapted_content.get("instagram")
|
||||||
|
if "content_facebook" not in update_data and "facebook" in platforms:
|
||||||
|
update_data["content_facebook"] = adapted_content.get("facebook")
|
||||||
|
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
setattr(post, field, value)
|
setattr(post, field, value)
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,112 @@ Responde SOLO con el contenido adaptado, sin explicaciones."""
|
|||||||
|
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
|
# === Adaptación con IA ===
|
||||||
|
|
||||||
|
async def adapt_with_ai(
|
||||||
|
self,
|
||||||
|
content: str,
|
||||||
|
platform: str
|
||||||
|
) -> AdaptedContent:
|
||||||
|
"""
|
||||||
|
Adaptar contenido usando IA si excede el límite.
|
||||||
|
|
||||||
|
Si el contenido cabe en el límite, lo retorna tal cual.
|
||||||
|
Si excede, usa DeepSeek para condensar manteniendo el mensaje.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Contenido a adaptar
|
||||||
|
platform: Plataforma destino
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AdaptedContent con el contenido adaptado
|
||||||
|
"""
|
||||||
|
limits = self.get_limits(platform)
|
||||||
|
max_chars = limits.get("max_characters", 2000)
|
||||||
|
|
||||||
|
# Si cabe, solo aplicar formato básico
|
||||||
|
if len(content) <= max_chars:
|
||||||
|
return AdaptedContent(
|
||||||
|
content=content,
|
||||||
|
platform=platform,
|
||||||
|
original_content=content,
|
||||||
|
truncated=False,
|
||||||
|
hashtags_adjusted=False,
|
||||||
|
changes_made=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Excede límite: usar IA para condensar
|
||||||
|
from openai import OpenAI
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
if not settings.DEEPSEEK_API_KEY:
|
||||||
|
# Fallback a adaptación por reglas si no hay API
|
||||||
|
return self.adapt(content, platform)
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=settings.DEEPSEEK_API_KEY,
|
||||||
|
base_url=settings.DEEPSEEK_BASE_URL or "https://api.deepseek.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dejar margen de 10 chars para seguridad
|
||||||
|
target_chars = max_chars - 10
|
||||||
|
|
||||||
|
prompt = f"""Condensa este contenido a máximo {target_chars} caracteres para {platform}.
|
||||||
|
|
||||||
|
CONTENIDO ORIGINAL:
|
||||||
|
{content}
|
||||||
|
|
||||||
|
REGLAS:
|
||||||
|
- Máximo {target_chars} caracteres (incluyendo hashtags y espacios)
|
||||||
|
- Mantén la esencia y mensaje principal
|
||||||
|
- Conserva el call-to-action si existe
|
||||||
|
- Incluye hashtags relevantes (máximo {limits.get('max_hashtags', 2)})
|
||||||
|
- NO uses emojis a menos que estén en el original
|
||||||
|
- Tono profesional
|
||||||
|
|
||||||
|
Responde SOLO con el contenido adaptado, sin explicaciones."""
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=500
|
||||||
|
)
|
||||||
|
|
||||||
|
adapted_content = response.choices[0].message.content.strip()
|
||||||
|
|
||||||
|
return AdaptedContent(
|
||||||
|
content=adapted_content,
|
||||||
|
platform=platform,
|
||||||
|
original_content=content,
|
||||||
|
truncated=True,
|
||||||
|
hashtags_adjusted=False,
|
||||||
|
changes_made=[f"Contenido adaptado con IA: {len(content)} → {len(adapted_content)} caracteres"]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def adapt_for_all_platforms_smart(
|
||||||
|
self,
|
||||||
|
content: str,
|
||||||
|
platforms: List[str]
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Adaptar contenido para múltiples plataformas usando IA cuando sea necesario.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Contenido base
|
||||||
|
platforms: Lista de plataformas
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict de plataforma -> contenido adaptado
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for platform in platforms:
|
||||||
|
adapted = await self.adapt_with_ai(content, platform)
|
||||||
|
results[platform] = adapted.content
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
# Instancia global
|
# Instancia global
|
||||||
platform_adapter = PlatformAdapter()
|
platform_adapter = PlatformAdapter()
|
||||||
|
|||||||
Reference in New Issue
Block a user