Files
ALwrity/backend/routers/campaign_creator.py

500 lines
20 KiB
Python

"""API endpoints for Campaign Creator - Multi-channel campaign management."""
from typing import Optional, List, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from services.campaign_creator import (
CampaignOrchestrator,
CampaignStorageService,
AssetAuditService,
ChannelPackService,
)
from services.product_marketing import BrandDNASyncService
from middleware.auth_middleware import get_current_user
from utils.logger_utils import get_service_logger
logger = get_service_logger("api.campaign_creator")
router = APIRouter(prefix="/api/campaign-creator", tags=["campaign-creator"])
# ====================
# REQUEST MODELS
# ====================
class CampaignCreateRequest(BaseModel):
"""Request to create a new campaign blueprint."""
campaign_name: str = Field(..., description="Campaign name")
goal: str = Field(..., description="Campaign goal (product_launch, awareness, conversion, etc.)")
kpi: Optional[str] = Field(None, description="Key performance indicator")
channels: List[str] = Field(..., description="Target channels (instagram, linkedin, tiktok, etc.)")
product_context: Optional[Dict[str, Any]] = Field(None, description="Product information")
class AssetProposalRequest(BaseModel):
"""Request to generate asset proposals."""
campaign_id: str = Field(..., description="Campaign ID")
product_context: Optional[Dict[str, Any]] = Field(None, description="Product information")
class AssetGenerateRequest(BaseModel):
"""Request to generate a specific asset."""
asset_proposal: Dict[str, Any] = Field(..., description="Asset proposal from generate_proposals")
product_context: Optional[Dict[str, Any]] = Field(None, description="Product information")
class AssetAuditRequest(BaseModel):
"""Request to audit uploaded assets."""
image_base64: str = Field(..., description="Base64 encoded image")
asset_metadata: Optional[Dict[str, Any]] = Field(None, description="Asset metadata")
# ====================
# DEPENDENCY
# ====================
def get_orchestrator() -> CampaignOrchestrator:
"""Get Campaign Orchestrator instance."""
return CampaignOrchestrator()
def get_campaign_storage() -> CampaignStorageService:
"""Get Campaign Storage Service instance."""
return CampaignStorageService()
def _require_user_id(current_user: Dict[str, Any], operation: str) -> str:
"""Ensure user_id is available for protected operations."""
user_id = current_user.get("sub") or current_user.get("user_id") or current_user.get("id")
if not user_id:
logger.error(
"[Campaign Creator] ❌ Missing user_id for %s operation - blocking request",
operation,
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authenticated user required for campaign creator operations.",
)
return str(user_id)
# ====================
# CAMPAIGN ENDPOINTS
# ====================
@router.post("/campaigns/validate-preflight", summary="Validate Campaign Pre-flight")
async def validate_campaign_preflight(
request: CampaignCreateRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
orchestrator: CampaignOrchestrator = Depends(get_orchestrator)
):
"""Validate campaign blueprint against subscription limits before creation."""
try:
user_id = _require_user_id(current_user, "campaign pre-flight validation")
logger.info(f"[Campaign Creator] Pre-flight validation for user {user_id}")
campaign_data = {
"campaign_name": request.campaign_name or "Temporary Campaign",
"goal": request.goal,
"kpi": request.kpi,
"channels": request.channels,
}
blueprint = orchestrator.create_campaign_blueprint(user_id, campaign_data)
validation_result = orchestrator.validate_campaign_preflight(user_id, blueprint)
logger.info(f"[Campaign Creator] ✅ Pre-flight validation completed: can_proceed={validation_result.get('can_proceed')}")
return validation_result
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error in pre-flight validation: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Pre-flight validation failed: {str(e)}")
@router.post("/campaigns/create-blueprint", summary="Create Campaign Blueprint")
async def create_campaign_blueprint(
request: CampaignCreateRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
orchestrator: CampaignOrchestrator = Depends(get_orchestrator)
):
"""Create a campaign blueprint with personalized asset nodes."""
try:
user_id = _require_user_id(current_user, "campaign blueprint creation")
logger.info(f"[Campaign Creator] Creating blueprint for user {user_id}: {request.campaign_name}")
campaign_data = {
"campaign_name": request.campaign_name,
"goal": request.goal,
"kpi": request.kpi,
"channels": request.channels,
}
blueprint = orchestrator.create_campaign_blueprint(user_id, campaign_data)
blueprint_dict = {
"campaign_id": blueprint.campaign_id,
"campaign_name": blueprint.campaign_name,
"goal": blueprint.goal,
"kpi": blueprint.kpi,
"phases": blueprint.phases,
"asset_nodes": [
{
"asset_id": node.asset_id,
"asset_type": node.asset_type,
"channel": node.channel,
"status": node.status,
}
for node in blueprint.asset_nodes
],
"channels": blueprint.channels,
"status": blueprint.status,
}
campaign_storage = get_campaign_storage()
campaign_storage.save_campaign(user_id, blueprint_dict)
logger.info(f"[Campaign Creator] ✅ Blueprint created and saved: {blueprint.campaign_id}")
return blueprint_dict
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error creating blueprint: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Campaign blueprint creation failed: {str(e)}")
@router.post("/campaigns/{campaign_id}/generate-proposals", summary="Generate Asset Proposals")
async def generate_asset_proposals(
campaign_id: str,
request: AssetProposalRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
orchestrator: CampaignOrchestrator = Depends(get_orchestrator)
):
"""Generate AI proposals for all assets in a campaign blueprint."""
try:
user_id = _require_user_id(current_user, "asset proposal generation")
logger.info(f"[Campaign Creator] Generating proposals for campaign {campaign_id}")
campaign_storage = get_campaign_storage()
campaign = campaign_storage.get_campaign(user_id, campaign_id)
if not campaign:
raise HTTPException(status_code=404, detail="Campaign not found")
from services.campaign_creator.orchestrator import CampaignBlueprint, CampaignAssetNode
asset_nodes = []
if campaign.asset_nodes:
for node_data in campaign.asset_nodes:
asset_nodes.append(CampaignAssetNode(
asset_id=node_data.get('asset_id'),
asset_type=node_data.get('asset_type'),
channel=node_data.get('channel'),
status=node_data.get('status', 'draft'),
))
blueprint = CampaignBlueprint(
campaign_id=campaign.campaign_id,
campaign_name=campaign.campaign_name,
goal=campaign.goal,
kpi=campaign.kpi,
channels=campaign.channels or [],
asset_nodes=asset_nodes,
)
proposals = orchestrator.generate_asset_proposals(
user_id=user_id,
blueprint=blueprint,
product_context=request.product_context,
)
try:
campaign_storage.save_proposals(user_id, campaign_id, proposals)
logger.info(f"[Campaign Creator] ✅ Saved {proposals['total_assets']} proposals to database")
except Exception as save_error:
logger.error(f"[Campaign Creator] ⚠️ Failed to save proposals to database: {str(save_error)}")
logger.info(f"[Campaign Creator] ✅ Generated {proposals['total_assets']} proposals")
return proposals
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error generating proposals: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Asset proposal generation failed: {str(e)}")
@router.post("/assets/generate", summary="Generate Asset")
async def generate_asset(
request: AssetGenerateRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
orchestrator: CampaignOrchestrator = Depends(get_orchestrator)
):
"""Generate a single asset using Image Studio APIs."""
try:
user_id = _require_user_id(current_user, "asset generation")
logger.info(f"[Campaign Creator] Generating asset for user {user_id}")
result = await orchestrator.generate_asset(
user_id=user_id,
asset_proposal=request.asset_proposal,
product_context=request.product_context,
)
if result.get('success'):
campaign_id = request.asset_proposal.get('campaign_id')
if not campaign_id:
asset_id = request.asset_proposal.get('asset_id', '')
if asset_id and '_' in asset_id:
parts = asset_id.split('_')
phase_indicators = ['teaser', 'launch', 'nurture', 'prelaunch', 'postlaunch']
for i, part in enumerate(parts):
if part.lower() in phase_indicators and i > 0:
campaign_id = '_'.join(parts[:i])
break
if campaign_id:
try:
campaign_storage = get_campaign_storage()
campaign = campaign_storage.get_campaign(user_id, campaign_id)
if campaign:
asset_node_id = request.asset_proposal.get('asset_id', '')
if asset_node_id:
from models.product_marketing_models import CampaignProposal
from services.database import SessionLocal
db = SessionLocal()
try:
proposal = db.query(CampaignProposal).filter(
CampaignProposal.campaign_id == campaign_id,
CampaignProposal.asset_node_id == asset_node_id,
CampaignProposal.user_id == user_id
).first()
if proposal:
proposal.status = 'ready'
db.commit()
logger.info(f"[Campaign Creator] ✅ Updated proposal status for {asset_node_id}")
finally:
db.close()
logger.info(f"[Campaign Creator] ✅ Asset generated for campaign {campaign_id}")
except Exception as update_error:
logger.warning(f"[Campaign Creator] ⚠️ Could not update campaign status: {str(update_error)}")
logger.info(f"[Campaign Creator] ✅ Asset generated successfully")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error generating asset: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Asset generation failed: {str(e)}")
# ====================
# BRAND DNA ENDPOINTS
# ====================
@router.get("/brand-dna", summary="Get Brand DNA Tokens")
async def get_brand_dna(
current_user: Dict[str, Any] = Depends(get_current_user),
brand_dna_sync: BrandDNASyncService = Depends(lambda: BrandDNASyncService())
):
"""Get brand DNA tokens for the authenticated user."""
try:
user_id = _require_user_id(current_user, "brand DNA retrieval")
brand_tokens = brand_dna_sync.get_brand_dna_tokens(user_id)
return {"brand_dna": brand_tokens}
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error getting brand DNA: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/brand-dna/channel/{channel}", summary="Get Channel-Specific Brand DNA")
async def get_channel_brand_dna(
channel: str,
current_user: Dict[str, Any] = Depends(get_current_user),
brand_dna_sync: BrandDNASyncService = Depends(lambda: BrandDNASyncService())
):
"""Get channel-specific brand DNA adaptations."""
try:
user_id = _require_user_id(current_user, "channel brand DNA retrieval")
channel_dna = brand_dna_sync.get_channel_specific_dna(user_id, channel)
return {"channel": channel, "brand_dna": channel_dna}
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error getting channel DNA: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ====================
# ASSET AUDIT ENDPOINTS
# ====================
@router.post("/assets/audit", summary="Audit Asset")
async def audit_asset(
request: AssetAuditRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
asset_audit: AssetAuditService = Depends(lambda: AssetAuditService())
):
"""Audit an uploaded asset and get enhancement recommendations."""
try:
user_id = _require_user_id(current_user, "asset audit")
audit_result = asset_audit.audit_asset(
request.image_base64,
request.asset_metadata,
)
return audit_result
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error auditing asset: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ====================
# CHANNEL PACK ENDPOINTS
# ====================
@router.get("/channels/{channel}/pack", summary="Get Channel Pack")
async def get_channel_pack(
channel: str,
asset_type: str = "social_post",
current_user: Dict[str, Any] = Depends(get_current_user),
channel_pack: ChannelPackService = Depends(lambda: ChannelPackService())
):
"""Get channel-specific pack configuration with templates and optimization tips."""
try:
pack = channel_pack.get_channel_pack(channel, asset_type)
return pack
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error getting channel pack: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ====================
# CAMPAIGN LISTING & RETRIEVAL
# ====================
@router.get("/campaigns", summary="List Campaigns")
async def list_campaigns(
status: Optional[str] = None,
current_user: Dict[str, Any] = Depends(get_current_user),
campaign_storage: CampaignStorageService = Depends(get_campaign_storage)
):
"""List all campaigns for the authenticated user."""
try:
user_id = _require_user_id(current_user, "list campaigns")
campaigns = campaign_storage.list_campaigns(user_id, status=status)
return {
"campaigns": [
{
"campaign_id": c.campaign_id,
"campaign_name": c.campaign_name,
"goal": c.goal,
"kpi": c.kpi,
"status": c.status,
"channels": c.channels,
"phases": c.phases,
"asset_nodes": c.asset_nodes,
"created_at": c.created_at.isoformat() if c.created_at else None,
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
}
for c in campaigns
],
"total": len(campaigns),
}
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error listing campaigns: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/campaigns/{campaign_id}", summary="Get Campaign")
async def get_campaign(
campaign_id: str,
current_user: Dict[str, Any] = Depends(get_current_user),
campaign_storage: CampaignStorageService = Depends(get_campaign_storage)
):
"""Get a specific campaign by ID."""
try:
user_id = _require_user_id(current_user, "get campaign")
campaign = campaign_storage.get_campaign(user_id, campaign_id)
if not campaign:
raise HTTPException(status_code=404, detail="Campaign not found")
return {
"campaign_id": campaign.campaign_id,
"campaign_name": campaign.campaign_name,
"goal": campaign.goal,
"kpi": campaign.kpi,
"status": campaign.status,
"channels": campaign.channels,
"phases": campaign.phases,
"asset_nodes": campaign.asset_nodes,
"product_context": campaign.product_context,
"created_at": campaign.created_at.isoformat() if campaign.created_at else None,
"updated_at": campaign.updated_at.isoformat() if campaign.updated_at else None,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error getting campaign: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/campaigns/{campaign_id}/proposals", summary="Get Campaign Proposals")
async def get_campaign_proposals(
campaign_id: str,
current_user: Dict[str, Any] = Depends(get_current_user),
campaign_storage: CampaignStorageService = Depends(get_campaign_storage)
):
"""Get proposals for a campaign."""
try:
user_id = _require_user_id(current_user, "get proposals")
proposals = campaign_storage.get_proposals(user_id, campaign_id)
proposals_dict = {}
for proposal in proposals:
proposals_dict[proposal.asset_node_id] = {
"asset_id": proposal.asset_node_id,
"asset_type": proposal.asset_type,
"channel": proposal.channel,
"proposed_prompt": proposal.proposed_prompt,
"recommended_template": proposal.recommended_template,
"recommended_provider": proposal.recommended_provider,
"cost_estimate": proposal.cost_estimate,
"concept_summary": proposal.concept_summary,
"status": proposal.status,
}
return {
"proposals": proposals_dict,
"total_assets": len(proposals),
}
except Exception as e:
logger.error(f"[Campaign Creator] ❌ Error getting proposals: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# ====================
# HEALTH CHECK
# ====================
@router.get("/health", summary="Health Check")
async def health_check():
"""Health check endpoint for Campaign Creator."""
return {
"status": "healthy",
"service": "campaign_creator",
"version": "1.0.0",
"modules": {
"orchestrator": "available",
"prompt_builder": "available",
"brand_dna_sync": "available",
"asset_audit": "available",
"channel_pack": "available",
"campaign_storage": "available",
}
}