"""Chat-related models""" import json from datetime import datetime from typing import Optional, List, TYPE_CHECKING from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from luxx.core.database import Base if TYPE_CHECKING: from luxx.models.user import LLMProvider, User def local_now(): return datetime.now() 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), 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 helpful.") 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" ) 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): """Unified Message model for Conversation and ChatRoom. role: user/assistant/system/tool content: JSON format with text, attachments, tool_calls, steps """ __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), nullable=True) role: Mapped[str] = mapped_column(String(16), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False, default="") sender_name: Mapped[str] = mapped_column(String(50), nullable=False, default="") mentions: Mapped[Optional[str]] = mapped_column(Text, nullable=True) token_count: Mapped[int] = mapped_column(Integer, default=0) usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True) conversation: Mapped[Optional["Conversation"]] = relationship("Conversation", back_populates="messages") created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) @property def target_type(self) -> str: return "conversation" if self.conversation_id else "room" @property def target_id(self) -> str: return self.conversation_id or self.room_id or "" def to_dict(self): result = { "id": self.id, "conversation_id": self.conversation_id, "room_id": self.room_id, "target_type": self.target_type, "target_id": self.target_id, "role": self.role, "sender_name": self.sender_name, "token_count": self.token_count, "created_at": self.created_at.isoformat() if self.created_at else None } # Parse usage JSON if self.usage: try: result["usage"] = json.loads(self.usage) except json.JSONDecodeError: result["usage"] = None # Parse mentions JSON if self.mentions: try: result["mentions"] = json.loads(self.mentions) except json.JSONDecodeError: result["mentions"] = [] else: result["mentions"] = [] # Parse content JSON try: content_obj = json.loads(self.content) if self.content else {} except json.JSONDecodeError: result["text"] = self.content result["attachments"] = [] result["tool_calls"] = [] result["process_steps"] = [] return result result["text"] = content_obj.get("text", "") result["attachments"] = content_obj.get("attachments", []) result["tool_calls"] = content_obj.get("tool_calls", []) result["process_steps"] = content_obj.get("steps", []) if "content" not in content_obj: result["content"] = result["text"] return result