"""ORM model definitions""" from datetime import datetime from typing import Optional, List from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from luxx.database import Base def local_now(): return datetime.now() class LLMProvider(Base): """LLM Provider configuration model""" __tablename__ = "llm_providers" id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) provider_type: Mapped[str] = mapped_column(String(50), nullable=False, default="openai") base_url: Mapped[str] = mapped_column(String(500), nullable=False) api_key: Mapped[str] = mapped_column(String(500), nullable=False) default_model: Mapped[str] = mapped_column(String(100), nullable=False, default="gpt-4") max_tokens: Mapped[int] = mapped_column(Integer, default=8192) is_default: Mapped[bool] = mapped_column(Boolean, default=False) enabled: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now) user: Mapped["User"] = relationship("User", backref="llm_providers") def to_dict(self, include_key: bool = False): result = { "id": self.id, "user_id": self.user_id, "name": self.name, "provider_type": self.provider_type, "base_url": self.base_url, "default_model": self.default_model, "max_tokens": self.max_tokens, "is_default": self.is_default, "enabled": self.enabled, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None } if include_key: result["api_key"] = self.api_key return result class Project(Base): """Project model""" __tablename__ = "projects" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now) user: Mapped["User"] = relationship("User", backref="projects") class User(Base): """User model""" __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True) username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) email: Mapped[Optional[str]] = mapped_column(String(120), unique=True, nullable=True) password_hash: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) role: Mapped[str] = mapped_column(String(20), default="user") permission_level: Mapped[int] = mapped_column(Integer, default=1) workspace_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) conversations: Mapped[List["Conversation"]] = relationship( "Conversation", back_populates="user", cascade="all, delete-orphan" ) def to_dict(self): return { "id": self.id, "username": self.username, "email": self.email, "role": self.role, "permission_level": self.permission_level, "workspace_path": self.workspace_path, "is_active": self.is_active, "created_at": self.created_at.isoformat() if self.created_at else None } class Conversation(Base): """Conversation model""" __tablename__ = "conversations" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True) project_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("projects.id"), nullable=True) title: Mapped[str] = mapped_column(String(255), nullable=False) model: Mapped[str] = mapped_column(String(64), nullable=False, default="deepseek-chat") system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.") temperature: Mapped[float] = mapped_column(Float, default=0.7) max_tokens: Mapped[int] = mapped_column(Integer, default=2000) thinking_enabled: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now) user: Mapped["User"] = relationship("User", back_populates="conversations") provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider") messages: Mapped[List["Message"]] = relationship( "Message", back_populates="conversation", cascade="all, delete-orphan", primaryjoin="Conversation.id == foreign(Message.conversation_id)" ) def to_dict(self): return { "id": self.id, "user_id": self.user_id, "provider_id": self.provider_id, "project_id": self.project_id, "title": self.title, "model": self.model, "system_prompt": self.system_prompt, "temperature": self.temperature, "max_tokens": self.max_tokens, "thinking_enabled": self.thinking_enabled, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None } class Message(Base): """Message model 同时服务于普通会话和聊天室: - 普通会话:conversation_id 非空,room_id 为空 - 聊天室:room_id 非空,conversation_id 为空,sender_name/sender_color/round_number 有值 """ __tablename__ = "messages" id: Mapped[str] = mapped_column(String(64), primary_key=True) conversation_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=True) room_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("chat_rooms.id"), nullable=True) role: Mapped[str] = mapped_column(String(16), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False, default="") token_count: Mapped[int] = mapped_column(Integer, default=0) usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # 聊天室专属字段(普通会话为空) sender_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) sender_color: Mapped[Optional[str]] = mapped_column(String(7), nullable=True, default="#2563eb") round_number: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) conversation: Mapped[Optional["Conversation"]] = relationship("Conversation", back_populates="messages") room: Mapped[Optional["ChatRoom"]] = relationship("ChatRoom", back_populates="messages") def to_dict(self): import json result = { "id": self.id, "conversation_id": self.conversation_id, "room_id": self.room_id, "role": self.role, "token_count": self.token_count, "created_at": self.created_at.isoformat() if self.created_at else None } if self.usage: try: result["usage"] = json.loads(self.usage) except json.JSONDecodeError: result["usage"] = None # 聊天室专属字段 if self.sender_name: result["sender_name"] = self.sender_name if self.sender_color: result["sender_color"] = self.sender_color if self.round_number is not None: result["round_number"] = self.round_number try: content_obj = json.loads(self.content) if self.content else {} except json.JSONDecodeError: result["content"] = self.content result["text"] = self.content result["attachments"] = [] result["process_steps"] = [] return result steps = content_obj.get("steps", []) result["process_steps"] = steps text_content = "".join( s.get("content", "") for s in steps if s.get("type") == "text" ) result["text"] = text_content result["content"] = text_content result["attachments"] = content_obj.get("attachments", []) return result # ============ Chat Room Models ============ class ChatRoom(Base): """Multi-agent chat room model""" __tablename__ = "chat_rooms" id: Mapped[str] = mapped_column(String(64), primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) task: Mapped[str] = mapped_column(Text, nullable=False, default="") status: Mapped[str] = mapped_column(String(20), nullable=False, default="idle") # idle, running, paused, completed, error max_rounds: Mapped[int] = mapped_column(Integer, default=5) current_round: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now) user: Mapped["User"] = relationship("User", backref="chat_rooms") agents: Mapped[List["RoomAgent"]] = relationship( "RoomAgent", back_populates="room", cascade="all, delete-orphan", order_by="RoomAgent.turn_order" ) messages: Mapped[List["Message"]] = relationship( "Message", back_populates="room", cascade="all, delete-orphan", primaryjoin="ChatRoom.id == foreign(Message.room_id)", order_by="Message.created_at" ) def to_dict(self, include_messages: bool = False): result = { "id": self.id, "user_id": self.user_id, "title": self.title, "task": self.task, "status": self.status, "max_rounds": self.max_rounds, "current_round": self.current_round, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, "agents": [a.to_dict() for a in self.agents] } if include_messages: result["messages"] = [m.to_dict() for m in self.messages] return result class RoomAgent(Base): """Agent configuration in a chat room""" __tablename__ = "room_agents" id: Mapped[int] = mapped_column(Integer, primary_key=True) room_id: Mapped[str] = mapped_column(String(64), ForeignKey("chat_rooms.id"), nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) role: Mapped[str] = mapped_column(String(255), nullable=False, default="") provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True) model: Mapped[str] = mapped_column(String(100), nullable=False, default="") system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful AI assistant.") color: Mapped[str] = mapped_column(String(7), nullable=False, default="#2563eb") turn_order: Mapped[int] = mapped_column(Integer, default=0) room: Mapped["ChatRoom"] = relationship("ChatRoom", back_populates="agents") provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider") def to_dict(self): return { "id": self.id, "room_id": self.room_id, "name": self.name, "role": self.role, "provider_id": self.provider_id, "model": self.model, "system_prompt": self.system_prompt, "color": self.color, "turn_order": self.turn_order }