Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions astrbot/builtin_stars/builtin_commands/commands/persona.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
import builtins
from typing import TYPE_CHECKING

from astrbot.api import sp, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult

if TYPE_CHECKING:
from astrbot.core.db.po import Persona


class PersonaCommands:
def __init__(self, context: star.Context):
self.context = context

def _build_tree_output(
self,
folder_tree: list[dict],
all_personas: list["Persona"],
depth: int = 0,
) -> list[str]:
"""递归构建树状输出,使用短线条表示层级"""
lines: list[str] = []
# 使用短线条作为缩进前缀,每层只用 "│" 加一个空格
prefix = "│ " * depth

for folder in folder_tree:
# 输出文件夹
lines.append(f"{prefix}├ 📁 {folder['name']}/")

# 获取该文件夹下的人格
folder_personas = [
p for p in all_personas if p.folder_id == folder["folder_id"]
]
child_prefix = "│ " * (depth + 1)

# 输出该文件夹下的人格
for persona in folder_personas:
lines.append(f"{child_prefix}├ 👤 {persona.persona_id}")

# 递归处理子文件夹
children = folder.get("children", [])
if children:
lines.extend(
self._build_tree_output(
children,
all_personas,
depth + 1,
)
)

return lines

async def persona(self, message: AstrMessageEvent):
l = message.message_str.split(" ") # noqa: E741
umo = message.unified_msg_origin
Expand Down Expand Up @@ -69,12 +111,32 @@ async def persona(self, message: AstrMessageEvent):
.use_t2i(False),
)
elif l[1] == "list":
parts = ["人格列表:\n"]
for persona in self.context.provider_manager.personas:
parts.append(f"- {persona['name']}\n")
parts.append("\n\n*输入 `/persona view 人格名` 查看人格详细信息")
msg = "".join(parts)
message.set_result(MessageEventResult().message(msg))
# 获取文件夹树和所有人格
folder_tree = await self.context.persona_manager.get_folder_tree()
all_personas = self.context.persona_manager.personas

lines = ["📂 人格列表:\n"]

# 构建树状输出
tree_lines = self._build_tree_output(folder_tree, all_personas)
lines.extend(tree_lines)

# 输出根目录下的人格(没有文件夹的)
root_personas = [p for p in all_personas if p.folder_id is None]
if root_personas:
if tree_lines: # 如果有文件夹内容,加个空行
lines.append("")
for persona in root_personas:
lines.append(f"👤 {persona.persona_id}")

# 统计信息
total_count = len(all_personas)
lines.append(f"\n共 {total_count} 个人格")
lines.append("\n*使用 `/persona <人格名>` 设置人格")
lines.append("*使用 `/persona view <人格名>` 查看详细信息")

msg = "\n".join(lines)
message.set_result(MessageEventResult().message(msg).use_t2i(False))
elif l[1] == "view":
if len(l) == 2:
message.set_result(MessageEventResult().message("请输入人格情景名"))
Expand Down
92 changes: 91 additions & 1 deletion astrbot/core/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CommandConflict,
ConversationV2,
Persona,
PersonaFolder,
PlatformMessageHistory,
PlatformSession,
PlatformStat,
Expand Down Expand Up @@ -251,8 +252,19 @@ async def insert_persona(
system_prompt: str,
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
folder_id: str | None = None,
sort_order: int = 0,
) -> Persona:
"""Insert a new persona record."""
"""Insert a new persona record.

Args:
persona_id: Unique identifier for the persona
system_prompt: System prompt for the persona
begin_dialogs: Optional list of initial dialog strings
tools: Optional list of tool names (None means all tools, [] means no tools)
folder_id: Optional folder ID to place the persona in (None means root)
sort_order: Sort order within the folder (default 0)
"""
...

@abc.abstractmethod
Expand Down Expand Up @@ -281,6 +293,84 @@ async def delete_persona(self, persona_id: str) -> None:
"""Delete a persona by its ID."""
...

# ====
# Persona Folder Management
# ====

@abc.abstractmethod
async def insert_persona_folder(
self,
name: str,
parent_id: str | None = None,
description: str | None = None,
sort_order: int = 0,
) -> PersonaFolder:
"""Insert a new persona folder."""
...

@abc.abstractmethod
async def get_persona_folder_by_id(self, folder_id: str) -> PersonaFolder | None:
"""Get a persona folder by its folder_id."""
...

@abc.abstractmethod
async def get_persona_folders(
self, parent_id: str | None = None
) -> list[PersonaFolder]:
"""Get all persona folders, optionally filtered by parent_id."""
...

@abc.abstractmethod
async def get_all_persona_folders(self) -> list[PersonaFolder]:
"""Get all persona folders."""
...

@abc.abstractmethod
async def update_persona_folder(
self,
folder_id: str,
name: str | None = None,
parent_id: T.Any = None,
description: T.Any = None,
sort_order: int | None = None,
) -> PersonaFolder | None:
"""Update a persona folder."""
...

@abc.abstractmethod
async def delete_persona_folder(self, folder_id: str) -> None:
"""Delete a persona folder by its folder_id."""
...

@abc.abstractmethod
async def move_persona_to_folder(
self, persona_id: str, folder_id: str | None
) -> Persona | None:
"""Move a persona to a folder (or root if folder_id is None)."""
...

@abc.abstractmethod
async def get_personas_by_folder(
self, folder_id: str | None = None
) -> list[Persona]:
"""Get all personas in a specific folder."""
...

@abc.abstractmethod
async def batch_update_sort_order(
self,
items: list[dict],
) -> None:
"""Batch update sort_order for personas and/or folders.

Args:
items: List of dicts with keys:
- id: The persona_id or folder_id
- type: Either "persona" or "folder"
- sort_order: The new sort_order value
"""
...

@abc.abstractmethod
async def insert_preference_or_update(
self,
Expand Down
42 changes: 42 additions & 0 deletions astrbot/core/db/po.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,44 @@ class ConversationV2(SQLModel, table=True):
)


class PersonaFolder(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)
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": datetime.now(timezone.utc)},
)

__table_args__ = (
UniqueConstraint(
"folder_id",
name="uix_persona_folder_id",
),
)


class Persona(SQLModel, table=True):
"""Persona is a set of instructions for LLMs to follow.

Expand All @@ -87,6 +125,10 @@ class Persona(SQLModel, table=True):
"""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."""
folder_id: str | None = Field(default=None, max_length=36)
"""所属文件夹ID,NULL 表示在根目录"""
sort_order: int = Field(default=0)
"""排序顺序"""
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
Expand Down
Loading