from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON
from sqlalchemy.orm import relationship
# The declarative_base class is already defined in database.py.
# We will import it from there to ensure all models use the same base.
from .database import Base
# --- SQLAlchemy Models ---
# These classes define the structure of the database tables and how they relate.
class User(Base):
"""
SQLAlchemy model for the 'users' table, used for OIDC authentication.
This table stores user information obtained during the OIDC login process.
"""
__tablename__ = 'users'
# The user's unique ID, which will be provided by the OIDC provider.
id = Column(String, primary_key=True, index=True)
# The unique OIDC ID from the provider.
oidc_id = Column(String, unique=True, nullable=True)
# The user's email address.
email = Column(String, nullable=True)
# The user's display name.
username = Column(String, nullable=True)
# Enterprise profile info
full_name = Column(String, nullable=True)
role = Column(String, default="user", nullable=False) # 'admin' or 'user'
group_id = Column(String, ForeignKey('groups.id'), nullable=True)
avatar_url = Column(String, nullable=True)
# Timestamp for when the user account was created.
created_at = Column(DateTime, default=datetime.utcnow)
# Track platform engagement for auditing
last_login_at = Column(DateTime, default=datetime.utcnow)
# User's preferences/settings (e.g. LLM/TTS/STT configs)
preferences = Column(JSON, default={}, nullable=True)
# Relationship to Group
group = relationship("Group", back_populates="users")
# Defines a one-to-many relationship with the Session table.
# 'back_populates' creates a link back to the User model from the Session model.
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
def __repr__(self):
return f"<User(id={self.id}, email='{self.email}')>"
class Group(Base):
"""
SQLAlchemy model for the 'groups' table.
Groups define policies for AI provider access.
"""
__tablename__ = 'groups'
id = Column(String, primary_key=True, index=True)
name = Column(String, unique=True, nullable=False)
description = Column(String, nullable=True)
# Policy: which providers are allowed for this group
# Example: {"llm": ["openai", "gemini"], "tts": ["gcloud_tts"], "stt": ["google_gemini"]}
policy = Column(JSON, default={}, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
users = relationship("User", back_populates="group")
def __repr__(self):
return f"<Group(id={self.id}, name='{self.name}')>"
class Session(Base):
"""
SQLAlchemy model for the 'sessions' table.
Each session represents a single conversation between a user and the AI.
It links a user to a series of messages.
"""
__tablename__ = 'sessions'
# Primary key for the session.
id = Column(Integer, primary_key=True, index=True)
# The ID of the user who owns this session.
# We add the ForeignKey to establish the link to the 'users' table.
user_id = Column(String, ForeignKey('users.id'), index=True, nullable=False)
# A title for the conversation, which can be generated by the AI.
title = Column(String, index=True, nullable=True)
# The name of the LLM model used for this session (e.g., "Gemini", "DeepSeek").
provider_name = Column(String, nullable=True)
# Track STT and TTS providers used in this session context
stt_provider_name = Column(String, nullable=True)
tts_provider_name = Column(String, nullable=True)
# The feature namespace this session belongs to (e.g., "coding_assistant").
feature_name = Column(String, default="default", nullable=False)
# Timestamp for when the session was created.
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Flag to indicate if the session has been archived or soft-deleted.
is_archived = Column(Boolean, default=False, nullable=False)
# Defines a one-to-many relationship with the Message table.
# 'back_populates' tells SQLAlchemy that there's a corresponding relationship
# on the other side. 'cascade' ensures that when a session is deleted,
# all its associated messages are also deleted.
messages = relationship("Message", back_populates="session", cascade="all, delete-orphan")
# Defines a many-to-one relationship back to the User table.
# This allows us to access the parent User object from a Session object.
user = relationship("User", back_populates="sessions")
def __repr__(self):
"""
Provides a helpful string representation of the object for debugging.
"""
return f"<Session(id={self.id}, title='{self.title}', user_id='{self.user_id}')>"
class Message(Base):
"""
SQLAlchemy model for the 'messages' table.
This table stores the individual chat messages within a session,
including who sent them (user or AI) and the content.
"""
__tablename__ = 'messages'
# Primary key for the message.
id = Column(Integer, primary_key=True, index=True)
# The foreign key that links this message to its parent session.
# This is a critical link for reconstructing chat history.
session_id = Column(Integer, ForeignKey('sessions.id'), nullable=False)
# Identifies the sender of the message, e.g., 'user' or 'assistant'.
sender = Column(String, nullable=False)
# The actual text content of the message.
content = Column(Text, nullable=False)
# Timestamp for when the message was sent.
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# The time taken for the model to generate the response, in seconds.
model_response_time = Column(Integer, nullable=True)
# The number of tokens in the message (both input and output).
token_count = Column(Integer, nullable=True)
# A JSON field to store unstructured metadata about the message, such as tool calls.
# This column has been renamed from 'metadata' to avoid a conflict.
message_metadata = Column(JSON, nullable=True)
# Path to the generated audio file for this message, if any.
audio_path = Column(String, nullable=True)
# Relationship back to the parent Session.
# This allows us to access the parent Session object from a Message object.
session = relationship("Session", back_populates="messages")
def __repr__(self):
"""
Provides a helpful string representation of the object for debugging.
"""
return f"<Message(id={self.id}, session_id={self.session_id}, sender='{self.sender}')>"
class Document(Base):
"""
SQLAlchemy model for the 'documents' table.
This table stores the metadata and original text content of a document.
The content is the data that will be chunked, embedded, and used for RAG.
"""
__tablename__ = 'documents'
# Primary key for the document, uniquely identifying each entry.
id = Column(Integer, primary_key=True, index=True)
# The title of the document for easy human-readable reference.
title = Column(String, index=True, nullable=False)
# The actual text content of the document. Using Text for potentially long strings.
text = Column(Text, nullable=False)
# The original source URL or path of the document.
source_url = Column(String, nullable=True)
# A string to identify the author of the document.
author = Column(String, nullable=True)
# The current processing status of the document (e.g., 'ready', 'processing', 'failed').
status = Column(String, default="processing", nullable=False)
# Timestamp for when the document was added to the database.
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# A string to identify the user who added the document, useful for multi-user apps.
user_id = Column(String, index=True, nullable=True)
# Defines a one-to-one relationship with the VectorMetadata table.
vector_metadata = relationship(
"VectorMetadata",
back_populates="document",
cascade="all, delete-orphan", # Deletes vector metadata when the document is deleted.
uselist=False
)
def __repr__(self):
"""
Provides a helpful string representation of the object for debugging.
"""
return f"<Document(id={self.id}, title='{self.title}', user_id='{self.user_id}')>"
class VectorMetadata(Base):
"""
SQLAlchemy model for the 'vector_metadata' table.
This table links a document to its corresponding vector representation
in the FAISS index. The primary key `id` of this table serves as the
vector ID in the FAISS store, making the `faiss_index` column redundant.
"""
__tablename__ = 'vector_metadata'
# Primary key for the metadata entry. This will also be the FAISS index.
id = Column(Integer, primary_key=True, index=True)
# Foreign key that links this metadata entry back to its Document.
document_id = Column(Integer, ForeignKey('documents.id'), unique=True)
# Foreign key to link this vector metadata to a specific session.
# This is crucial for retrieving relevant RAG context for a given conversation.
session_id = Column(Integer, ForeignKey('sessions.id'), nullable=True)
# The name of the embedding model used to create the vector.
embedding_model = Column(String, nullable=False)
# Defines a many-to-one relationship with the Document table.
document = relationship("Document", back_populates="vector_metadata")
# Defines a many-to-one relationship with the Session table.
session = relationship("Session")
def __repr__(self):
"""
Provides a helpful string representation of the object for debugging.
"""
return f"<VectorMetadata(id={self.id}, document_id={self.document_id}, session_id={self.session_id})>"
# --- New Asset Management Models ---
class PromptTemplate(Base):
"""
SQLAlchemy model for centralized system prompts.
"""
__tablename__ = 'prompt_templates'
id = Column(Integer, primary_key=True, index=True)
slug = Column(String, unique=True, index=True, nullable=False)
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
version = Column(Integer, default=1)
owner_id = Column(String, ForeignKey('users.id'), nullable=False)
group_id = Column(String, ForeignKey('groups.id'), nullable=True)
is_public = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
owner = relationship("User")
group = relationship("Group")
def __repr__(self):
return f"<PromptTemplate(slug='{self.slug}', version={self.version})>"
class Skill(Base):
"""
SQLAlchemy model for AI capabilities (Skills/Tools).
"""
__tablename__ = 'skills'
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True, nullable=False)
description = Column(String, nullable=True)
# type: 'local', 'remote_grpc', 'mcp'
skill_type = Column(String, default="local", nullable=False)
# Stores tool definition, parameters, or endpoint config
config = Column(JSON, default={}, nullable=True)
owner_id = Column(String, ForeignKey('users.id'), nullable=False)
group_id = Column(String, ForeignKey('groups.id'), nullable=True)
is_system = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
owner = relationship("User")
group = relationship("Group")
def __repr__(self):
return f"<Skill(name='{self.name}', type='{self.skill_type}')>"
class MCPServer(Base):
"""
SQLAlchemy model for Model Context Protocol (MCP) server configurations.
"""
__tablename__ = 'mcp_servers'
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
url = Column(String, nullable=False)
auth_config = Column(JSON, default={}, nullable=True)
owner_id = Column(String, ForeignKey('users.id'), nullable=False)
group_id = Column(String, ForeignKey('groups.id'), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
owner = relationship("User")
group = relationship("Group")
def __repr__(self):
return f"<MCPServer(name='{self.name}', url='{self.url}')>"
class AssetPermission(Base):
"""
SQLAlchemy model for granular permission control on assets.
"""
__tablename__ = 'asset_permissions'
id = Column(Integer, primary_key=True, index=True)
# resource_type: 'prompt', 'skill', 'mcp_server'
resource_type = Column(String, nullable=False, index=True)
resource_id = Column(Integer, nullable=False, index=True)
# Grant to a specific user OR a specific group
user_id = Column(String, ForeignKey('users.id'), nullable=True)
group_id = Column(String, ForeignKey('groups.id'), nullable=True)
# access_level: 'view', 'execute', 'admin'
access_level = Column(String, default="execute", nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User")
group = relationship("Group")
def __repr__(self):
return f"<AssetPermission(type='{self.resource_type}', id={self.resource_id}, level='{self.access_level}')>"
class AgentNode(Base):
"""
Admin-configured Agent Node.
Only admins register and configure nodes. Groups are then granted access.
Users see nodes available to their group and can attach them to sessions.
Lifecycle:
1. Admin creates the node record here (description, skill_config, invite_token).
2. Admin deploys the client-side node software with the generated config YAML.
3. Node connects → last_status flips to 'online'.
4. Admin grants access to one or more groups (NodeGroupAccess).
5. Users in those groups see the node in preferences / session setup.
"""
__tablename__ = 'agent_nodes'
id = Column(Integer, primary_key=True, index=True)
# Stable identifier used in the node's YAML config (e.g. "dev-macbook-m3")
node_id = Column(String, unique=True, index=True, nullable=False)
# Human-readable name shown in the UI
display_name = Column(String, nullable=False)
# Rich description — like a skill description; tells users what this node is for
description = Column(String, nullable=True)
# Admin user who registered this node
registered_by = Column(String, ForeignKey('users.id'), nullable=False)
# Skill enablement toggles + per-skill config
# Example:
# {
# "shell": {"enabled": true, "cwd_jail": "/home/user/projects"},
# "browser": {"enabled": false},
# "sync": {"enabled": true, "max_file_size_mb": 50}
# }
skill_config = Column(JSON, default={
"shell": {"enabled": True},
"browser": {"enabled": True},
"sync": {"enabled": True},
}, nullable=False)
# Actual capabilities reported by the node on connect (read-only, set by node)
capabilities = Column(JSON, default={}, nullable=True)
# Pre-signed invite token generated at node creation (used in downloaded config YAML)
invite_token = Column(String, unique=True, nullable=True, index=True)
# Whether this node is administratively active (can be disabled without deleting)
is_active = Column(Boolean, default=True, nullable=False)
# Live status updated by NodeRegistryService: 'online' | 'offline' | 'stale'
last_status = Column(String, default="offline", nullable=False)
# Last heartbeat timestamp
last_seen_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
registered_by_user = relationship("User", foreign_keys=[registered_by])
# Groups that have been granted access to this node
group_access = relationship("NodeGroupAccess", back_populates="node", cascade="all, delete-orphan")
def __repr__(self):
return f"<AgentNode(node_id='{self.node_id}', status='{self.last_status}')>"
class NodeGroupAccess(Base):
"""
Grants a group access to a specific agent node.
Admin sets this; users in the group can then see and use the node.
"""
__tablename__ = 'node_group_access'
id = Column(Integer, primary_key=True, index=True)
node_id = Column(String, ForeignKey('agent_nodes.node_id'), nullable=False, index=True)
group_id = Column(String, ForeignKey('groups.id'), nullable=False, index=True)
# access_level: 'view' (see but not use), 'use' (can attach to session), 'admin' (can config)
access_level = Column(String, default="use", nullable=False)
granted_by = Column(String, ForeignKey('users.id'), nullable=False)
granted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
node = relationship("AgentNode", back_populates="group_access")
group = relationship("Group")
granted_by_user = relationship("User", foreign_keys=[granted_by])
def __repr__(self):
return f"<NodeGroupAccess(node='{self.node_id}', group='{self.group_id}', level='{self.access_level}')>"