"""Chat-related models""" 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.core.database import Base 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), 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) # Relationships 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. content 字段统一使用 JSON 格式存储: - text: 文本内容 - attachments: 附件列表 - tool_calls: 工具调用列表 - steps: 处理步骤列表 participant_id 统一发送者格式: - "user:{id}" for users - "agent:{id}" for agents """ __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) participant_id: Mapped[str] = mapped_column(String(64), nullable=False) # "user:123" or "agent:abc" role: Mapped[str] = mapped_column(String(16), nullable=False) # user/assistant/system/tool 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) # JSON array token_count: Mapped[int] = mapped_column(Integer, default=0) usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) # Relationships conversation: Mapped[Optional["Conversation"]] = relationship("Conversation", back_populates="messages") @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 "" @property def sender_type(self) -> str: return self.participant_id.split(":")[0] if ":" in self.participant_id else "user" @property def sender_id(self) -> str: return self.participant_id.split(":")[1] if ":" in self.participant_id else self.participant_id def to_dict(self): """Convert to dictionary, extracting process_steps for frontend""" import json result = { "id": self.id, "conversation_id": self.conversation_id, "room_id": self.room_id, "target_type": self.target_type, "target_id": self.target_id, "participant_id": self.participant_id, "role": self.role, "sender_type": self.sender_type, "sender_id": self.sender_id, "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["content"] = self.content 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 result: result["content"] = result["text"] return result