Skip to content

Commit 7bf44bd

Browse files
authored
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
1 parent 881b409 commit 7bf44bd

File tree

19 files changed

+269
-24
lines changed

19 files changed

+269
-24
lines changed

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from astrbot.core.message.message_event_result import (
2424
MessageChain,
2525
)
26+
from astrbot.core.persona_error_reply import (
27+
extract_persona_custom_error_message_from_event,
28+
)
2629
from astrbot.core.provider.entities import (
2730
LLMResponse,
2831
ProviderRequest,
@@ -78,6 +81,11 @@ class FollowUpTicket:
7881

7982

8083
class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
84+
def _get_persona_custom_error_message(self) -> str | None:
85+
"""Read persona-level custom error message from event extras when available."""
86+
event = getattr(self.run_context.context, "event", None)
87+
return extract_persona_custom_error_message_from_event(event)
88+
8189
@override
8290
async def reset(
8391
self,
@@ -463,12 +471,14 @@ async def step(self):
463471
self.stats.end_time = time.time()
464472
self._transition_state(AgentState.ERROR)
465473
self._resolve_unconsumed_follow_ups()
474+
custom_error_message = self._get_persona_custom_error_message()
475+
error_text = custom_error_message or (
476+
f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}"
477+
)
466478
yield AgentResponse(
467479
type="err",
468480
data=AgentResponseData(
469-
chain=MessageChain().message(
470-
f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}",
471-
),
481+
chain=MessageChain().message(error_text),
472482
),
473483
)
474484
return

astrbot/core/astr_agent_run_util.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
MessageEventResult,
1515
ResultContentType,
1616
)
17+
from astrbot.core.persona_error_reply import (
18+
extract_persona_custom_error_message_from_event,
19+
)
1720
from astrbot.core.provider.entities import LLMResponse
1821
from astrbot.core.provider.provider import TTSProvider
1922

@@ -235,7 +238,16 @@ async def run_agent(
235238
pass
236239
logger.error(traceback.format_exc())
237240

238-
err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
241+
custom_error_message = extract_persona_custom_error_message_from_event(
242+
astr_event
243+
)
244+
if custom_error_message:
245+
err_msg = custom_error_message
246+
else:
247+
err_msg = (
248+
f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: "
249+
f"{e!s}\n\n请在平台日志查看和分享错误详情。\n"
250+
)
239251

240252
error_llm_response = LLMResponse(
241253
role="err",

astrbot/core/astr_main_agent.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
)
3838
from astrbot.core.conversation_mgr import Conversation
3939
from astrbot.core.message.components import File, Image, Reply
40+
from astrbot.core.persona_error_reply import (
41+
extract_persona_custom_error_message_from_persona,
42+
set_persona_custom_error_message_on_event,
43+
)
4044
from astrbot.core.platform.astr_message_event import AstrMessageEvent
4145
from astrbot.core.provider import Provider
4246
from astrbot.core.provider.entities import ProviderRequest
@@ -285,6 +289,10 @@ async def _ensure_persona_and_skills(
285289
provider_settings=cfg,
286290
)
287291

292+
set_persona_custom_error_message_on_event(
293+
event, extract_persona_custom_error_message_from_persona(persona)
294+
)
295+
288296
if persona:
289297
# Inject persona system prompt
290298
if prompt := persona["prompt"]:

astrbot/core/db/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ async def insert_persona(
306306
begin_dialogs: list[str] | None = None,
307307
tools: list[str] | None = None,
308308
skills: list[str] | None = None,
309+
custom_error_message: str | None = None,
309310
folder_id: str | None = None,
310311
sort_order: int = 0,
311312
) -> Persona:
@@ -317,6 +318,7 @@ async def insert_persona(
317318
begin_dialogs: Optional list of initial dialog strings
318319
tools: Optional list of tool names (None means all tools, [] means no tools)
319320
skills: Optional list of skill names (None means all skills, [] means no skills)
321+
custom_error_message: Optional persona-level fallback error message
320322
folder_id: Optional folder ID to place the persona in (None means root)
321323
sort_order: Sort order within the folder (default 0)
322324
"""
@@ -340,6 +342,7 @@ async def update_persona(
340342
begin_dialogs: list[str] | None = None,
341343
tools: list[str] | None = None,
342344
skills: list[str] | None = None,
345+
custom_error_message: str | None = None,
343346
) -> Persona | None:
344347
"""Update a persona's system prompt or begin dialogs."""
345348
...

astrbot/core/db/po.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class Persona(TimestampMixin, SQLModel, table=True):
126126
"""None means use ALL tools for default, empty list means no tools, otherwise a list of tool names."""
127127
skills: list | None = Field(default=None, sa_type=JSON)
128128
"""None means use ALL skills for default, empty list means no skills, otherwise a list of skill names."""
129+
custom_error_message: str | None = Field(default=None, sa_type=Text)
130+
"""Optional custom error message sent to end users when the agent request fails."""
129131
folder_id: str | None = Field(default=None, max_length=36)
130132
"""所属文件夹ID,NULL 表示在根目录"""
131133
sort_order: int = Field(default=0)
@@ -472,6 +474,8 @@ class Personality(TypedDict):
472474
"""工具列表。None 表示使用所有工具,空列表表示不使用任何工具"""
473475
skills: list[str] | None
474476
"""Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills"""
477+
custom_error_message: str | None
478+
"""可选的人格自定义报错回复信息。配置后将优先发送给最终用户。"""
475479

476480
# cache
477481
_begin_dialogs_processed: list[dict]

astrbot/core/db/sqlite.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
from astrbot.core.db.po import (
3333
Stats as DeprecatedStats,
3434
)
35+
from astrbot.core.sentinels import NOT_GIVEN
3536

36-
NOT_GIVEN = T.TypeVar("NOT_GIVEN")
3737
TxResult = T.TypeVar("TxResult")
3838
CRON_FIELD_NOT_SET = object()
3939

@@ -58,6 +58,7 @@ async def initialize(self) -> None:
5858
# 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容)
5959
await self._ensure_persona_folder_columns(conn)
6060
await self._ensure_persona_skills_column(conn)
61+
await self._ensure_persona_custom_error_message_column(conn)
6162
await conn.commit()
6263

6364
async def _ensure_persona_folder_columns(self, conn) -> None:
@@ -92,6 +93,16 @@ async def _ensure_persona_skills_column(self, conn) -> None:
9293
if "skills" not in columns:
9394
await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON"))
9495

96+
async def _ensure_persona_custom_error_message_column(self, conn) -> None:
97+
"""确保 personas 表有 custom_error_message 列。"""
98+
result = await conn.execute(text("PRAGMA table_info(personas)"))
99+
columns = {row[1] for row in result.fetchall()}
100+
101+
if "custom_error_message" not in columns:
102+
await conn.execute(
103+
text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT")
104+
)
105+
95106
# ====
96107
# Platform Statistics
97108
# ====
@@ -675,6 +686,7 @@ async def insert_persona(
675686
begin_dialogs=None,
676687
tools=None,
677688
skills=None,
689+
custom_error_message=None,
678690
folder_id=None,
679691
sort_order=0,
680692
):
@@ -688,6 +700,7 @@ async def insert_persona(
688700
begin_dialogs=begin_dialogs or [],
689701
tools=tools,
690702
skills=skills,
703+
custom_error_message=custom_error_message,
691704
folder_id=folder_id,
692705
sort_order=sort_order,
693706
)
@@ -719,6 +732,7 @@ async def update_persona(
719732
begin_dialogs=None,
720733
tools=NOT_GIVEN,
721734
skills=NOT_GIVEN,
735+
custom_error_message=NOT_GIVEN,
722736
):
723737
"""Update a persona's system prompt or begin dialogs."""
724738
async with self.get_db() as session:
@@ -734,6 +748,8 @@ async def update_persona(
734748
values["tools"] = tools
735749
if skills is not NOT_GIVEN:
736750
values["skills"] = skills
751+
if custom_error_message is not NOT_GIVEN:
752+
values["custom_error_message"] = custom_error_message
737753
if not values:
738754
return None
739755
query = query.values(**values)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping
4+
from typing import Any
5+
6+
PERSONA_CUSTOM_ERROR_MESSAGE_EXTRA_KEY = "persona_custom_error_message"
7+
8+
9+
def normalize_persona_custom_error_message(value: object) -> str | None:
10+
"""Normalize persona custom error reply text."""
11+
if not isinstance(value, str):
12+
return None
13+
message = value.strip()
14+
return message or None
15+
16+
17+
def extract_persona_custom_error_message_from_persona(
18+
persona: Mapping[str, Any] | None,
19+
) -> str | None:
20+
"""Extract normalized custom error reply text from persona mapping."""
21+
if persona is None:
22+
return None
23+
return normalize_persona_custom_error_message(persona.get("custom_error_message"))
24+
25+
26+
def extract_persona_custom_error_message_from_event(event: Any) -> str | None:
27+
"""Extract normalized custom error reply text from event extras."""
28+
try:
29+
if event is None or not hasattr(event, "get_extra"):
30+
return None
31+
raw_message = event.get_extra(PERSONA_CUSTOM_ERROR_MESSAGE_EXTRA_KEY)
32+
return normalize_persona_custom_error_message(raw_message)
33+
except Exception:
34+
return None
35+
36+
37+
def set_persona_custom_error_message_on_event(
38+
event: Any, message: object
39+
) -> str | None:
40+
"""Normalize and store persona custom error reply text into event extras."""
41+
normalized = normalize_persona_custom_error_message(message)
42+
try:
43+
if event is not None and hasattr(event, "set_extra"):
44+
event.set_extra(PERSONA_CUSTOM_ERROR_MESSAGE_EXTRA_KEY, normalized)
45+
except Exception:
46+
pass
47+
return normalized
48+
49+
50+
async def resolve_persona_custom_error_message(
51+
*,
52+
event: Any,
53+
persona_manager: Any,
54+
provider_settings: dict | None = None,
55+
conversation_persona_id: str | None = None,
56+
) -> str | None:
57+
"""Resolve normalized custom error reply text for the selected persona."""
58+
(
59+
_persona_id,
60+
persona,
61+
_force_applied_persona_id,
62+
_use_webchat_special_default,
63+
) = await persona_manager.resolve_selected_persona(
64+
umo=event.unified_msg_origin,
65+
conversation_persona_id=conversation_persona_id,
66+
platform_name=event.get_platform_name(),
67+
provider_settings=provider_settings,
68+
)
69+
return extract_persona_custom_error_message_from_persona(persona)
70+
71+
72+
async def resolve_event_conversation_persona_id(
73+
event: Any, conversation_manager: Any
74+
) -> str | None:
75+
"""Resolve current conversation persona_id from event and conversation manager."""
76+
curr_cid = await conversation_manager.get_curr_conversation_id(
77+
event.unified_msg_origin
78+
)
79+
if not curr_cid:
80+
return None
81+
conversation = await conversation_manager.get_conversation(
82+
event.unified_msg_origin, curr_cid
83+
)
84+
if not conversation:
85+
return None
86+
return conversation.persona_id

astrbot/core/persona_mgr.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from astrbot.core.db import BaseDatabase
55
from astrbot.core.db.po import Persona, PersonaFolder, Personality
66
from astrbot.core.platform.message_session import MessageSession
7+
from astrbot.core.sentinels import NOT_GIVEN
78

89
DEFAULT_PERSONALITY = Personality(
910
prompt="You are a helpful and friendly assistant.",
@@ -12,6 +13,7 @@
1213
mood_imitation_dialogs=[],
1314
tools=None,
1415
skills=None,
16+
custom_error_message=None,
1517
_begin_dialogs_processed=[],
1618
_mood_imitation_dialogs_processed="",
1719
)
@@ -126,19 +128,27 @@ async def update_persona(
126128
persona_id: str,
127129
system_prompt: str | None = None,
128130
begin_dialogs: list[str] | None = None,
129-
tools: list[str] | None = None,
130-
skills: list[str] | None = None,
131+
tools: list[str] | None | object = NOT_GIVEN,
132+
skills: list[str] | None | object = NOT_GIVEN,
133+
custom_error_message: str | None | object = NOT_GIVEN,
131134
):
132135
"""更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具"""
133136
existing_persona = await self.db.get_persona_by_id(persona_id)
134137
if not existing_persona:
135138
raise ValueError(f"Persona with ID {persona_id} does not exist.")
139+
update_kwargs = {}
140+
if tools is not NOT_GIVEN:
141+
update_kwargs["tools"] = tools
142+
if skills is not NOT_GIVEN:
143+
update_kwargs["skills"] = skills
144+
if custom_error_message is not NOT_GIVEN:
145+
update_kwargs["custom_error_message"] = custom_error_message
146+
136147
persona = await self.db.update_persona(
137148
persona_id,
138149
system_prompt,
139150
begin_dialogs,
140-
tools=tools,
141-
skills=skills,
151+
**update_kwargs,
142152
)
143153
if persona:
144154
for i, p in enumerate(self.personas):
@@ -298,6 +308,7 @@ async def create_persona(
298308
begin_dialogs: list[str] | None = None,
299309
tools: list[str] | None = None,
300310
skills: list[str] | None = None,
311+
custom_error_message: str | None = None,
301312
folder_id: str | None = None,
302313
sort_order: int = 0,
303314
) -> Persona:
@@ -320,6 +331,7 @@ async def create_persona(
320331
begin_dialogs,
321332
tools=tools,
322333
skills=skills,
334+
custom_error_message=custom_error_message,
323335
folder_id=folder_id,
324336
sort_order=sort_order,
325337
)
@@ -346,6 +358,7 @@ def get_v3_persona_data(
346358
"mood_imitation_dialogs": [], # deprecated
347359
"tools": persona.tools,
348360
"skills": persona.skills,
361+
"custom_error_message": persona.custom_error_message,
349362
}
350363
for persona in self.personas
351364
]
@@ -402,6 +415,7 @@ def get_v3_persona_data(
402415
begin_dialogs=selected_default_persona["begin_dialogs"],
403416
tools=selected_default_persona["tools"] or None,
404417
skills=selected_default_persona["skills"] or None,
418+
custom_error_message=selected_default_persona["custom_error_message"],
405419
)
406420

407421
return v3_persona_config, personas_v3, selected_default_persona

astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
MessageEventResult,
2020
ResultContentType,
2121
)
22+
from astrbot.core.persona_error_reply import (
23+
extract_persona_custom_error_message_from_event,
24+
)
2225
from astrbot.core.pipeline.stage import Stage
2326
from astrbot.core.platform.astr_message_event import AstrMessageEvent
2427
from astrbot.core.provider.entities import (
@@ -366,11 +369,13 @@ async def process(
366369

367370
except Exception as e:
368371
logger.error(f"Error occurred while processing agent: {e}")
369-
await event.send(
370-
MessageChain().message(
371-
f"Error occurred while processing agent request: {e}"
372-
)
372+
custom_error_message = extract_persona_custom_error_message_from_event(
373+
event
374+
)
375+
error_text = custom_error_message or (
376+
f"Error occurred while processing agent request: {e}"
373377
)
378+
await event.send(MessageChain().message(error_text))
374379
finally:
375380
if follow_up_capture:
376381
await finalize_follow_up_capture(

0 commit comments

Comments
 (0)