mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' of github.com:MaiM-with-u/MaiBot into dev
commit
7d2b402ba2
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 # 不再重试请求该模型
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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动作时未提供文本内容")
|
||||
|
|
|
|||
Loading…
Reference in New Issue