Luxx/luxx/models/chat.py

135 lines
5.3 KiB
Python

"""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