feat: support persona custom error reply message with fallback (#5547)

* feat: support persona custom error reply message with fallback

* refactor: centralize persona custom error message helpers
This commit is contained in:
エイカク
2026-02-28 13:34:12 +09:00
committed by GitHub
parent 881b409ebc
commit 7bf44bd8d2
19 changed files with 269 additions and 24 deletions
@@ -23,6 +23,9 @@ from astrbot.core.message.components import Json
from astrbot.core.message.message_event_result import (
MessageChain,
)
from astrbot.core.persona_error_reply import (
extract_persona_custom_error_message_from_event,
)
from astrbot.core.provider.entities import (
LLMResponse,
ProviderRequest,
@@ -78,6 +81,11 @@ class FollowUpTicket:
class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
def _get_persona_custom_error_message(self) -> str | None:
"""Read persona-level custom error message from event extras when available."""
event = getattr(self.run_context.context, "event", None)
return extract_persona_custom_error_message_from_event(event)
@override
async def reset(
self,
@@ -463,12 +471,14 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
self.stats.end_time = time.time()
self._transition_state(AgentState.ERROR)
self._resolve_unconsumed_follow_ups()
custom_error_message = self._get_persona_custom_error_message()
error_text = custom_error_message or (
f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}"
)
yield AgentResponse(
type="err",
data=AgentResponseData(
chain=MessageChain().message(
f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}",
),
chain=MessageChain().message(error_text),
),
)
return
+13 -1
View File
@@ -14,6 +14,9 @@ from astrbot.core.message.message_event_result import (
MessageEventResult,
ResultContentType,
)
from astrbot.core.persona_error_reply import (
extract_persona_custom_error_message_from_event,
)
from astrbot.core.provider.entities import LLMResponse
from astrbot.core.provider.provider import TTSProvider
@@ -235,7 +238,16 @@ async def run_agent(
pass
logger.error(traceback.format_exc())
err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
custom_error_message = extract_persona_custom_error_message_from_event(
astr_event
)
if custom_error_message:
err_msg = custom_error_message
else:
err_msg = (
f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: "
f"{e!s}\n\n请在平台日志查看和分享错误详情。\n"
)
error_llm_response = LLMResponse(
role="err",
+8
View File
@@ -37,6 +37,10 @@ from astrbot.core.astr_main_agent_resources import (
)
from astrbot.core.conversation_mgr import Conversation
from astrbot.core.message.components import File, Image, Reply
from astrbot.core.persona_error_reply import (
extract_persona_custom_error_message_from_persona,
set_persona_custom_error_message_on_event,
)
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider import Provider
from astrbot.core.provider.entities import ProviderRequest
@@ -285,6 +289,10 @@ async def _ensure_persona_and_skills(
provider_settings=cfg,
)
set_persona_custom_error_message_on_event(
event, extract_persona_custom_error_message_from_persona(persona)
)
if persona:
# Inject persona system prompt
if prompt := persona["prompt"]:
+3
View File
@@ -306,6 +306,7 @@ class BaseDatabase(abc.ABC):
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
skills: list[str] | None = None,
custom_error_message: str | None = None,
folder_id: str | None = None,
sort_order: int = 0,
) -> Persona:
@@ -317,6 +318,7 @@ class BaseDatabase(abc.ABC):
begin_dialogs: Optional list of initial dialog strings
tools: Optional list of tool names (None means all tools, [] means no tools)
skills: Optional list of skill names (None means all skills, [] means no skills)
custom_error_message: Optional persona-level fallback error message
folder_id: Optional folder ID to place the persona in (None means root)
sort_order: Sort order within the folder (default 0)
"""
@@ -340,6 +342,7 @@ class BaseDatabase(abc.ABC):
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
skills: list[str] | None = None,
custom_error_message: str | None = None,
) -> Persona | None:
"""Update a persona's system prompt or begin dialogs."""
...
+4
View File
@@ -126,6 +126,8 @@ class Persona(TimestampMixin, SQLModel, table=True):
"""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)
"""所属文件夹IDNULL 表示在根目录"""
sort_order: int = Field(default=0)
@@ -472,6 +474,8 @@ class Personality(TypedDict):
"""工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
skills: list[str] | None
"""Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills"""
custom_error_message: str | None
"""可选的人格自定义报错回复信息。配置后将优先发送给最终用户。"""
# cache
_begin_dialogs_processed: list[dict]
+17 -1
View File
@@ -32,8 +32,8 @@ from astrbot.core.db.po import (
from astrbot.core.db.po import (
Stats as DeprecatedStats,
)
from astrbot.core.sentinels import NOT_GIVEN
NOT_GIVEN = T.TypeVar("NOT_GIVEN")
TxResult = T.TypeVar("TxResult")
CRON_FIELD_NOT_SET = object()
@@ -58,6 +58,7 @@ class SQLiteDatabase(BaseDatabase):
# 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容)
await self._ensure_persona_folder_columns(conn)
await self._ensure_persona_skills_column(conn)
await self._ensure_persona_custom_error_message_column(conn)
await conn.commit()
async def _ensure_persona_folder_columns(self, conn) -> None:
@@ -92,6 +93,16 @@ class SQLiteDatabase(BaseDatabase):
if "skills" not in columns:
await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON"))
async def _ensure_persona_custom_error_message_column(self, conn) -> None:
"""确保 personas 表有 custom_error_message 列。"""
result = await conn.execute(text("PRAGMA table_info(personas)"))
columns = {row[1] for row in result.fetchall()}
if "custom_error_message" not in columns:
await conn.execute(
text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT")
)
# ====
# Platform Statistics
# ====
@@ -675,6 +686,7 @@ class SQLiteDatabase(BaseDatabase):
begin_dialogs=None,
tools=None,
skills=None,
custom_error_message=None,
folder_id=None,
sort_order=0,
):
@@ -688,6 +700,7 @@ class SQLiteDatabase(BaseDatabase):
begin_dialogs=begin_dialogs or [],
tools=tools,
skills=skills,
custom_error_message=custom_error_message,
folder_id=folder_id,
sort_order=sort_order,
)
@@ -719,6 +732,7 @@ class SQLiteDatabase(BaseDatabase):
begin_dialogs=None,
tools=NOT_GIVEN,
skills=NOT_GIVEN,
custom_error_message=NOT_GIVEN,
):
"""Update a persona's system prompt or begin dialogs."""
async with self.get_db() as session:
@@ -734,6 +748,8 @@ class SQLiteDatabase(BaseDatabase):
values["tools"] = tools
if skills is not NOT_GIVEN:
values["skills"] = skills
if custom_error_message is not NOT_GIVEN:
values["custom_error_message"] = custom_error_message
if not values:
return None
query = query.values(**values)
+86
View File
@@ -0,0 +1,86 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
PERSONA_CUSTOM_ERROR_MESSAGE_EXTRA_KEY = "persona_custom_error_message"
def normalize_persona_custom_error_message(value: object) -> str | None:
"""Normalize persona custom error reply text."""
if not isinstance(value, str):
return None
message = value.strip()
return message or None
def extract_persona_custom_error_message_from_persona(
persona: Mapping[str, Any] | None,
) -> str | None:
"""Extract normalized custom error reply text from persona mapping."""
if persona is None:
return None
return normalize_persona_custom_error_message(persona.get("custom_error_message"))
def extract_persona_custom_error_message_from_event(event: Any) -> str | None:
"""Extract normalized custom error reply text from event extras."""
try:
if event is None or not hasattr(event, "get_extra"):
return None
raw_message = event.get_extra(PERSONA_CUSTOM_ERROR_MESSAGE_EXTRA_KEY)
return normalize_persona_custom_error_message(raw_message)
except Exception:
return None
def set_persona_custom_error_message_on_event(
event: Any, message: object
) -> str | None:
"""Normalize and store persona custom error reply text into event extras."""
normalized = normalize_persona_custom_error_message(message)
try:
if event is not None and hasattr(event, "set_extra"):
event.set_extra(PERSONA_CUSTOM_ERROR_MESSAGE_EXTRA_KEY, normalized)
except Exception:
pass
return normalized
async def resolve_persona_custom_error_message(
*,
event: Any,
persona_manager: Any,
provider_settings: dict | None = None,
conversation_persona_id: str | None = None,
) -> str | None:
"""Resolve normalized custom error reply text for the selected persona."""
(
_persona_id,
persona,
_force_applied_persona_id,
_use_webchat_special_default,
) = await persona_manager.resolve_selected_persona(
umo=event.unified_msg_origin,
conversation_persona_id=conversation_persona_id,
platform_name=event.get_platform_name(),
provider_settings=provider_settings,
)
return extract_persona_custom_error_message_from_persona(persona)
async def resolve_event_conversation_persona_id(
event: Any, conversation_manager: Any
) -> str | None:
"""Resolve current conversation persona_id from event and conversation manager."""
curr_cid = await conversation_manager.get_curr_conversation_id(
event.unified_msg_origin
)
if not curr_cid:
return None
conversation = await conversation_manager.get_conversation(
event.unified_msg_origin, curr_cid
)
if not conversation:
return None
return conversation.persona_id
+18 -4
View File
@@ -4,6 +4,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
from astrbot.core.db import BaseDatabase
from astrbot.core.db.po import Persona, PersonaFolder, Personality
from astrbot.core.platform.message_session import MessageSession
from astrbot.core.sentinels import NOT_GIVEN
DEFAULT_PERSONALITY = Personality(
prompt="You are a helpful and friendly assistant.",
@@ -12,6 +13,7 @@ DEFAULT_PERSONALITY = Personality(
mood_imitation_dialogs=[],
tools=None,
skills=None,
custom_error_message=None,
_begin_dialogs_processed=[],
_mood_imitation_dialogs_processed="",
)
@@ -126,19 +128,27 @@ class PersonaManager:
persona_id: str,
system_prompt: str | None = None,
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
skills: list[str] | None = None,
tools: list[str] | None | object = NOT_GIVEN,
skills: list[str] | None | object = NOT_GIVEN,
custom_error_message: str | None | object = NOT_GIVEN,
):
"""更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具"""
existing_persona = await self.db.get_persona_by_id(persona_id)
if not existing_persona:
raise ValueError(f"Persona with ID {persona_id} does not exist.")
update_kwargs = {}
if tools is not NOT_GIVEN:
update_kwargs["tools"] = tools
if skills is not NOT_GIVEN:
update_kwargs["skills"] = skills
if custom_error_message is not NOT_GIVEN:
update_kwargs["custom_error_message"] = custom_error_message
persona = await self.db.update_persona(
persona_id,
system_prompt,
begin_dialogs,
tools=tools,
skills=skills,
**update_kwargs,
)
if persona:
for i, p in enumerate(self.personas):
@@ -298,6 +308,7 @@ class PersonaManager:
begin_dialogs: list[str] | None = None,
tools: list[str] | None = None,
skills: list[str] | None = None,
custom_error_message: str | None = None,
folder_id: str | None = None,
sort_order: int = 0,
) -> Persona:
@@ -320,6 +331,7 @@ class PersonaManager:
begin_dialogs,
tools=tools,
skills=skills,
custom_error_message=custom_error_message,
folder_id=folder_id,
sort_order=sort_order,
)
@@ -346,6 +358,7 @@ class PersonaManager:
"mood_imitation_dialogs": [], # deprecated
"tools": persona.tools,
"skills": persona.skills,
"custom_error_message": persona.custom_error_message,
}
for persona in self.personas
]
@@ -402,6 +415,7 @@ class PersonaManager:
begin_dialogs=selected_default_persona["begin_dialogs"],
tools=selected_default_persona["tools"] or None,
skills=selected_default_persona["skills"] or None,
custom_error_message=selected_default_persona["custom_error_message"],
)
return v3_persona_config, personas_v3, selected_default_persona
@@ -19,6 +19,9 @@ from astrbot.core.message.message_event_result import (
MessageEventResult,
ResultContentType,
)
from astrbot.core.persona_error_reply import (
extract_persona_custom_error_message_from_event,
)
from astrbot.core.pipeline.stage import Stage
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.provider.entities import (
@@ -366,11 +369,13 @@ class InternalAgentSubStage(Stage):
except Exception as e:
logger.error(f"Error occurred while processing agent: {e}")
await event.send(
MessageChain().message(
f"Error occurred while processing agent request: {e}"
)
custom_error_message = extract_persona_custom_error_message_from_event(
event
)
error_text = custom_error_message or (
f"Error occurred while processing agent request: {e}"
)
await event.send(MessageChain().message(error_text))
finally:
if follow_up_capture:
await finalize_follow_up_capture(
@@ -15,6 +15,11 @@ from astrbot.core.message.message_event_result import (
MessageEventResult,
ResultContentType,
)
from astrbot.core.persona_error_reply import (
resolve_event_conversation_persona_id,
resolve_persona_custom_error_message,
set_persona_custom_error_message_on_event,
)
if TYPE_CHECKING:
from astrbot.core.agent.runners.base import BaseAgentRunner
@@ -39,6 +44,7 @@ AGENT_RUNNER_TYPE_KEY = {
async def run_third_party_agent(
runner: "BaseAgentRunner",
stream_to_general: bool = False,
custom_error_message: str | None = None,
) -> AsyncGenerator[MessageChain | None, None]:
"""
运行第三方 agent runner 并转换响应格式
@@ -55,10 +61,12 @@ async def run_third_party_agent(
yield resp.data["chain"]
except Exception as e:
logger.error(f"Third party agent runner error: {e}")
err_msg = (
f"\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n"
f"错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
)
err_msg = custom_error_message
if not err_msg:
err_msg = (
f"\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n"
f"错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
)
yield MessageChain().message(err_msg)
@@ -77,6 +85,24 @@ class ThirdPartyAgentSubStage(Stage):
"unsupported_streaming_strategy"
]
async def _resolve_persona_custom_error_message(
self, event: AstrMessageEvent
) -> str | None:
try:
conversation_persona_id = await resolve_event_conversation_persona_id(
event,
self.ctx.plugin_manager.context.conversation_manager,
)
return await resolve_persona_custom_error_message(
event=event,
persona_manager=self.ctx.plugin_manager.context.persona_manager,
provider_settings=self.conf["provider_settings"],
conversation_persona_id=conversation_persona_id,
)
except Exception as e:
logger.debug("Failed to resolve persona custom error message: %s", e)
return None
async def process(
self, event: AstrMessageEvent, provider_wake_prefix: str
) -> AsyncGenerator[None, None]:
@@ -112,6 +138,9 @@ class ThirdPartyAgentSubStage(Stage):
if not req.prompt and not req.image_urls:
return
custom_error_message = await self._resolve_persona_custom_error_message(event)
set_persona_custom_error_message_on_event(event, custom_error_message)
# call event hook
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
return
@@ -161,6 +190,7 @@ class ThirdPartyAgentSubStage(Stage):
run_third_party_agent(
runner,
stream_to_general=False,
custom_error_message=custom_error_message,
),
),
)
@@ -179,6 +209,7 @@ class ThirdPartyAgentSubStage(Stage):
async for _ in run_third_party_agent(
runner,
stream_to_general=stream_to_general,
custom_error_message=custom_error_message,
):
yield
+1
View File
@@ -0,0 +1 @@
NOT_GIVEN = object()
+35 -7
View File
@@ -58,6 +58,7 @@ class PersonaRoute(Route):
"begin_dialogs": persona.begin_dialogs or [],
"tools": persona.tools,
"skills": persona.skills,
"custom_error_message": persona.custom_error_message,
"folder_id": persona.folder_id,
"sort_order": persona.sort_order,
"created_at": persona.created_at.isoformat()
@@ -98,6 +99,7 @@ class PersonaRoute(Route):
"begin_dialogs": persona.begin_dialogs or [],
"tools": persona.tools,
"skills": persona.skills,
"custom_error_message": persona.custom_error_message,
"folder_id": persona.folder_id,
"sort_order": persona.sort_order,
"created_at": persona.created_at.isoformat()
@@ -123,6 +125,7 @@ class PersonaRoute(Route):
begin_dialogs = data.get("begin_dialogs", [])
tools = data.get("tools")
skills = data.get("skills")
custom_error_message = data.get("custom_error_message")
folder_id = data.get("folder_id") # None 表示根目录
sort_order = data.get("sort_order", 0)
@@ -132,6 +135,11 @@ class PersonaRoute(Route):
if not system_prompt:
return Response().error("系统提示词不能为空").__dict__
if custom_error_message is not None:
if not isinstance(custom_error_message, str):
return Response().error("自定义报错回复信息必须是字符串").__dict__
custom_error_message = custom_error_message.strip() or None
# 验证 begin_dialogs 格式
if begin_dialogs and len(begin_dialogs) % 2 != 0:
return (
@@ -146,6 +154,7 @@ class PersonaRoute(Route):
begin_dialogs=begin_dialogs if begin_dialogs else None,
tools=tools if tools else None,
skills=skills if skills else None,
custom_error_message=custom_error_message,
folder_id=folder_id,
sort_order=sort_order,
)
@@ -161,6 +170,7 @@ class PersonaRoute(Route):
"begin_dialogs": persona.begin_dialogs or [],
"tools": persona.tools or [],
"skills": persona.skills or [],
"custom_error_message": persona.custom_error_message,
"folder_id": persona.folder_id,
"sort_order": persona.sort_order,
"created_at": persona.created_at.isoformat()
@@ -187,12 +197,24 @@ class PersonaRoute(Route):
persona_id = data.get("persona_id")
system_prompt = data.get("system_prompt")
begin_dialogs = data.get("begin_dialogs")
has_tools = "tools" in data
tools = data.get("tools")
has_skills = "skills" in data
skills = data.get("skills")
has_custom_error_message = "custom_error_message" in data
custom_error_message = data.get("custom_error_message")
if not persona_id:
return Response().error("缺少必要参数: persona_id").__dict__
if has_custom_error_message:
if custom_error_message is not None and not isinstance(
custom_error_message, str
):
return Response().error("自定义报错回复信息必须是字符串").__dict__
if isinstance(custom_error_message, str):
custom_error_message = custom_error_message.strip() or None
# 验证 begin_dialogs 格式
if begin_dialogs is not None and len(begin_dialogs) % 2 != 0:
return (
@@ -201,13 +223,19 @@ class PersonaRoute(Route):
.__dict__
)
await self.persona_mgr.update_persona(
persona_id=persona_id,
system_prompt=system_prompt,
begin_dialogs=begin_dialogs,
tools=tools,
skills=skills,
)
update_kwargs = {
"persona_id": persona_id,
"system_prompt": system_prompt,
"begin_dialogs": begin_dialogs,
}
if has_tools:
update_kwargs["tools"] = tools
if has_skills:
update_kwargs["skills"] = skills
if has_custom_error_message:
update_kwargs["custom_error_message"] = custom_error_message
await self.persona_mgr.update_persona(**update_kwargs)
return Response().ok({"message": "人格更新成功"}).__dict__
except ValueError as e:
@@ -21,6 +21,17 @@
<v-textarea v-model="personaForm.system_prompt" :label="tm('form.systemPrompt')"
:rules="systemPromptRules" variant="outlined" rows="16" class="mb-4" />
<v-textarea
v-model="personaForm.custom_error_message"
:label="tm('form.customErrorMessage')"
:hint="tm('form.customErrorMessageHelp')"
variant="outlined"
rows="4"
persistent-hint
clearable
class="mb-4"
/>
</v-col>
<v-col cols="12" md="6" class="persona-panels-col">
@@ -360,6 +371,7 @@ export default {
personaForm: {
persona_id: '',
system_prompt: '',
custom_error_message: '',
begin_dialogs: [],
tools: [],
skills: [],
@@ -480,6 +492,7 @@ export default {
this.personaForm = {
persona_id: '',
system_prompt: '',
custom_error_message: '',
begin_dialogs: [],
tools: [],
skills: [],
@@ -494,6 +507,7 @@ export default {
this.personaForm = {
persona_id: persona.persona_id,
system_prompt: persona.system_prompt,
custom_error_message: persona.custom_error_message || '',
begin_dialogs: [...(persona.begin_dialogs || [])],
tools: persona.tools === null ? null : [...(persona.tools || [])],
skills: persona.skills === null ? null : [...(persona.skills || [])],
@@ -40,6 +40,7 @@ import type { FolderTreeNode, SelectableItem } from '@/components/folder/types'
interface Persona {
persona_id: string
system_prompt: string
custom_error_message?: string | null
folder_id?: string | null
[key: string]: any
}
@@ -20,6 +20,8 @@
"form": {
"personaId": "Persona ID",
"systemPrompt": "System Prompt",
"customErrorMessage": "Custom Error Reply Message (Optional)",
"customErrorMessageHelp": "When this persona's LLM request fails (for example, connection failures), this error reply is sent first. Leave empty to use the default error message.",
"presetDialogs": "Preset Dialogs",
"presetDialogsHelp": "Add some preset dialogs to help the bot better understand the role settings. The number of dialogs must be even (users and assistants take turns).",
"userMessage": "User Message",
@@ -20,6 +20,8 @@
"form": {
"personaId": "人格 ID",
"systemPrompt": "系统提示词",
"customErrorMessage": "自定义报错回复信息(可选)",
"customErrorMessageHelp": "当该人格的 LLM 请求失败(例如连接失败)时,优先发送这条报错回复;留空则发送默认报错信息。",
"presetDialogs": "预设对话",
"presetDialogsHelp": "添加一些预设的对话来帮助机器人更好地理解角色设定。",
"userMessage": "用户消息",
+1
View File
@@ -18,6 +18,7 @@ export interface PersonaFolder {
export interface Persona {
persona_id: string;
system_prompt: string;
custom_error_message: string | null;
begin_dialogs: string[];
tools: string[] | null;
skills: string[] | null;
@@ -79,6 +79,7 @@ import { useModuleI18n } from '@/i18n/composables';
interface Persona {
persona_id: string;
system_prompt: string;
custom_error_message?: string | null;
begin_dialogs?: string[] | null;
tools?: string[] | null;
skills?: string[] | null;
@@ -137,6 +137,11 @@
<pre class="system-prompt-content">{{ viewingPersona.system_prompt }}</pre>
</div>
<div v-if="viewingPersona.custom_error_message" class="mb-4">
<h4 class="text-h6 mb-2">{{ tm('form.customErrorMessage') }}</h4>
<pre class="system-prompt-content">{{ viewingPersona.custom_error_message }}</pre>
</div>
<div v-if="viewingPersona.begin_dialogs && viewingPersona.begin_dialogs.length > 0" class="mb-4">
<h4 class="text-h6 mb-2">{{ tm('form.presetDialogs') }}</h4>
<div v-for="(dialog, index) in viewingPersona.begin_dialogs" :key="index" class="mb-2">
@@ -281,6 +286,7 @@ import type { Folder, FolderTreeNode } from '@/components/folder/types';
interface Persona {
persona_id: string;
system_prompt: string;
custom_error_message?: string | null;
begin_dialogs?: string[] | null;
tools?: string[] | null;
skills?: string[] | null;