"""SQLAlchemy models for the AI Assistant plugin.""" from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, ) from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin # --- Providers --- class AIProvider(Base, TenantMixin): """LLM provider configuration (OpenAI, Anthropic, Ollama, etc.).""" __tablename__ = "ai_providers" __table_args__ = ( Index("ix_ai_providers_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(String(100), nullable=False) provider_type: Mapped[str] = mapped_column(String(50), nullable=False) api_key: Mapped[str] = mapped_column(Text, nullable=False, default="") base_url: Mapped[str] = mapped_column(String(500), nullable=False, default="") is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) # --- Models --- class AIModel(Base, TenantMixin): """Available model per provider.""" __tablename__ = "ai_models" __table_args__ = ( Index("ix_ai_models_provider", "provider_id"), Index("ix_ai_models_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) provider_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_providers.id", ondelete="CASCADE"), nullable=False, ) model_id: Mapped[str] = mapped_column(String(200), nullable=False) display_name: Mapped[str] = mapped_column(String(200), nullable=False) context_window: Mapped[int] = mapped_column(Integer, nullable=False, default=4096) supports_tools: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) supports_streaming: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) # --- Presets --- class AIPreset(Base, TenantMixin): """Model preset: model + parameters + optional system prompt.""" __tablename__ = "ai_presets" __table_args__ = ( Index("ix_ai_presets_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(String(100), nullable=False) model_id: Mapped[str] = mapped_column(String(200), nullable=False) provider_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_providers.id", ondelete="SET NULL"), nullable=True, ) temperature: Mapped[float] = mapped_column(Float, nullable=False, default=0.7) max_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=2048) top_p: Mapped[float] = mapped_column(Float, nullable=False, default=1.0) system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="") config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) # --- Agents --- class AIAgent(Base, TenantMixin): """AI agent with system prompt and assigned tools.""" __tablename__ = "ai_agents" __table_args__ = ( Index("ix_ai_agents_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(String(100), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False, default="") system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="") preset_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_presets.id", ondelete="SET NULL"), nullable=True, ) tool_ids: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) # --- Chat Sessions --- class AIChatSession(Base, TenantMixin): """Chat session for a user with a specific agent.""" __tablename__ = "ai_chat_sessions" __table_args__ = ( Index("ix_ai_sessions_user", "user_id"), Index("ix_ai_sessions_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) agent_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_agents.id", ondelete="SET NULL"), nullable=True, ) title: Mapped[str] = mapped_column(String(255), nullable=False, default="Neuer Chat") is_pinned: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_sidebar: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) folder_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_chat_folders.id", ondelete="SET NULL"), nullable=True, ) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # --- Chat Messages --- class AIChatMessage(Base, TenantMixin): """Individual message in a chat session.""" __tablename__ = "ai_chat_messages" __table_args__ = ( Index("ix_ai_messages_session", "session_id"), Index("ix_ai_messages_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) session_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_chat_sessions.id", ondelete="CASCADE"), nullable=False, ) role: Mapped[str] = mapped_column(String(20), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False, default="") tool_calls: Mapped[dict | None] = mapped_column(JSONB, nullable=True) tool_results: Mapped[dict | None] = mapped_column(JSONB, nullable=True) tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0) model_used: Mapped[str] = mapped_column(String(200), nullable=False, default="") # --- Chat Folders --- class AIChatFolder(Base, TenantMixin): """Folder for organizing chat sessions.""" __tablename__ = "ai_chat_folders" __table_args__ = ( Index("ix_ai_folders_user", "user_id"), Index("ix_ai_folders_tenant", "tenant_id"), Index("ix_ai_folders_parent", "parent_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(String(255), nullable=False) parent_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_chat_folders.id", ondelete="CASCADE"), nullable=True, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # --- Chat Attachments --- class AIChatAttachment(Base, TenantMixin): """File attached to a chat message.""" __tablename__ = "ai_chat_attachments" __table_args__ = ( Index("ix_ai_attachments_message", "message_id"), Index("ix_ai_attachments_session", "session_id"), Index("ix_ai_attachments_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) message_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_chat_messages.id", ondelete="CASCADE"), nullable=True, ) session_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("ai_chat_sessions.id", ondelete="CASCADE"), nullable=False, ) filename: Mapped[str] = mapped_column(String(255), nullable=False) mime_type: Mapped[str] = mapped_column(String(255), nullable=False, default="application/octet-stream") size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0) storage_path: Mapped[str] = mapped_column(String(1024), nullable=False)