"""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") # openai, deepseek, glm, etc. 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) # 默认 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) # Relationships user: Mapped["User"] = relationship("User", backref="llm_providers") def to_dict(self, include_key: bool = False): """Convert to dictionary, optionally include API key""" 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) # Relationships 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") is_active: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) # Relationships 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, "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) # 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): """Message model content 字段统一使用 JSON 格式存储: **User 消息:** { "text": "用户输入的文本内容", "attachments": [ {"name": "utils.py", "extension": "py", "content": "..."} ] } **Assistant 消息:** { "text": "AI 回复的文本内容", "tool_calls": [...], // 遗留的扁平结构 "steps": [ // 有序步骤,用于渲染(主要数据源) {"id": "step-0", "index": 0, "type": "thinking", "content": "..."}, {"id": "step-1", "index": 1, "type": "text", "content": "..."}, {"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_xxx", "name": "...", "arguments": "..."}, {"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_xxx", "name": "...", "content": "..."} ] } """ __tablename__ = "messages" id: Mapped[str] = mapped_column(String(64), primary_key=True) conversation_id: Mapped[str] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=False) role: Mapped[str] = mapped_column(String(16), nullable=False) # user, assistant, system, tool 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) # JSON string for usage info created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now) # Relationships conversation: Mapped["Conversation"] = relationship("Conversation", back_populates="messages") def to_dict(self): """Convert to dictionary, extracting process_steps for frontend""" import json result = { "id": self.id, "conversation_id": self.conversation_id, "role": self.role, "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 content JSON try: content_obj = json.loads(self.content) if self.content else {} except json.JSONDecodeError: # Legacy plain text content result["content"] = self.content result["text"] = self.content result["attachments"] = [] result["tool_calls"] = [] result["process_steps"] = [] return result # Extract common fields result["text"] = content_obj.get("text", "") result["attachments"] = content_obj.get("attachments", []) result["tool_calls"] = content_obj.get("tool_calls", []) # Extract steps as process_steps for frontend rendering result["process_steps"] = content_obj.get("steps", []) # For backward compatibility if "content" not in result: result["content"] = result["text"] return result