"""
IRIS Database Models
SQLAlchemy models for core functionality
"""
from datetime import datetime
from typing import Optional, Dict, Any
from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, JSON, ForeignKey, BigInteger
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
Base = declarative_base()
class User(Base):
"""Telegram user model"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
telegram_id = Column(BigInteger, unique=True, index=True, nullable=False)
username = Column(String(255), nullable=True)
first_name = Column(String(255), nullable=True)
last_name = Column(String(255), nullable=True)
language_code = Column(String(10), default="en")
# User preferences and settings
preferences = Column(JSON, default=dict)
is_active = Column(Boolean, default=True)
is_premium = Column(Boolean, default=False)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
last_activity = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
files = relationship("File", back_populates="user", cascade="all, delete-orphan")
def __repr__(self):
return f"<User(telegram_id={self.telegram_id}, username={self.username})>"
class Session(Base):
"""User conversation sessions with LLM"""
__tablename__ = "sessions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
session_id = Column(String(255), unique=True, index=True, nullable=False)
# Session context and state
context = Column(JSON, default=dict) # LLM conversation context
state = Column(String(50), default="active") # active, paused, completed
# Session metadata
title = Column(String(500), nullable=True)
summary = Column(Text, nullable=True)
message_count = Column(Integer, default=0)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
last_message_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
user = relationship("User", back_populates="sessions")
messages = relationship("Message", back_populates="session", cascade="all, delete-orphan")
def __repr__(self):
return f"<Session(id={self.session_id}, user_id={self.user_id}, state={self.state})>"
class ThreadCredentials(Base):
"""Thread-scoped credentials that survive agent memory limits"""
__tablename__ = "thread_credentials"
id = Column(Integer, primary_key=True, index=True)
thread_id = Column(String(500), nullable=False, index=True)
credential_type = Column(String(50), nullable=False) # 'pec', 'microsoft'
# PEC credentials
pec_email = Column(String(255), nullable=True)
pec_password = Column(Text, nullable=True) # TODO: Encrypt in production
# Microsoft credentials
user_email = Column(String(255), nullable=True)
# Metadata
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
last_used_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True), nullable=True)
def __repr__(self):
return f"<ThreadCredentials(thread_id={self.thread_id}, type={self.credential_type})>"
class Message(Base):
"""Individual messages in a session"""
__tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True)
session_id = Column(Integer, ForeignKey("sessions.id"), nullable=False)
# Message content
role = Column(String(20), nullable=False) # user, assistant, system
content = Column(Text, nullable=False)
message_type = Column(String(50), default="text") # text, file, command, etc.
# Message metadata
telegram_message_id = Column(BigInteger, nullable=True)
tokens_used = Column(Integer, nullable=True)
processing_time = Column(Integer, nullable=True) # milliseconds
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
session = relationship("Session", back_populates="messages")
def __repr__(self):
return f"<Message(role={self.role}, type={self.message_type})>"
class File(Base):
"""Uploaded files and documents"""
__tablename__ = "files"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# File identification
file_id = Column(String(255), unique=True, index=True, nullable=False)
telegram_file_id = Column(String(255), nullable=True)
# File metadata
filename = Column(String(500), nullable=False)
original_filename = Column(String(500), nullable=True)
file_type = Column(String(100), nullable=False) # pdf, docx, txt, etc.
mime_type = Column(String(200), nullable=True)
file_size = Column(BigInteger, nullable=True) # bytes
# File storage
file_path = Column(String(1000), nullable=True) # local storage path
storage_type = Column(String(50), default="local") # local, s3, etc.
# File analysis
analysis_status = Column(String(50), default="pending") # pending, processing, completed, failed
analysis_result = Column(JSON, default=dict) # LLM analysis results
extracted_text = Column(Text, nullable=True)
# File metadata
description = Column(Text, nullable=True)
tags = Column(JSON, default=list) # user-defined tags
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
analyzed_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
user = relationship("User", back_populates="files")
def __repr__(self):
return f"<File(filename={self.filename}, type={self.file_type}, status={self.analysis_status})>"
class SystemLog(Base):
"""System logs and events"""
__tablename__ = "system_logs"
id = Column(Integer, primary_key=True, index=True)
# Log details
level = Column(String(20), nullable=False) # DEBUG, INFO, WARNING, ERROR, CRITICAL
message = Column(Text, nullable=False)
component = Column(String(100), nullable=True) # telegram_bot, llm_core, etc.
# Context
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
session_id = Column(String(255), nullable=True)
log_metadata = Column(JSON, default=dict)
# Timestamp
created_at = Column(DateTime(timezone=True), server_default=func.now())
def __repr__(self):
return f"<SystemLog(level={self.level}, component={self.component})>"
class HybridBookingSession(Base):
"""Hybrid booking sessions for external meeting confirmations"""
__tablename__ = "hybrid_booking_sessions"
id = Column(Integer, primary_key=True, index=True)
# Session identification
session_id = Column(String(255), unique=True, index=True, nullable=False)
booking_url = Column(String(500), nullable=False)
# Organizer information
organizer_email = Column(String(255), nullable=False)
organizer_name = Column(String(255), nullable=False)
# External attendee information
external_email = Column(String(255), nullable=False)
external_name = Column(String(255), nullable=False)
# Meeting details
meeting_subject = Column(String(500), nullable=False)
meeting_duration = Column(Integer, nullable=False) # minutes
# Internal attendees (JSON array of {email, name})
internal_attendees = Column(JSON, default=list)
# Proposed time slots (JSON array of {start, end})
proposed_slots = Column(JSON, nullable=False)
# Attachments (JSON array of {name, contentType, size, contentBytes})
attachments = Column(JSON, default=list)
# Booking status and result
status = Column(String(50), default="pending") # pending, confirmed, expired, cancelled
confirmed_slot = Column(JSON, nullable=True) # {start, end}
event_id = Column(String(255), nullable=True) # Microsoft Graph event ID
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
confirmed_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
def __repr__(self):
return f"<HybridBookingSession(session_id={self.session_id}, status={self.status})>"