Merge branch 'dev' of github.com:MaiM-with-u/MaiBot into dev

pull/1222/head
UnCLAS-Prommer 2025-08-27 21:51:57 +08:00
commit 7d2b402ba2
No known key found for this signature in database
6 changed files with 71 additions and 41 deletions

View File

@ -96,3 +96,14 @@ class PermissionDeniedException(Exception):
def __str__(self):
return self.message
class EmptyResponseException(Exception):
"""响应内容为空"""
def __init__(self, message: str = "响应内容为空,这可能是一个临时性问题"):
super().__init__(message)
self.message = message
def __str__(self):
return self.message

View File

@ -37,6 +37,7 @@ from ..exceptions import (
NetworkConnectionError,
RespNotOkException,
ReqAbortException,
EmptyResponseException,
)
from ..payload_content.message import Message, RoleType
from ..payload_content.resp_format import RespFormat, RespFormatType
@ -224,6 +225,9 @@ def _build_stream_api_resp(
resp.tool_calls.append(ToolCall(call_id, function_name, arguments))
if not resp.content and not resp.tool_calls:
raise EmptyResponseException()
return resp
@ -284,26 +288,27 @@ def _default_normal_response_parser(
"""
api_response = APIResponse()
if not hasattr(resp, "candidates") or not resp.candidates:
raise RespParseException(resp, "响应解析失败缺失candidates字段")
# 解析思考内容
try:
if resp.candidates[0].content and resp.candidates[0].content.parts:
for part in resp.candidates[0].content.parts:
if not part.text:
continue
if part.thought:
api_response.reasoning_content = (
api_response.reasoning_content + part.text if api_response.reasoning_content else part.text
)
if candidates := resp.candidates:
if candidates[0].content and candidates[0].content.parts:
for part in candidates[0].content.parts:
if not part.text:
continue
if part.thought:
api_response.reasoning_content = (
api_response.reasoning_content + part.text if api_response.reasoning_content else part.text
)
except Exception as e:
logger.warning(f"解析思考内容时发生错误: {e},跳过解析")
if resp.text:
api_response.content = resp.text
# 解析响应内容
api_response.content = resp.text
if resp.function_calls:
# 解析工具调用
if function_calls := resp.function_calls:
api_response.tool_calls = []
for call in resp.function_calls:
for call in function_calls:
try:
if not isinstance(call.args, dict):
raise RespParseException(resp, "响应解析失败,工具调用参数无法解析为字典类型")
@ -313,17 +318,22 @@ def _default_normal_response_parser(
except Exception as e:
raise RespParseException(resp, "响应解析失败,无法解析工具调用参数") from e
if resp.usage_metadata:
# 解析使用情况
if usage_metadata := resp.usage_metadata:
_usage_record = (
resp.usage_metadata.prompt_token_count or 0,
(resp.usage_metadata.candidates_token_count or 0) + (resp.usage_metadata.thoughts_token_count or 0),
resp.usage_metadata.total_token_count or 0,
usage_metadata.prompt_token_count or 0,
(usage_metadata.candidates_token_count or 0) + (usage_metadata.thoughts_token_count or 0),
usage_metadata.total_token_count or 0,
)
else:
_usage_record = None
api_response.raw_data = resp
# 最终的、唯一的空响应检查
if not api_response.content and not api_response.tool_calls:
raise EmptyResponseException("响应中既无文本内容也无工具调用")
return api_response, _usage_record

View File

@ -30,6 +30,7 @@ from ..exceptions import (
NetworkConnectionError,
RespNotOkException,
ReqAbortException,
EmptyResponseException,
)
from ..payload_content.message import Message, RoleType
from ..payload_content.resp_format import RespFormat
@ -235,6 +236,9 @@ def _build_stream_api_resp(
resp.tool_calls.append(ToolCall(call_id, function_name, arguments))
if not resp.content and not resp.tool_calls:
raise EmptyResponseException()
return resp
@ -332,7 +336,7 @@ def _default_normal_response_parser(
api_response = APIResponse()
if not hasattr(resp, "choices") or len(resp.choices) == 0:
raise RespParseException(resp, "响应解析失败缺失choices字段")
raise EmptyResponseException("响应解析失败缺失choices字段或choices列表为空")
message_part = resp.choices[0].message
if hasattr(message_part, "reasoning_content") and message_part.reasoning_content: # type: ignore
@ -377,6 +381,9 @@ def _default_normal_response_parser(
# 将原始响应存储在原始数据中
api_response.raw_data = resp
if not api_response.content and not api_response.tool_calls:
raise EmptyResponseException()
return api_response, _usage_record

View File

@ -14,7 +14,13 @@ from .payload_content.resp_format import RespFormat
from .payload_content.tool_option import ToolOption, ToolCall, ToolOptionBuilder, ToolParamType
from .model_client.base_client import BaseClient, APIResponse, client_registry
from .utils import compress_messages, llm_usage_recorder
from .exceptions import NetworkConnectionError, ReqAbortException, RespNotOkException, RespParseException
from .exceptions import (
NetworkConnectionError,
ReqAbortException,
RespNotOkException,
RespParseException,
EmptyResponseException,
)
install(extra_lines=3)
@ -192,12 +198,6 @@ class LLMRequest:
endpoint="/chat/completions",
time_cost=time.time() - start_time,
)
if not content:
if raise_when_empty:
logger.warning(f"生成的响应为空, 请求类型: {self.request_type}")
raise RuntimeError("生成的响应为空")
content = "生成的响应为空,请检查模型配置或输入内容是否正确"
return content, (reasoning_content, model_info.name, tool_calls)
@ -367,6 +367,13 @@ class LLMRequest:
can_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 连接异常,将于{retry_interval}秒后重试",
cannot_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 连接异常超过最大重试次数请检查网络连接状态或URL是否正确",
)
elif isinstance(e, EmptyResponseException): # 空响应错误
return self._check_retry(
remain_try,
retry_interval,
can_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 收到空响应,将于{retry_interval}秒后重试。原因: {e}",
cannot_retry_msg=f"任务-'{task_name}' 模型-'{model_name}': 收到空响应,超过最大重试次数,放弃请求",
)
elif isinstance(e, ReqAbortException):
logger.warning(f"任务-'{task_name}' 模型-'{model_name}': 请求被中断,详细信息-{str(e.message)}")
return -1, None # 不再重试请求该模型

View File

@ -43,9 +43,9 @@ DEFAULT_BODY_CODE = {
}
def get_head_code() -> dict:
async def get_head_code() -> dict:
"""获取头部动作代码字典"""
head_code_str = global_prompt_manager.get_prompt("head_code_prompt")
head_code_str = await global_prompt_manager.format_prompt("head_code_prompt")
if not head_code_str:
return DEFAULT_HEAD_CODE
try:
@ -55,9 +55,9 @@ def get_head_code() -> dict:
return DEFAULT_HEAD_CODE
def get_body_code() -> dict:
async def get_body_code() -> dict:
"""获取身体动作代码字典"""
body_code_str = global_prompt_manager.get_prompt("body_code_prompt")
body_code_str = await global_prompt_manager.format_prompt("body_code_prompt")
if not body_code_str:
return DEFAULT_BODY_CODE
try:
@ -143,7 +143,7 @@ class ChatAction:
async def send_action_update(self):
"""发送动作更新到前端"""
body_code = get_body_code().get(self.body_action, "")
body_code = (await get_body_code()).get(self.body_action, "")
await send_api.custom_to_stream(
message_type="body_action",
content=body_code,
@ -184,7 +184,7 @@ class ChatAction:
try:
# 冷却池处理:过滤掉冷却中的动作
self._update_body_action_cooldown()
available_actions = [k for k in get_body_code().keys() if k not in self.body_action_cooldown]
available_actions = [k for k in (await get_body_code()).keys() if k not in self.body_action_cooldown]
all_actions = "\n".join(available_actions)
prompt = await global_prompt_manager.format_prompt(
@ -246,7 +246,7 @@ class ChatAction:
try:
# 冷却池处理:过滤掉冷却中的动作
self._update_body_action_cooldown()
available_actions = [k for k in get_body_code().keys() if k not in self.body_action_cooldown]
available_actions = [k for k in (await get_body_code()).keys() if k not in self.body_action_cooldown]
all_actions = "\n".join(available_actions)
prompt = await global_prompt_manager.format_prompt(

View File

@ -13,21 +13,16 @@ class TTSAction(BaseAction):
"""TTS语音转换动作处理类"""
# 激活设置
focus_activation_type = ActionActivationType.LLM_JUDGE
normal_activation_type = ActionActivationType.KEYWORD
activation_type = ActionActivationType.LLM_JUDGE
parallel_action = False
# 动作基本信息
action_name = "tts_action"
action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景"
# 关键词配置 - Normal模式下使用关键词触发
activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"]
keyword_case_sensitive = False
# 动作参数定义
action_parameters = {
"text": "需要转换为语音的文本内容,必填,内容应当适合语音播报,语句流畅、清晰",
"voice_text": "你想用语音表达的内容,这段内容将会以语音形式发出",
}
# 动作使用场景
@ -46,7 +41,7 @@ class TTSAction(BaseAction):
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}")
# 获取要转换的文本
text = self.action_data.get("text")
text = self.action_data.get("voice_text")
if not text:
logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容")