"""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. 统一消息模型,支持: - Conversation: 单人会话 - ChatRoom: 聊天室(多 Agent) sender_type: user | agent | system content: JSON 格式 {"text": "...", "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) # 发送者信息 sender_id: Mapped[str] = mapped_column(String(64), nullable=False, default="") # 用户ID 或 AgentID sender_type: Mapped[str] = mapped_column(String(16), nullable=False, default="user") # "user" | "agent" | "system" sender_name: Mapped[str] = mapped_column(String(50), nullable=False, default="") # 消息内容(兼容旧格式,同时保留 role 字段用于兼容 Conversation) role: Mapped[str] = mapped_column(String(16), nullable=False, default="user") # 保留,兼容 Conversation content: Mapped[str] = mapped_column(Text, nullable=False, default="") # 流式响应元数据 is_streaming: Mapped[bool] = mapped_column(Boolean, default=False) stream_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) parent_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) # 回复的消息ID # 元数据 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, include_stream_data: bool = False): """转换消息为字典 统一使用 sender 格式,content 保留 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, "sender": { "id": self.sender_id, "type": self.sender_type, "name": self.sender_name }, # 兼容字段 "sender_id": self.sender_id, "sender_type": self.sender_type, "sender_name": self.sender_name, "role": self.role, # 保留,兼容 Conversation "token_count": self.token_count, "is_streaming": self.is_streaming, "created_at": self.created_at.isoformat() if self.created_at else None } # 流式数据 if include_stream_data: result["stream_id"] = self.stream_id result["parent_id"] = self.parent_id # 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 - 提取 text 和 steps try: content_obj = json.loads(self.content) if self.content else {} result["content"] = content_obj.get("text", content_obj.get("content", self.content)) result["text"] = result["content"] result["process_steps"] = content_obj.get("steps", content_obj.get("process_steps", [])) result["attachments"] = content_obj.get("attachments", []) result["tool_calls"] = content_obj.get("tool_calls", []) except json.JSONDecodeError: # 纯文本内容 result["text"] = self.content result["content"] = self.content result["process_steps"] = [] result["attachments"] = [] result["tool_calls"] = [] return result