import uuid from dataclasses import dataclass, field from datetime import datetime, timezone from typing import TypedDict from sqlmodel import JSON, Field, SQLModel, Text, UniqueConstraint class TimestampMixin(SQLModel): created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), sa_column_kwargs={"onupdate": lambda: datetime.now(timezone.utc)}, ) class PlatformStat(SQLModel, table=True): """This class represents the statistics of bot usage across different platforms. Note: In astrbot v4, we moved `platform` table to here. """ __tablename__: str = "platform_stats" id: int = Field(primary_key=True, sa_column_kwargs={"autoincrement": True}) timestamp: datetime = Field(nullable=False) platform_id: str = Field(nullable=False) platform_type: str = Field(nullable=False) # such as "aiocqhttp", "slack", etc. count: int = Field(default=0, nullable=False) __table_args__ = ( UniqueConstraint( "timestamp", "platform_id", "platform_type", name="uix_platform_stats", ), ) class ConversationV2(TimestampMixin, SQLModel, table=True): __tablename__: str = "conversations" inner_conversation_id: int | None = Field( default=None, primary_key=True, sa_column_kwargs={"autoincrement": True}, ) conversation_id: str = Field( max_length=36, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) platform_id: str = Field(nullable=False) user_id: str = Field(nullable=False) content: list | None = Field(default=None, sa_type=JSON) title: str | None = Field(default=None, max_length=255) persona_id: str | None = Field(default=None) token_usage: int = Field(default=0, nullable=False) """content is a list of OpenAI-formated messages in list[dict] format. token_usage is the total token value of the messages. when 0, will use estimated token counter. """ __table_args__ = ( UniqueConstraint( "conversation_id", name="uix_conversation_id", ), ) class PersonaFolder(TimestampMixin, SQLModel, table=True): """Persona 文件夹,支持递归层级结构。 用于组织和管理多个 Persona,类似于文件系统的目录结构。 """ __tablename__: str = "persona_folders" id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) folder_id: str = Field( max_length=36, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) name: str = Field(max_length=255, nullable=False) parent_id: str | None = Field(default=None, max_length=36) """父文件夹ID,NULL表示根目录""" description: str | None = Field(default=None, sa_type=Text) sort_order: int = Field(default=0) __table_args__ = ( UniqueConstraint( "folder_id", name="uix_persona_folder_id", ), ) class Persona(TimestampMixin, SQLModel, table=True): """Persona is a set of instructions for LLMs to follow. It can be used to customize the behavior of LLMs. """ __tablename__: str = "personas" id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) persona_id: str = Field(max_length=255, nullable=False) system_prompt: str = Field(sa_type=Text, nullable=False) begin_dialogs: list | None = Field(default=None, sa_type=JSON) """a list of strings, each representing a dialog to start with""" tools: list | None = Field(default=None, sa_type=JSON) """None means use ALL tools for default, empty list means no tools, otherwise a list of tool names.""" skills: list | None = Field(default=None, sa_type=JSON) """None means use ALL skills for default, empty list means no skills, otherwise a list of skill names.""" custom_error_message: str | None = Field(default=None, sa_type=Text) """Optional custom error message sent to end users when the agent request fails.""" folder_id: str | None = Field(default=None, max_length=36) """所属文件夹ID,NULL 表示在根目录""" sort_order: int = Field(default=0) """排序顺序""" __table_args__ = ( UniqueConstraint( "persona_id", name="uix_persona_id", ), ) class CronJob(TimestampMixin, SQLModel, table=True): """Cron job definition for scheduler and WebUI management.""" __tablename__: str = "cron_jobs" id: int | None = Field( default=None, primary_key=True, sa_column_kwargs={"autoincrement": True}, ) job_id: str = Field( max_length=64, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) name: str = Field(max_length=255, nullable=False) description: str | None = Field(default=None, sa_type=Text) job_type: str = Field(max_length=32, nullable=False) # basic | active_agent cron_expression: str | None = Field(default=None, max_length=255) timezone: str | None = Field(default=None, max_length=64) payload: dict = Field(default_factory=dict, sa_type=JSON) enabled: bool = Field(default=True) persistent: bool = Field(default=True) run_once: bool = Field(default=False) status: str = Field(default="scheduled", max_length=32) last_run_at: datetime | None = Field(default=None) next_run_time: datetime | None = Field(default=None) last_error: str | None = Field(default=None, sa_type=Text) class Preference(TimestampMixin, SQLModel, table=True): """This class represents preferences for bots.""" __tablename__: str = "preferences" id: int | None = Field( default=None, primary_key=True, sa_column_kwargs={"autoincrement": True}, ) scope: str = Field(nullable=False) """Scope of the preference, such as 'global', 'umo', 'plugin'.""" scope_id: str = Field(nullable=False) """ID of the scope, such as 'global', 'umo', 'plugin_name'.""" key: str = Field(nullable=False) value: dict = Field(sa_type=JSON, nullable=False) __table_args__ = ( UniqueConstraint( "scope", "scope_id", "key", name="uix_preference_scope_scope_id_key", ), ) class PlatformMessageHistory(TimestampMixin, SQLModel, table=True): """This class represents the message history for a specific platform. It is used to store messages that are not LLM-generated, such as user messages or platform-specific messages. """ __tablename__: str = "platform_message_history" id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) platform_id: str = Field(nullable=False) user_id: str = Field(nullable=False) # An id of group, user in platform sender_id: str | None = Field(default=None) # ID of the sender in the platform sender_name: str | None = Field( default=None, ) # Name of the sender in the platform content: dict = Field(sa_type=JSON, nullable=False) # a message chain list class PlatformSession(TimestampMixin, SQLModel, table=True): """Platform session table for managing user sessions across different platforms. A session represents a chat window for a specific user on a specific platform. Each session can have multiple conversations (对话) associated with it. """ __tablename__: str = "platform_sessions" inner_id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) session_id: str = Field( max_length=100, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) platform_id: str = Field(default="webchat", nullable=False) """Platform identifier (e.g., 'webchat', 'qq', 'discord')""" creator: str = Field(nullable=False) """Username of the session creator""" display_name: str | None = Field(default=None, max_length=255) """Display name for the session""" is_group: int = Field(default=0, nullable=False) """0 for private chat, 1 for group chat (not implemented yet)""" __table_args__ = ( UniqueConstraint( "session_id", name="uix_platform_session_id", ), ) class Attachment(TimestampMixin, SQLModel, table=True): """This class represents attachments for messages in AstrBot. Attachments can be images, files, or other media types. """ __tablename__: str = "attachments" inner_attachment_id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) attachment_id: str = Field( max_length=36, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) path: str = Field(nullable=False) # Path to the file on disk type: str = Field(nullable=False) # Type of the file (e.g., 'image', 'file') mime_type: str = Field(nullable=False) # MIME type of the file __table_args__ = ( UniqueConstraint( "attachment_id", name="uix_attachment_id", ), ) class ApiKey(TimestampMixin, SQLModel, table=True): """API keys used by external developers to access Open APIs.""" __tablename__: str = "api_keys" inner_id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) key_id: str = Field( max_length=36, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) name: str = Field(max_length=255, nullable=False) key_hash: str = Field(max_length=128, nullable=False, unique=True) key_prefix: str = Field(max_length=24, nullable=False) scopes: list | None = Field(default=None, sa_type=JSON) created_by: str = Field(max_length=255, nullable=False) last_used_at: datetime | None = Field(default=None) expires_at: datetime | None = Field(default=None) revoked_at: datetime | None = Field(default=None) __table_args__ = ( UniqueConstraint( "key_id", name="uix_api_key_id", ), UniqueConstraint( "key_hash", name="uix_api_key_hash", ), ) class ChatUIProject(TimestampMixin, SQLModel, table=True): """This class represents projects for organizing ChatUI conversations. Projects allow users to group related conversations together. """ __tablename__: str = "chatui_projects" inner_id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) project_id: str = Field( max_length=36, nullable=False, unique=True, default_factory=lambda: str(uuid.uuid4()), ) creator: str = Field(nullable=False) """Username of the project creator""" emoji: str | None = Field(default="📁", max_length=10) """Emoji icon for the project""" title: str = Field(nullable=False, max_length=255) """Title of the project""" description: str | None = Field(default=None, max_length=1000) """Description of the project""" __table_args__ = ( UniqueConstraint( "project_id", name="uix_chatui_project_id", ), ) class SessionProjectRelation(SQLModel, table=True): """This class represents the relationship between platform sessions and ChatUI projects.""" __tablename__: str = "session_project_relations" id: int | None = Field( primary_key=True, sa_column_kwargs={"autoincrement": True}, default=None, ) session_id: str = Field(nullable=False, max_length=100) """Session ID from PlatformSession""" project_id: str = Field(nullable=False, max_length=36) """Project ID from ChatUIProject""" __table_args__ = ( UniqueConstraint( "session_id", name="uix_session_project_relation", ), ) class CommandConfig(TimestampMixin, SQLModel, table=True): """Per-command configuration overrides for dashboard management.""" __tablename__ = "command_configs" # type: ignore handler_full_name: str = Field( primary_key=True, max_length=512, ) plugin_name: str = Field(nullable=False, max_length=255) module_path: str = Field(nullable=False, max_length=255) original_command: str = Field(nullable=False, max_length=255) resolved_command: str | None = Field(default=None, max_length=255) enabled: bool = Field(default=True, nullable=False) keep_original_alias: bool = Field(default=False, nullable=False) conflict_key: str | None = Field(default=None, max_length=255) resolution_strategy: str | None = Field(default=None, max_length=64) note: str | None = Field(default=None, sa_type=Text) extra_data: dict | None = Field(default=None, sa_type=JSON) auto_managed: bool = Field(default=False, nullable=False) class CommandConflict(TimestampMixin, SQLModel, table=True): """Conflict tracking for duplicated command names.""" __tablename__ = "command_conflicts" # type: ignore id: int | None = Field( default=None, primary_key=True, sa_column_kwargs={"autoincrement": True} ) conflict_key: str = Field(nullable=False, max_length=255) handler_full_name: str = Field(nullable=False, max_length=512) plugin_name: str = Field(nullable=False, max_length=255) status: str = Field(default="pending", max_length=32) resolution: str | None = Field(default=None, max_length=64) resolved_command: str | None = Field(default=None, max_length=255) note: str | None = Field(default=None, sa_type=Text) extra_data: dict | None = Field(default=None, sa_type=JSON) auto_generated: bool = Field(default=False, nullable=False) __table_args__ = ( UniqueConstraint( "conflict_key", "handler_full_name", name="uix_conflict_handler", ), ) @dataclass class Conversation: """LLM 对话类 对于 WebChat,history 存储了包括指令、回复、图片等在内的所有消息。 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 在 v4.0.0 版本及之后,WebChat 的历史记录被迁移至 `PlatformMessageHistory` 表中, """ platform_id: str user_id: str cid: str """对话 ID, 是 uuid 格式的字符串""" history: str = "" """字符串格式的对话列表。""" title: str | None = "" persona_id: str | None = "" created_at: int = 0 updated_at: int = 0 token_usage: int = 0 """对话的总 token 数量。AstrBot 会保留最近一次 LLM 请求返回的总 token 数,方便统计。token_usage 可能为 0,表示未知。""" class Personality(TypedDict): """LLM 人格类。 在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。 """ prompt: str name: str begin_dialogs: list[str] mood_imitation_dialogs: list[str] """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。""" tools: list[str] | None """工具列表。None 表示使用所有工具,空列表表示不使用任何工具""" skills: list[str] | None """Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills""" custom_error_message: str | None """可选的人格自定义报错回复信息。配置后将优先发送给最终用户。""" # cache _begin_dialogs_processed: list[dict] _mood_imitation_dialogs_processed: str # ==== # Deprecated, and will be removed in future versions. # ==== @dataclass class Platform: """平台使用统计数据""" name: str count: int timestamp: int @dataclass class Stats: platform: list[Platform] = field(default_factory=list)