Luxx/luxx/models.py

286 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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