Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
26 changes: 26 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
"show_tool_use_status": False,
"streaming_segmented": False,
"max_agent_step": 30,
"error_handling": {
"retry_on_failure": 0,
"report_error_message": True,
"fallback_response": "",
},
},
"provider_stt_settings": {
"enable": False,
Expand Down Expand Up @@ -2086,6 +2091,27 @@
},
},
},
"error_handling": {
"description": "错误处理",
"type": "object",
"items": {
"provider_settings.error_handling.retry_on_failure": {
"description": "LLM 请求失败时重试次数",
"type": "int",
"hint": "当请求失败时自动重试的次数。0 表示不重试。",
},
"provider_settings.error_handling.report_error_message": {
"description": "向用户报告详细错误",
"type": "bool",
"hint": "是否将详细的技术性错误信息作为消息发送给用户。如果关闭,将使用下方的备用回复。",
},
"provider_settings.error_handling.fallback_response": {
"description": "备用回复",
"type": "string",
"hint": "当“向用户报告详细错误”被关闭且所有重试都失败时,发送给用户的固定回复。如果留空,则不发送任何消息(静默失败)。",
},
},
},
},
},
"platform_group": {
Expand Down
141 changes: 94 additions & 47 deletions astrbot/core/pipeline/process_stage/method/llm_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,62 +221,87 @@ async def on_agent_done(self, run_context, llm_response):


async def run_agent(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): 我们发现了这些问题:

  • 将条件简化为类似 switch 的形式 (switch)
  • run_agent 中发现代码质量低 - 9% (low-code-quality)


解释
此函数的质量得分低于 25% 的质量阈值。
此得分是方法长度、认知复杂度和工作内存的组合。

你如何解决这个问题?

重构此函数以使其更短、更具可读性可能值得。

  • 通过将部分功能提取到自己的函数中来减少函数长度。这是你能做的最重要的事情——理想情况下,一个函数应该少于 10 行。
  • 减少嵌套,也许通过引入守卫子句来提前返回。
  • 确保变量的作用域紧密,以便使用相关概念的代码在函数中坐在一起而不是分散。
Original comment in English

issue (code-quality): We've found these issues:

  • Simplify conditional into switch-like form (switch)
  • Low code quality found in run_agent - 9% (low-code-quality)


Explanation
The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

agent_runner: AgentRunner, max_step: int = 30, show_tool_use: bool = True
agent_runner: AgentRunner,
max_step: int = 30,
show_tool_use: bool = True,
retry_on_failure: int = 0,
report_error_message: bool = True,
fallback_response: str = "",
) -> AsyncGenerator[MessageChain, None]:
step_idx = 0
astr_event = agent_runner.run_context.event
while step_idx < max_step:
step_idx += 1
success = False
# Retry loop
for attempt in range(retry_on_failure + 1):
step_idx = 0
try:
async for resp in agent_runner.step():
if astr_event.is_stopped():
return
if resp.type == "tool_call_result":
msg_chain = resp.data["chain"]
if msg_chain.type == "tool_direct_result":
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
resp.data["chain"].type = "tool_call_result"
await astr_event.send(resp.data["chain"])
while step_idx < max_step:
step_idx += 1
async for resp in agent_runner.step():
if astr_event.is_stopped():
return
if resp.type == "tool_call_result":
msg_chain = resp.data["chain"]
if msg_chain.type == "tool_direct_result":
# tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
resp.data["chain"].type = "tool_call_result"
await astr_event.send(resp.data["chain"])
continue
# 对于其他情况,暂时先不处理
continue
elif resp.type == "tool_call":
if agent_runner.streaming:
# 用来标记流式响应需要分节
yield MessageChain(chain=[], type="break")
if show_tool_use or astr_event.get_platform_name() == "webchat":
resp.data["chain"].type = "tool_call"
await astr_event.send(resp.data["chain"])
continue
# 对于其他情况,暂时先不处理
continue
elif resp.type == "tool_call":
if agent_runner.streaming:
# 用来标记流式响应需要分节
yield MessageChain(chain=[], type="break")
if show_tool_use or astr_event.get_platform_name() == "webchat":
resp.data["chain"].type = "tool_call"
await astr_event.send(resp.data["chain"])
continue

if not agent_runner.streaming:
content_typ = (
ResultContentType.LLM_RESULT
if resp.type == "llm_result"
else ResultContentType.GENERAL_RESULT
)
astr_event.set_result(
MessageEventResult(
chain=resp.data["chain"].chain,
result_content_type=content_typ,
if not agent_runner.streaming:
content_typ = (
ResultContentType.LLM_RESULT
if resp.type == "llm_result"
else ResultContentType.GENERAL_RESULT
)
)
yield
astr_event.clear_result()
else:
if resp.type == "streaming_delta":
yield resp.data["chain"] # MessageChain
if agent_runner.done():
astr_event.set_result(
MessageEventResult(
chain=resp.data["chain"].chain,
result_content_type=content_typ,
)
)
yield
astr_event.clear_result()
else:
if resp.type == "streaming_delta":
yield resp.data["chain"] # MessageChain
if agent_runner.done():
success = True
break
if success:
break

except Exception as e:
logger.error(traceback.format_exc())
astr_event.set_result(
MessageEventResult().message(
f"AstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {str(e)}\n\n请在控制台查看和分享错误详情。\n"
)
logger.error(
f"Attempt {attempt + 1}/{retry_on_failure + 1} failed: {traceback.format_exc()}"
)
return
if attempt >= retry_on_failure:
# All retries exhausted, handle final error
if report_error_message:
astr_event.set_result(
MessageEventResult().message(
f"AstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {str(e)}\n\n请在控制台查看和分享错误详情。\n"
)
)
elif fallback_response:
astr_event.set_result(
MessageEventResult().message(fallback_response)
)
# If report_error_message is False and fallback_response is empty, do nothing (silent fail).
return
# If retries are left, continue to the next attempt
continue
if success:
asyncio.create_task(
Metric.upload(
llm_tick=1,
Expand Down Expand Up @@ -304,6 +329,14 @@ async def initialize(self, ctx: PipelineContext) -> None:
self.max_step = 30
self.show_tool_use: bool = settings.get("show_tool_use_status", True)

# Load error handling settings
error_handling_settings = settings.get("error_handling", {})
self.retry_on_failure = error_handling_settings.get("retry_on_failure", 0)
self.report_error_message = error_handling_settings.get(
"report_error_message", True
)
self.fallback_response = error_handling_settings.get("fallback_response", "")
Comment on lines 339 to 363
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): 考虑验证 error_handling 设置的类型和值。

直接从配置中加载 error_handling 设置而不进行验证,可能会导致 retry_on_failure 等值无效时出现运行时错误。请为这些设置添加类型和值检查。

Suggested change
# Load error handling settings
error_handling_settings = settings.get("error_handling", {})
self.retry_on_failure = error_handling_settings.get("retry_on_failure", 0)
self.report_error_message = error_handling_settings.get(
"report_error_message", True
)
self.fallback_response = error_handling_settings.get("fallback_response", "")
# Load error handling settings with validation
error_handling_settings = settings.get("error_handling", {})
# Validate retry_on_failure: must be a non-negative integer
retry_on_failure = error_handling_settings.get("retry_on_failure", 0)
if not isinstance(retry_on_failure, int) or retry_on_failure < 0:
logger.warning(
f"Invalid value for retry_on_failure: {retry_on_failure}. Using default 0."
)
retry_on_failure = 0
self.retry_on_failure = retry_on_failure
# Validate report_error_message: must be a boolean
report_error_message = error_handling_settings.get("report_error_message", True)
if not isinstance(report_error_message, bool):
logger.warning(
f"Invalid value for report_error_message: {report_error_message}. Using default True."
)
report_error_message = True
self.report_error_message = report_error_message
# Validate fallback_response: must be a string
fallback_response = error_handling_settings.get("fallback_response", "")
if not isinstance(fallback_response, str):
logger.warning(
f"Invalid value for fallback_response: {fallback_response}. Using default ''."
)
fallback_response = ""
self.fallback_response = fallback_response
Original comment in English

suggestion (bug_risk): Consider validating error_handling settings for type and value.

Directly loading error_handling settings from the config without validation may lead to runtime errors if values like retry_on_failure are invalid. Please add type and value checks for these settings.

Suggested change
# Load error handling settings
error_handling_settings = settings.get("error_handling", {})
self.retry_on_failure = error_handling_settings.get("retry_on_failure", 0)
self.report_error_message = error_handling_settings.get(
"report_error_message", True
)
self.fallback_response = error_handling_settings.get("fallback_response", "")
# Load error handling settings with validation
error_handling_settings = settings.get("error_handling", {})
# Validate retry_on_failure: must be a non-negative integer
retry_on_failure = error_handling_settings.get("retry_on_failure", 0)
if not isinstance(retry_on_failure, int) or retry_on_failure < 0:
logger.warning(
f"Invalid value for retry_on_failure: {retry_on_failure}. Using default 0."
)
retry_on_failure = 0
self.retry_on_failure = retry_on_failure
# Validate report_error_message: must be a boolean
report_error_message = error_handling_settings.get("report_error_message", True)
if not isinstance(report_error_message, bool):
logger.warning(
f"Invalid value for report_error_message: {report_error_message}. Using default True."
)
report_error_message = True
self.report_error_message = report_error_message
# Validate fallback_response: must be a string
fallback_response = error_handling_settings.get("fallback_response", "")
if not isinstance(fallback_response, str):
logger.warning(
f"Invalid value for fallback_response: {fallback_response}. Using default ''."
)
fallback_response = ""
self.fallback_response = fallback_response


for bwp in self.bot_wake_prefixs:
if self.provider_wake_prefix.startswith(bwp):
logger.info(
Expand Down Expand Up @@ -477,7 +510,14 @@ async def process(
MessageEventResult()
.set_result_content_type(ResultContentType.STREAMING_RESULT)
.set_async_stream(
run_agent(agent_runner, self.max_step, self.show_tool_use)
run_agent(
agent_runner,
self.max_step,
self.show_tool_use,
self.retry_on_failure,
self.report_error_message,
self.fallback_response,
)
)
)
yield
Expand All @@ -496,7 +536,14 @@ async def process(
)
)
else:
async for _ in run_agent(agent_runner, self.max_step, self.show_tool_use):
async for _ in run_agent(
agent_runner,
self.max_step,
self.show_tool_use,
self.retry_on_failure,
self.report_error_message,
self.fallback_response,
):
yield

await self._save_to_history(event, req, agent_runner.get_final_llm_resp())
Expand Down
Loading