"""SQLAlchemy ORM models for AI Media OS.""" from sqlalchemy import Column, Integer, String, Text, Float, DateTime, Boolean, JSON, ForeignKey, Index, func, UniqueConstraint, Date from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime try: from pgvector.sqlalchemy import Vector _HAS_PGVECTOR = True except ImportError: Vector = None _HAS_PGVECTOR = False Base = declarative_base() class Agent(Base): """AI agent persona configuration.""" __tablename__ = "agents" id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) description = Column(Text) current_version_id = Column(Integer, ForeignKey("agent_versions.id")) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) versions = relationship("AgentVersion", back_populates="agent", cascade="all, delete-orphan", foreign_keys="[AgentVersion.agent_id]") current_version = relationship("AgentVersion", foreign_keys="[Agent.current_version_id]") class AgentVersion(Base): """Versioned agent configuration.""" __tablename__ = "agent_versions" __table_args__ = ( UniqueConstraint("agent_id", "version", name="uq_agent_version"), ) id = Column(Integer, primary_key=True) agent_id = Column(Integer, ForeignKey("agents.id", ondelete="CASCADE"), nullable=False) version = Column(Integer, nullable=False) config = Column(JSON, nullable=False) # Model, temperature, etc. created_at = Column(DateTime, default=datetime.utcnow) created_by = Column(String) agent = relationship("Agent", back_populates="versions", foreign_keys="[AgentVersion.agent_id]") prompts = relationship("PromptTemplate", back_populates="agent_version", cascade="all, delete-orphan") posts = relationship("Post", back_populates="agent_version") class PromptTemplate(Base): """Prompt templates for agent versions.""" __tablename__ = "prompt_templates" id = Column(Integer, primary_key=True) agent_version_id = Column(Integer, ForeignKey("agent_versions.id", ondelete="CASCADE"), nullable=False) name = Column(String, nullable=False) prompt_text = Column(Text, nullable=False) description = Column(Text) created_at = Column(DateTime, default=datetime.utcnow) agent_version = relationship("AgentVersion", back_populates="prompts") class Trend(Base): """Detected social trend.""" __tablename__ = "trends" __table_args__ = ( Index("ix_trends_status", "status"), Index("ix_trends_created_at", "created_at"), Index("ix_trends_source", "source"), ) id = Column(Integer, primary_key=True) topic = Column(String, nullable=False) source = Column(String, nullable=False) source_url = Column(String) score = Column(Float) status = Column(String, default="pending_generation") used_for_post = Column(Boolean, default=False) raw_data = Column(JSON) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) topic_embedding = Column(Vector(1536) if _HAS_PGVECTOR else Text, nullable=True) posts = relationship("Post", back_populates="trend") class MusicTrack(Base): """Royalty-free music tracks curated for social media posts.""" __tablename__ = "music_tracks" id = Column(Integer, primary_key=True) title = Column(String, nullable=False) artist = Column(String) genre = Column(String) # electronic|acoustic|pop|jazz|ambient|cinematic mood = Column(String) # energetic|calm|upbeat|inspirational|modern|warm|romantic|sophisticated|playful bpm = Column(Integer) duration_secs = Column(Integer) preview_url = Column(String) cover_url = Column(String) tags = Column(JSON, default=list) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) posts = relationship("Post", back_populates="music_track") class Post(Base): """Generated social media post.""" __tablename__ = "posts" __table_args__ = ( Index("ix_posts_status", "status"), Index("ix_posts_approval_status", "approval_status"), Index("ix_posts_platform", "platform"), Index("ix_posts_published_at", "published_at"), Index("ix_posts_created_at", "created_at"), ) id = Column(Integer, primary_key=True) trend_id = Column(Integer, ForeignKey("trends.id", ondelete="SET NULL")) agent_version_id = Column(Integer, ForeignKey("agent_versions.id"), nullable=True) content = Column(Text, nullable=False) post_type = Column(String, default="image") # image | text_only | carousel | video | link | text_image image_url = Column(String) carousel_urls = Column(JSON) # list of image URLs for carousel posts video_url = Column(String) link_url = Column(String) link_title = Column(String) link_description = Column(String) status = Column(String, default="draft") platform = Column(String) platform_post_id = Column(String) music_id = Column(Integer, ForeignKey("music_tracks.id", ondelete="SET NULL"), nullable=True) scheduled_at = Column(DateTime) published_at = Column(DateTime) approval_status = Column(String, default="pending") approval_notes = Column(Text) approved_by = Column(String) approved_at = Column(DateTime) token_count = Column(Integer) generation_cost = Column(Float) hallucination_score = Column(Float) quality_score = Column(Integer) quality_breakdown = Column(JSON) # Post-publish engagement metrics (fetched from Instagram ~24h after publish) ig_likes = Column(Integer) ig_comments = Column(Integer) ig_reach = Column(Integer) ig_saves = Column(Integer) ig_shares = Column(Integer) engagement_fetched_at = Column(DateTime) generation_error = Column(Text) content_embedding = Column(Vector(1536) if _HAS_PGVECTOR else Text, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) trend = relationship("Trend", back_populates="posts") agent_version = relationship("AgentVersion", back_populates="posts") media = relationship("Media", back_populates="post", cascade="all, delete-orphan") music_track = relationship("MusicTrack", back_populates="posts") class Media(Base): """Media (image/video) associated with posts.""" __tablename__ = "media" __table_args__ = ( Index("ix_media_post_id", "post_id"), Index("ix_media_moderation_status", "moderation_status"), ) id = Column(Integer, primary_key=True) post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE")) media_type = Column(String, nullable=False) file_name = Column(String, nullable=False) file_size = Column(Integer) mime_type = Column(String) r2_key = Column(String) r2_url = Column(String) moderation_status = Column(String, default="pending") nsfw_score = Column(Float) moderation_details = Column(JSON) thumbnail_url = Column(String) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) post = relationship("Post", back_populates="media") class WorkflowExecution(Base): """Temporal workflow execution tracking.""" __tablename__ = "workflow_executions" __table_args__ = ( Index("ix_workflow_status", "status"), Index("ix_workflow_name", "workflow_name"), Index("ix_workflow_started_at", "started_at"), Index("ix_workflow_post_id", "post_id"), UniqueConstraint("workflow_id", name="uq_workflow_id"), ) id = Column(Integer, primary_key=True) workflow_id = Column(String, nullable=False, unique=True) workflow_name = Column(String, nullable=False) trend_id = Column(Integer, ForeignKey("trends.id")) post_id = Column(Integer, ForeignKey("posts.id")) status = Column(String, default="running") started_at = Column(DateTime, default=datetime.utcnow) completed_at = Column(DateTime) error_message = Column(Text) execution_metadata = Column(JSON) class ObservabilityTrace(Base): """Langfuse trace references and metadata.""" __tablename__ = "observability_traces" __table_args__ = ( Index("ix_trace_post_id", "post_id"), Index("ix_trace_workflow_id", "workflow_id"), Index("ix_trace_created_at", "created_at"), ) id = Column(Integer, primary_key=True) trace_id = Column(String, unique=True) post_id = Column(Integer, ForeignKey("posts.id")) workflow_id = Column(String, ForeignKey("workflow_executions.workflow_id")) trace_name = Column(String) input = Column(JSON) output = Column(JSON) trace_metadata = Column(JSON) token_count = Column(Integer) cost_usd = Column(Float) duration_ms = Column(Integer) created_at = Column(DateTime, default=datetime.utcnow) class ApiUsage(Base): """Track API usage and costs.""" __tablename__ = "api_usage" __table_args__ = ( UniqueConstraint("service", "period_start", "period_end", name="uq_service_period"), ) id = Column(Integer, primary_key=True) service = Column(String, nullable=False) request_count = Column(Integer, default=0) token_count = Column(Integer, default=0) cost_usd = Column(Float, default=0.0) period_start = Column(DateTime) period_end = Column(DateTime) class SystemAlert(Base): """System alerts and errors.""" __tablename__ = "system_alerts" __table_args__ = ( Index("ix_alert_severity", "severity"), Index("ix_alert_resolved", "resolved"), Index("ix_alert_created_at", "created_at"), ) id = Column(Integer, primary_key=True) alert_type = Column(String, nullable=False) service = Column(String) message = Column(Text, nullable=False) severity = Column(String, default="warning") resolved = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow) resolved_at = Column(DateTime) class User(Base): """Web app user (authenticated via Firebase Google Auth).""" __tablename__ = "users" id = Column(Integer, primary_key=True) firebase_uid = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False) display_name = Column(String) photo_url = Column(String) is_active = Column(Boolean, default=True) is_admin = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow) last_login = Column(DateTime, default=datetime.utcnow) class PlatformConfig(Base): """Per-platform configuration — tokens stored encrypted.""" __tablename__ = "platform_configs" id = Column(Integer, primary_key=True) platform = Column(String, nullable=False, unique=True) # instagram | twitter | linkedin | youtube is_enabled = Column(Boolean, default=False) access_token_enc = Column(Text) # encrypted refresh_token_enc = Column(Text) # encrypted account_id = Column(String) extra_config = Column(JSON, default={}) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class ModelConfig(Base): """LLM model configuration with fallback.""" __tablename__ = "model_configs" id = Column(Integer, primary_key=True) name = Column(String, nullable=False) # e.g. "caption_generator" primary_model = Column(String, nullable=False) # e.g. "google/gemma-3-27b-it:free" fallback_model = Column(String) # used when primary hits quota api_base = Column(String) api_key_enc = Column(Text) # encrypted is_active = Column(Boolean, default=True) last_error = Column(Text) last_error_at = Column(DateTime) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class BrandVoice(Base): """Active brand voice and prompt rules for content generation.""" __tablename__ = "brand_voices" __table_args__ = ( Index("ix_brand_voice_active", "is_active"), ) id = Column(Integer, primary_key=True) brand_name = Column(String, default="Orbis") voice = Column(Text) audience = Column(Text) tone_rules = Column(Text) banned_words = Column(Text) cta_rules = Column(Text) hashtag_rules = Column(Text) auto_hashtags = Column(Text) # space-separated tags always appended to every post caption_templates = Column(JSON, default=list) # [{name, template}] example_posts = Column(Text) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class BrandKnowledge(Base): """Brand knowledge base entries — FAQs, products, personas, context injected at generation time.""" __tablename__ = "brand_knowledge" __table_args__ = ( Index("ix_brand_knowledge_active", "is_active"), Index("ix_brand_knowledge_type", "content_type"), ) id = Column(Integer, primary_key=True) title = Column(String, nullable=False) content = Column(Text, nullable=False) content_type = Column(String, default="general") # general | faq | product | persona | competitor embedding = Column(Vector(1536) if _HAS_PGVECTOR else Text, nullable=True) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class AgentFinding(Base): """AI agent team findings — daily analysis results.""" __tablename__ = "agent_findings" __table_args__ = ( Index("ix_agent_findings_run_date", "run_date"), Index("ix_agent_findings_agent_role", "agent_role"), Index("ix_agent_findings_severity", "severity"), ) id = Column(Integer, primary_key=True) agent_role = Column(String, nullable=False) # engineer|analyst|marketing|reviewer|architect|designer|manager category = Column(String) # health|performance|content|quality|strategy|summary severity = Column(String, default="info") # info|warning|critical title = Column(String, nullable=False) body = Column(Text) # LLM-written natural language finding action_items = Column(JSON, default=list) # list of strings data_snapshot = Column(JSON, default=dict) # raw numbers used to produce this finding run_date = Column(Date, nullable=False) created_at = Column(DateTime, default=datetime.utcnow) class ProviderKey(Base): """Per-provider LLM API credentials — stored encrypted, manageable from Settings UI.""" __tablename__ = "provider_keys" id = Column(Integer, primary_key=True) provider = Column(String, nullable=False, unique=True) # openrouter|anthropic|groq|... or any custom slug display_name = Column(String) api_key_enc = Column(Text) # Fernet-encrypted api_base = Column(String) # required for custom providers, optional override for known ones is_enabled = Column(Boolean, default=True) models = Column(JSON, default=list) # ordered model IDs to try disabled_models = Column(JSON, default=list) # model IDs toggled off by user updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class Notification(Base): """In-app notifications (token exhausted, publish failed, approval needed).""" __tablename__ = "notifications" __table_args__ = ( Index("ix_notif_user_id", "user_id"), Index("ix_notif_read", "is_read"), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE")) type = Column(String, nullable=False) # approval_needed | publish_failed | quota_exceeded title = Column(String, nullable=False) message = Column(Text) payload = Column(JSON, default={}) # e.g. {"workflow_id": "...", "post_id": 1} is_read = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow)