diff --git a/src/llm_models/exceptions.py b/src/llm_models/exceptions.py index 5b04f58c..ff847ad8 100644 --- a/src/llm_models/exceptions.py +++ b/src/llm_models/exceptions.py @@ -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 diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index d253d29c..67f9a300 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -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 diff --git a/src/llm_models/model_client/openai_client.py b/src/llm_models/model_client/openai_client.py index bba00f94..51bb692f 100644 --- a/src/llm_models/model_client/openai_client.py +++ b/src/llm_models/model_client/openai_client.py @@ -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 diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 1125e9fd..7ab76969 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -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 # 不再重试请求该模型 diff --git a/src/mais4u/mais4u_chat/body_emotion_action_manager.py b/src/mais4u/mais4u_chat/body_emotion_action_manager.py index 83e6818f..18780686 100644 --- a/src/mais4u/mais4u_chat/body_emotion_action_manager.py +++ b/src/mais4u/mais4u_chat/body_emotion_action_manager.py @@ -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( diff --git a/src/plugins/built_in/tts_plugin/plugin.py b/src/plugins/built_in/tts_plugin/plugin.py index d83fc762..8d772b3e 100644 --- a/src/plugins/built_in/tts_plugin/plugin.py +++ b/src/plugins/built_in/tts_plugin/plugin.py @@ -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动作时未提供文本内容")