From 5c1f0b0dfa5d3bad2adf841ad25bd37ce0751a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Tue, 19 Aug 2025 00:39:54 +0800 Subject: [PATCH 01/18] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 999bd5fd..0a5e9a3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,3 +47,4 @@ reportportal-client scikit-learn seaborn structlog +google.geai From de67810950fcd357620c6a9832dcfb8a9a2af520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Tue, 19 Aug 2025 00:42:40 +0800 Subject: [PATCH 02/18] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0a5e9a3d..721cf95f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,4 +47,4 @@ reportportal-client scikit-learn seaborn structlog -google.geai +google.genai From 938e17ea154f17860c86dcbb6b94a6ef4be9d708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Tue, 19 Aug 2025 16:12:25 +0800 Subject: [PATCH 03/18] Update model_config_template.toml --- template/model_config_template.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/model_config_template.toml b/template/model_config_template.toml index 92ac8881..0d756314 100644 --- a/template/model_config_template.toml +++ b/template/model_config_template.toml @@ -5,7 +5,7 @@ version = "1.3.0" [[api_providers]] # API服务提供商(可以配置多个) name = "DeepSeek" # API服务商名称(可随意命名,在models的api-provider中需使用这个命名) -base_url = "https://api.deepseek.cn/v1" # API服务商的BaseURL +base_url = "https://api.deepseek.com/v1" # API服务商的BaseURL api_key = "your-api-key-here" # API密钥(请替换为实际的API密钥) client_type = "openai" # 请求客户端(可选,默认值为"openai",使用gimini等Google系模型时请配置为"gemini") max_retry = 2 # 最大重试次数(单个模型API调用失败,最多重试的次数) From a8ff08e2a72d66947ec5af7cfc62e39163d7396a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Tue, 19 Aug 2025 16:59:51 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E9=81=BF=E5=85=8D=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF=E9=97=AE=E9=A2=98=E5=B9=B6=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/import_openie.py | 26 ++++++++- src/chat/knowledge/embedding_store.py | 58 ++++++++++++-------- src/chat/utils/utils.py | 1 + src/llm_models/model_client/base_client.py | 11 +++- src/llm_models/model_client/openai_client.py | 6 ++ src/llm_models/utils_model.py | 6 +- 6 files changed, 82 insertions(+), 26 deletions(-) diff --git a/scripts/import_openie.py b/scripts/import_openie.py index fe9f5269..c4367892 100644 --- a/scripts/import_openie.py +++ b/scripts/import_openie.py @@ -6,6 +6,7 @@ import sys import os +import asyncio from time import sleep sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -172,7 +173,7 @@ def handle_import_openie(openie_data: OpenIE, embed_manager: EmbeddingManager, k return True -def main(): # sourcery skip: dict-comprehension +async def main_async(): # sourcery skip: dict-comprehension # 新增确认提示 print("=== 重要操作确认 ===") print("OpenIE导入时会大量发送请求,可能会撞到请求速度上限,请注意选用的模型") @@ -239,6 +240,29 @@ def main(): # sourcery skip: dict-comprehension return None +def main(): + """主函数 - 设置新的事件循环并运行异步主函数""" + # 检查是否有现有的事件循环 + try: + loop = asyncio.get_running_loop() + if loop.is_closed(): + # 如果事件循环已关闭,创建新的 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + except RuntimeError: + # 没有运行的事件循环,创建新的 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + # 在新的事件循环中运行异步主函数 + loop.run_until_complete(main_async()) + finally: + # 确保事件循环被正确关闭 + if not loop.is_closed(): + loop.close() + + if __name__ == "__main__": # logger.info(f"111111111111111111111111{ROOT_PATH}") main() diff --git a/src/chat/knowledge/embedding_store.py b/src/chat/knowledge/embedding_store.py index d0f6e774..dec5b595 100644 --- a/src/chat/knowledge/embedding_store.py +++ b/src/chat/knowledge/embedding_store.py @@ -117,30 +117,36 @@ class EmbeddingStore: self.idx2hash = None def _get_embedding(self, s: str) -> List[float]: - """获取字符串的嵌入向量,处理异步调用""" + """获取字符串的嵌入向量,使用完全同步的方式避免事件循环问题""" + # 创建新的事件循环并在完成后立即关闭 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: - # 尝试获取当前事件循环 - asyncio.get_running_loop() - # 如果在事件循环中,使用线程池执行 - import concurrent.futures - - def run_in_thread(): - return asyncio.run(get_embedding(s)) - - with concurrent.futures.ThreadPoolExecutor() as executor: - future = executor.submit(run_in_thread) - result = future.result() - if result is None: - logger.error(f"获取嵌入失败: {s}") - return [] - return result - except RuntimeError: - # 没有运行的事件循环,直接运行 - result = asyncio.run(get_embedding(s)) - if result is None: + # 创建新的LLMRequest实例 + from src.llm_models.utils_model import LLMRequest + from src.config.config import model_config + + llm = LLMRequest(model_set=model_config.model_task_config.embedding, request_type="embedding") + + # 使用新的事件循环运行异步方法 + embedding, _ = loop.run_until_complete(llm.get_embedding(s)) + + if embedding and len(embedding) > 0: + return embedding + else: logger.error(f"获取嵌入失败: {s}") return [] - return result + + except Exception as e: + logger.error(f"获取嵌入时发生异常: {s}, 错误: {e}") + return [] + finally: + # 确保事件循环被正确关闭 + try: + loop.close() + except Exception: + pass def _get_embeddings_batch_threaded(self, strs: List[str], chunk_size: int = 10, max_workers: int = 10, progress_callback=None) -> List[Tuple[str, List[float]]]: """使用多线程批量获取嵌入向量 @@ -181,8 +187,14 @@ class EmbeddingStore: for i, s in enumerate(chunk_strs): try: - # 直接使用异步函数 - embedding = asyncio.run(llm.get_embedding(s)) + # 在线程中创建独立的事件循环 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + embedding = loop.run_until_complete(llm.get_embedding(s)) + finally: + loop.close() + if embedding and len(embedding) > 0: chunk_results.append((start_idx + i, s, embedding[0])) # embedding[0] 是实际的向量 else: diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 55ab3b44..9dca4089 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -111,6 +111,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: async def get_embedding(text, request_type="embedding") -> Optional[List[float]]: """获取文本的embedding向量""" + # 每次都创建新的LLMRequest实例以避免事件循环冲突 llm = LLMRequest(model_set=model_config.model_task_config.embedding, request_type=request_type) try: embedding, _ = await llm.get_embedding(text) diff --git a/src/llm_models/model_client/base_client.py b/src/llm_models/model_client/base_client.py index 97c34546..807f6484 100644 --- a/src/llm_models/model_client/base_client.py +++ b/src/llm_models/model_client/base_client.py @@ -159,14 +159,23 @@ class ClientRegistry: return decorator - def get_client_class_instance(self, api_provider: APIProvider) -> BaseClient: + def get_client_class_instance(self, api_provider: APIProvider, force_new=False) -> BaseClient: """ 获取注册的API客户端实例 Args: api_provider: APIProvider实例 + force_new: 是否强制创建新实例(用于解决事件循环问题) Returns: BaseClient: 注册的API客户端实例 """ + # 如果强制创建新实例,直接创建不使用缓存 + if force_new: + if client_class := self.client_registry.get(api_provider.client_type): + return client_class(api_provider) + else: + raise KeyError(f"'{api_provider.client_type}' 类型的 Client 未注册") + + # 正常的缓存逻辑 if api_provider.name not in self.client_instance_cache: if client_class := self.client_registry.get(api_provider.client_type): self.client_instance_cache[api_provider.name] = client_class(api_provider) diff --git a/src/llm_models/model_client/openai_client.py b/src/llm_models/model_client/openai_client.py index c580899a..bba00f94 100644 --- a/src/llm_models/model_client/openai_client.py +++ b/src/llm_models/model_client/openai_client.py @@ -388,6 +388,7 @@ class OpenaiClient(BaseClient): base_url=api_provider.base_url, api_key=api_provider.api_key, max_retries=0, + timeout=api_provider.timeout, ) async def get_response( @@ -520,6 +521,11 @@ class OpenaiClient(BaseClient): extra_body=extra_params, ) except APIConnectionError as e: + # 添加详细的错误信息以便调试 + logger.error(f"OpenAI API连接错误(嵌入模型): {str(e)}") + logger.error(f"错误类型: {type(e)}") + if hasattr(e, '__cause__') and e.__cause__: + logger.error(f"底层错误: {str(e.__cause__)}") raise NetworkConnectionError() from e except APIStatusError as e: # 重封装APIError为RespNotOkException diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index e8e4db5f..f0229c2c 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -248,7 +248,11 @@ class LLMRequest: ) model_info = model_config.get_model_info(least_used_model_name) api_provider = model_config.get_provider(model_info.api_provider) - client = client_registry.get_client_class_instance(api_provider) + + # 对于嵌入任务,强制创建新的客户端实例以避免事件循环问题 + force_new_client = (self.request_type == "embedding") + client = client_registry.get_client_class_instance(api_provider, force_new=force_new_client) + logger.debug(f"选择请求模型: {model_info.name}") total_tokens, penalty, usage_penalty = self.model_usage[model_info.name] self.model_usage[model_info.name] = (total_tokens, penalty, usage_penalty + 1) # 增加使用惩罚值防止连续使用 From 64839559190d98830df558293ccef3c4692aaaac Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:11:03 +0000 Subject: [PATCH 05/18] fix(llm): Add retry mechanism for empty API responses --- src/llm_models/exceptions.py | 11 +++++ src/llm_models/model_client/gemini_client.py | 46 ++++++++++++-------- src/llm_models/model_client/openai_client.py | 9 +++- src/llm_models/utils_model.py | 21 ++++++--- 4 files changed, 61 insertions(+), 26 deletions(-) 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..2b2d9183 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 := getattr(resp, "candidates", None)) and 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 = getattr(resp, "text", None) - if resp.function_calls: + # 解析工具调用 + if function_calls := getattr(resp, "function_calls", None): 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 := getattr(resp, "usage_metadata", None): _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 # 不再重试请求该模型 From 1dbbbab8fa087508a9f58024e8fdd9bdb3af9bd3 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Tue, 26 Aug 2025 09:52:30 +0800 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Ds4u?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mais4u_chat/body_emotion_action_manager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mais4u/mais4u_chat/body_emotion_action_manager.py b/src/mais4u/mais4u_chat/body_emotion_action_manager.py index c30fd7ba..c2829276 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, @@ -185,7 +185,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( @@ -248,7 +248,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( From 527645016e95d40fb21389b57c3522caf3b5a208 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 26 Aug 2025 21:37:00 +0800 Subject: [PATCH 07/18] remove getattr in gemini client --- src/llm_models/model_client/gemini_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 2b2d9183..67f9a300 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -290,7 +290,7 @@ def _default_normal_response_parser( # 解析思考内容 try: - if (candidates := getattr(resp, "candidates", None)) and candidates: + if candidates := resp.candidates: if candidates[0].content and candidates[0].content.parts: for part in candidates[0].content.parts: if not part.text: @@ -303,10 +303,10 @@ def _default_normal_response_parser( logger.warning(f"解析思考内容时发生错误: {e},跳过解析") # 解析响应内容 - api_response.content = getattr(resp, "text", None) + api_response.content = resp.text # 解析工具调用 - if function_calls := getattr(resp, "function_calls", None): + if function_calls := resp.function_calls: api_response.tool_calls = [] for call in function_calls: try: @@ -319,7 +319,7 @@ def _default_normal_response_parser( raise RespParseException(resp, "响应解析失败,无法解析工具调用参数") from e # 解析使用情况 - if usage_metadata := getattr(resp, "usage_metadata", None): + if usage_metadata := resp.usage_metadata: _usage_record = ( usage_metadata.prompt_token_count or 0, (usage_metadata.candidates_token_count or 0) + (usage_metadata.thoughts_token_count or 0), From 01197cb2b70f4565cad778056ef27651aad35cd5 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 27 Aug 2025 20:43:48 +0800 Subject: [PATCH 08/18] =?UTF-8?q?fix=EF=BC=9A=E5=B0=9D=E8=AF=95=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DTTS=E6=8F=92=E4=BB=B6=E7=9A=84=E5=A4=8D=E8=AF=BB?= =?UTF-8?q?=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/built_in/tts_plugin/plugin.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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动作时未提供文本内容") From 8a55e14aa4318fb1526fd5b83618a474d23410d0 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Wed, 27 Aug 2025 21:51:29 +0800 Subject: [PATCH 09/18] =?UTF-8?q?events=E4=B8=BB=E4=BD=93=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 2 +- plugins/hello_world_plugin/plugin.py | 8 +- src/plugin_system/__init__.py | 4 +- src/plugin_system/base/__init__.py | 2 + src/plugin_system/base/base_event.py | 38 --- src/plugin_system/base/base_events_handler.py | 11 +- src/plugin_system/base/component_types.py | 6 + src/plugin_system/core/events_manager.py | 253 +++++++++++------- src/plugin_system/core/to_do_event.md | 9 +- 9 files changed, 190 insertions(+), 143 deletions(-) delete mode 100644 src/plugin_system/base/base_event.py diff --git a/bot.py b/bot.py index 631abd61..ea5244f2 100644 --- a/bot.py +++ b/bot.py @@ -66,7 +66,7 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression from src.plugin_system.core.events_manager import events_manager from src.plugin_system.base.component_types import EventType # 触发 ON_STOP 事件 - _ = await events_manager.handle_mai_events(event_type=EventType.ON_STOP) + await events_manager.handle_mai_events(event_type=EventType.ON_STOP) # 停止所有异步任务 await async_task_manager.stop_and_wait_all_tasks() diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index f9855481..c4e6d72c 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -11,7 +11,7 @@ from src.plugin_system import ( BaseEventHandler, EventType, MaiMessages, - ToolParamType + ToolParamType, ) @@ -136,12 +136,12 @@ class PrintMessage(BaseEventHandler): handler_name = "print_message_handler" handler_description = "打印接收到的消息" - async def execute(self, message: MaiMessages) -> Tuple[bool, bool, str | None]: + async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, str | None, None]: """执行打印消息事件处理""" # 打印接收到的消息 if self.get_config("print_message.enabled", False): - print(f"接收到消息: {message.raw_message}") - return True, True, "消息已打印" + print(f"接收到消息: {message.raw_message if message else '无效消息'}") + return True, True, "消息已打印", None # ===== 插件注册 ===== diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index a102ecd0..45b8de9b 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -25,6 +25,7 @@ from .base import ( EventType, MaiMessages, ToolParamType, + CustomEventHandlerResult, ) # 导入工具模块 @@ -37,7 +38,7 @@ from .utils import ( from .apis import ( chat_api, - tool_api, + tool_api, component_manage_api, config_api, database_api, @@ -92,6 +93,7 @@ __all__ = [ "ToolParamType", # 消息 "MaiMessages", + "CustomEventHandlerResult", # 装饰器 "register_plugin", "ConfigField", diff --git a/src/plugin_system/base/__init__.py b/src/plugin_system/base/__init__.py index bc63d35d..19b608e4 100644 --- a/src/plugin_system/base/__init__.py +++ b/src/plugin_system/base/__init__.py @@ -23,6 +23,7 @@ from .component_types import ( EventType, MaiMessages, ToolParamType, + CustomEventHandlerResult, ) from .config_types import ConfigField @@ -46,4 +47,5 @@ __all__ = [ "BaseEventHandler", "MaiMessages", "ToolParamType", + "CustomEventHandlerResult", ] diff --git a/src/plugin_system/base/base_event.py b/src/plugin_system/base/base_event.py deleted file mode 100644 index 6adb333c..00000000 --- a/src/plugin_system/base/base_event.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import TYPE_CHECKING, List, Type - -from src.common.logger import get_logger -from src.plugin_system.base.component_types import EventType, MaiMessages - -if TYPE_CHECKING: - from .base_events_handler import BaseEventHandler - -logger = get_logger("base_event") - -class BaseEvent: - def __init__(self, event_type: EventType | str) -> None: - self.event_type = event_type - self.subscribers: List["BaseEventHandler"] = [] - - def register_handler_to_event(self, handler: "BaseEventHandler") -> bool: - if handler not in self.subscribers: - self.subscribers.append(handler) - return True - logger.warning(f"Handler {handler.handler_name} 已经注册,不可多次注册") - return False - - def remove_handler_from_event(self, handler_class: Type["BaseEventHandler"]) -> bool: - for handler in self.subscribers: - if isinstance(handler, handler_class): - self.subscribers.remove(handler) - return True - logger.warning(f"Handler {handler_class.__name__} 未注册,无法移除") - return False - - def trigger_event(self, message: MaiMessages): - copied_message = message.deepcopy() - for handler in self.subscribers: - result = handler.execute(copied_message) - - # TODO: Unfinished Events Handler - - \ No newline at end of file diff --git a/src/plugin_system/base/base_events_handler.py b/src/plugin_system/base/base_events_handler.py index 630b1ef2..130858e7 100644 --- a/src/plugin_system/base/base_events_handler.py +++ b/src/plugin_system/base/base_events_handler.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod -from typing import Tuple, Optional, Dict +from typing import Tuple, Optional, Dict, List from src.common.logger import get_logger -from .component_types import MaiMessages, EventType, EventHandlerInfo, ComponentType +from .component_types import MaiMessages, EventType, EventHandlerInfo, ComponentType, CustomEventHandlerResult logger = get_logger("base_event_handler") @@ -30,16 +30,19 @@ class BaseEventHandler(ABC): """对应插件名""" self.plugin_config: Optional[Dict] = None """插件配置字典""" + self._events_subscribed: List[EventType | str] = [] if self.event_type == EventType.UNKNOWN: raise NotImplementedError("事件处理器必须指定 event_type") @abstractmethod - async def execute(self, message: MaiMessages | None) -> Tuple[bool, bool, Optional[str]]: + async def execute( + self, message: MaiMessages | None + ) -> Tuple[bool, bool, Optional[str], Optional[CustomEventHandlerResult]]: """执行事件处理的抽象方法,子类必须实现 Args: message (MaiMessages | None): 事件消息对象,当你注册的事件为ON_START和ON_STOP时message为None Returns: - Tuple[bool, bool, Optional[str]]: (是否执行成功, 是否需要继续处理, 可选的返回消息) + Tuple[bool, bool, Optional[str], Optional[CustomEventHandlerResult]]: (是否执行成功, 是否需要继续处理, 可选的返回消息, 可选的自定义结果) """ raise NotImplementedError("子类必须实现 execute 方法") diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index d02ad1ef..5473d7f0 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -285,3 +285,9 @@ class MaiMessages: def deepcopy(self): return copy.deepcopy(self) + +@dataclass +class CustomEventHandlerResult: + message: str = "" + timestamp: float = 0.0 + extra_info: Optional[Dict] = None \ No newline at end of file diff --git a/src/plugin_system/core/events_manager.py b/src/plugin_system/core/events_manager.py index b00dfd6f..baada939 100644 --- a/src/plugin_system/core/events_manager.py +++ b/src/plugin_system/core/events_manager.py @@ -5,7 +5,7 @@ from typing import List, Dict, Optional, Type, Tuple, TYPE_CHECKING from src.chat.message_receive.message import MessageRecv from src.chat.message_receive.chat_stream import get_chat_manager from src.common.logger import get_logger -from src.plugin_system.base.component_types import EventType, EventHandlerInfo, MaiMessages +from src.plugin_system.base.component_types import EventType, EventHandlerInfo, MaiMessages, CustomEventHandlerResult from src.plugin_system.base.base_events_handler import BaseEventHandler from .global_announcement_manager import global_announcement_manager @@ -18,9 +18,23 @@ logger = get_logger("events_manager") class EventsManager: def __init__(self): # 有权重的 events 订阅者注册表 - self._events_subscribers: Dict[EventType | str, List[BaseEventHandler]] = {event: [] for event in EventType} + self._events_subscribers: Dict[EventType | str, List[BaseEventHandler]] = {} self._handler_mapping: Dict[str, Type[BaseEventHandler]] = {} # 事件处理器映射表 self._handler_tasks: Dict[str, List[asyncio.Task]] = {} # 事件处理器正在处理的任务 + self._events_result_history: Dict[EventType | str, List[CustomEventHandlerResult]] = {} # 事件的结果历史记录 + self._history_enable_map: Dict[EventType | str, bool] = {} # 是否启用历史记录的映射表,同时作为events注册表 + + # 事件注册(同时作为注册样例) + for event in EventType: + self.register_event(event, enable_history_result=False) + + def register_event(self, event_type: EventType | str, enable_history_result: bool = False): + if event_type in self._events_subscribers: + raise ValueError(f"事件类型 {event_type} 已存在") + self._events_subscribers[event_type] = [] + self._history_enable_map[event_type] = enable_history_result + if enable_history_result: + self._events_result_history[event_type] = [] def register_event_subscriber(self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler]) -> bool: """注册事件处理器 @@ -32,69 +46,23 @@ class EventsManager: Returns: bool: 是否注册成功 """ + if not issubclass(handler_class, BaseEventHandler): + logger.error(f"类 {handler_class.__name__} 不是 BaseEventHandler 的子类") + return False + handler_name = handler_info.name if handler_name in self._handler_mapping: logger.warning(f"事件处理器 {handler_name} 已存在,跳过注册") return False - if not issubclass(handler_class, BaseEventHandler): - logger.error(f"类 {handler_class.__name__} 不是 BaseEventHandler 的子类") + if handler_info.event_type not in self._history_enable_map: + logger.error(f"事件类型 {handler_info.event_type} 未注册,无法为其注册处理器 {handler_name}") return False self._handler_mapping[handler_name] = handler_class return self._insert_event_handler(handler_class, handler_info) - def _prepare_message( - self, - event_type: EventType, - message: Optional[MessageRecv] = None, - llm_prompt: Optional[str] = None, - llm_response: Optional["LLMGenerationDataModel"] = None, - stream_id: Optional[str] = None, - action_usage: Optional[List[str]] = None, - ) -> Optional[MaiMessages]: - """根据事件类型和输入,准备和转换消息对象。""" - if message: - return self._transform_event_message(message, llm_prompt, llm_response) - - if event_type not in [EventType.ON_START, EventType.ON_STOP]: - assert stream_id, "如果没有消息,必须为非启动/关闭事件提供流ID" - if event_type in [EventType.ON_MESSAGE, EventType.ON_PLAN, EventType.POST_LLM, EventType.AFTER_LLM]: - return self._build_message_from_stream(stream_id, llm_prompt, llm_response) - else: - return self._transform_event_without_message(stream_id, llm_prompt, llm_response, action_usage) - - return None # ON_START, ON_STOP事件没有消息体 - - def _dispatch_handler_task(self, handler: BaseEventHandler, message: Optional[MaiMessages]): - """分发一个非阻塞(异步)的事件处理任务。""" - try: - task = asyncio.create_task(handler.execute(message)) - - task_name = f"{handler.plugin_name}-{handler.handler_name}" - task.set_name(task_name) - task.add_done_callback(self._task_done_callback) - - self._handler_tasks.setdefault(handler.handler_name, []).append(task) - except Exception as e: - logger.error(f"创建事件处理器任务 {handler.handler_name} 时发生异常: {e}", exc_info=True) - - async def _dispatch_intercepting_handler(self, handler: BaseEventHandler, message: Optional[MaiMessages]) -> bool: - """分发并等待一个阻塞(同步)的事件处理器,返回是否应继续处理。""" - try: - success, continue_processing, result = await handler.execute(message) - - if not success: - logger.error(f"EventHandler {handler.handler_name} 执行失败: {result}") - else: - logger.debug(f"EventHandler {handler.handler_name} 执行成功: {result}") - - return continue_processing - except Exception as e: - logger.error(f"EventHandler {handler.handler_name} 发生异常: {e}", exc_info=True) - return True # 发生异常时默认不中断其他处理 - async def handle_mai_events( self, event_type: EventType, @@ -115,6 +83,8 @@ class EventsManager: transformed_message = self._prepare_message( event_type, message, llm_prompt, llm_response, stream_id, action_usage ) + if transformed_message: + transformed_message = transformed_message.deepcopy() # 2. 获取并遍历处理器 handlers = self._events_subscribers.get(event_type, []) @@ -137,16 +107,68 @@ class EventsManager: handler.set_plugin_config(plugin_config) # 4. 根据类型分发任务 - if handler.intercept_message: + if handler.intercept_message or event_type == EventType.ON_STOP: # 让ON_STOP的所有事件处理器都发挥作用,防止还没执行即被取消 # 阻塞执行,并更新 continue_flag - should_continue = await self._dispatch_intercepting_handler(handler, transformed_message) + should_continue = await self._dispatch_intercepting_handler(handler, event_type, transformed_message) continue_flag = continue_flag and should_continue else: # 异步执行,不阻塞 - self._dispatch_handler_task(handler, transformed_message) + self._dispatch_handler_task(handler, event_type, transformed_message) return continue_flag + async def cancel_handler_tasks(self, handler_name: str) -> None: + tasks_to_be_cancelled = self._handler_tasks.get(handler_name, []) + if remaining_tasks := [task for task in tasks_to_be_cancelled if not task.done()]: + for task in remaining_tasks: + task.cancel() + try: + await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5) + logger.info(f"已取消事件处理器 {handler_name} 的所有任务") + except asyncio.TimeoutError: + logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消") + except Exception as e: + logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}") + if handler_name in self._handler_tasks: + del self._handler_tasks[handler_name] + + async def unregister_event_subscriber(self, handler_name: str) -> bool: + """取消注册事件处理器""" + if handler_name not in self._handler_mapping: + logger.warning(f"事件处理器 {handler_name} 不存在,无法取消注册") + return False + + await self.cancel_handler_tasks(handler_name) + + handler_class = self._handler_mapping.pop(handler_name) + if not self._remove_event_handler_instance(handler_class): + return False + + logger.info(f"事件处理器 {handler_name} 已成功取消注册") + return True + + async def get_event_result_history(self, event_type: EventType | str) -> List[CustomEventHandlerResult]: + """获取事件的结果历史记录""" + if event_type == EventType.UNKNOWN: + raise ValueError("未知事件类型") + if event_type not in self._history_enable_map: + raise ValueError(f"事件类型 {event_type} 未注册") + if not self._history_enable_map[event_type]: + raise ValueError(f"事件类型 {event_type} 的历史记录未启用") + + return self._events_result_history[event_type] + + async def clear_event_result_history(self, event_type: EventType | str) -> None: + """清空事件的结果历史记录""" + if event_type == EventType.UNKNOWN: + raise ValueError("未知事件类型") + if event_type not in self._history_enable_map: + raise ValueError(f"事件类型 {event_type} 未注册") + if not self._history_enable_map[event_type]: + raise ValueError(f"事件类型 {event_type} 的历史记录未启用") + + self._events_result_history[event_type] = [] + def _insert_event_handler(self, handler_class: Type[BaseEventHandler], handler_info: EventHandlerInfo) -> bool: """插入事件处理器到对应的事件类型列表中并设置其插件配置""" if handler_class.event_type == EventType.UNKNOWN: @@ -179,7 +201,10 @@ class EventsManager: return False def _transform_event_message( - self, message: MessageRecv, llm_prompt: Optional[str] = None, llm_response: Optional["LLMGenerationDataModel"] = None + self, + message: MessageRecv, + llm_prompt: Optional[str] = None, + llm_response: Optional["LLMGenerationDataModel"] = None, ) -> MaiMessages: """转换事件消息格式""" # 直接赋值部分内容 @@ -263,52 +288,100 @@ class EventsManager: additional_data={"response_is_processed": True}, ) - def _task_done_callback(self, task: asyncio.Task[Tuple[bool, bool, str | None]]): + def _prepare_message( + self, + event_type: EventType, + message: Optional[MessageRecv] = None, + llm_prompt: Optional[str] = None, + llm_response: Optional["LLMGenerationDataModel"] = None, + stream_id: Optional[str] = None, + action_usage: Optional[List[str]] = None, + ) -> Optional[MaiMessages]: + """根据事件类型和输入,准备和转换消息对象。""" + if message: + return self._transform_event_message(message, llm_prompt, llm_response) + + if event_type not in [EventType.ON_START, EventType.ON_STOP]: + assert stream_id, "如果没有消息,必须为非启动/关闭事件提供流ID" + if event_type in [EventType.ON_MESSAGE, EventType.ON_PLAN, EventType.POST_LLM, EventType.AFTER_LLM]: + return self._build_message_from_stream(stream_id, llm_prompt, llm_response) + else: + return self._transform_event_without_message(stream_id, llm_prompt, llm_response, action_usage) + + return None # ON_START, ON_STOP事件没有消息体 + + def _dispatch_handler_task( + self, handler: BaseEventHandler, event_type: EventType | str, message: Optional[MaiMessages] = None + ): + """分发一个非阻塞(异步)的事件处理任务。""" + if event_type == EventType.UNKNOWN: + raise ValueError("未知事件类型") + try: + task = asyncio.create_task(handler.execute(message)) + + task_name = f"{handler.plugin_name}-{handler.handler_name}" + task.set_name(task_name) + task.add_done_callback(lambda t: self._task_done_callback(t, event_type)) + + self._handler_tasks.setdefault(handler.handler_name, []).append(task) + except Exception as e: + logger.error(f"创建事件处理器任务 {handler.handler_name} 时发生异常: {e}", exc_info=True) + + async def _dispatch_intercepting_handler( + self, handler: BaseEventHandler, event_type: EventType | str, message: Optional[MaiMessages] = None + ) -> bool: + """分发并等待一个阻塞(同步)的事件处理器,返回是否应继续处理。""" + if event_type == EventType.UNKNOWN: + raise ValueError("未知事件类型") + if event_type not in self._history_enable_map: + raise ValueError(f"事件类型 {event_type} 未注册") + try: + success, continue_processing, return_message, custom_result = await handler.execute(message) + + if not success: + logger.error(f"EventHandler {handler.handler_name} 执行失败: {return_message}") + else: + logger.debug(f"EventHandler {handler.handler_name} 执行成功: {return_message}") + + if self._history_enable_map[event_type] and custom_result: + self._events_result_history[event_type].append(custom_result) + return continue_processing + except KeyError: + logger.error(f"事件 {event_type} 注册的历史记录启用情况与实际不符合") + return True + except Exception as e: + logger.error(f"EventHandler {handler.handler_name} 发生异常: {e}", exc_info=True) + return True # 发生异常时默认不中断其他处理 + + def _task_done_callback( + self, + task: asyncio.Task[Tuple[bool, bool, str | None, CustomEventHandlerResult | None]], + event_type: EventType | str, + ): """任务完成回调""" task_name = task.get_name() or "Unknown Task" + if event_type == EventType.UNKNOWN: + raise ValueError("未知事件类型") + if event_type not in self._history_enable_map: + raise ValueError(f"事件类型 {event_type} 未注册") try: - success, _, result = task.result() # 忽略是否继续的标志,因为消息本身未被拦截 + success, _, result, custom_result = task.result() # 忽略是否继续的标志,因为消息本身未被拦截 if success: logger.debug(f"事件处理任务 {task_name} 已成功完成: {result}") else: logger.error(f"事件处理任务 {task_name} 执行失败: {result}") + + if self._history_enable_map[event_type] and custom_result: + self._events_result_history[event_type].append(custom_result) except asyncio.CancelledError: pass + except KeyError: + logger.error(f"事件 {event_type} 注册的历史记录启用情况与实际不符合") except Exception as e: logger.error(f"事件处理任务 {task_name} 发生异常: {e}") finally: with contextlib.suppress(ValueError, KeyError): self._handler_tasks[task_name].remove(task) - async def cancel_handler_tasks(self, handler_name: str) -> None: - tasks_to_be_cancelled = self._handler_tasks.get(handler_name, []) - if remaining_tasks := [task for task in tasks_to_be_cancelled if not task.done()]: - for task in remaining_tasks: - task.cancel() - try: - await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5) - logger.info(f"已取消事件处理器 {handler_name} 的所有任务") - except asyncio.TimeoutError: - logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消") - except Exception as e: - logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}") - if handler_name in self._handler_tasks: - del self._handler_tasks[handler_name] - - async def unregister_event_subscriber(self, handler_name: str) -> bool: - """取消注册事件处理器""" - if handler_name not in self._handler_mapping: - logger.warning(f"事件处理器 {handler_name} 不存在,无法取消注册") - return False - - await self.cancel_handler_tasks(handler_name) - - handler_class = self._handler_mapping.pop(handler_name) - if not self._remove_event_handler_instance(handler_class): - return False - - logger.info(f"事件处理器 {handler_name} 已成功取消注册") - return True - events_manager = EventsManager() diff --git a/src/plugin_system/core/to_do_event.md b/src/plugin_system/core/to_do_event.md index 11923ff0..bebce6d9 100644 --- a/src/plugin_system/core/to_do_event.md +++ b/src/plugin_system/core/to_do_event.md @@ -1,13 +1,12 @@ - [x] 自定义事件 - [ ] 允许handler随时订阅 -- [ ] 允许handler随时取消订阅 -- [ ] 允许其他组件给handler增加订阅 -- [ ] 允许其他组件给handler取消订阅 +- [x] 允许其他组件给handler增加订阅 +- [x] 允许其他组件给handler取消订阅 - [ ] 允许一个handler订阅多个事件 -- [ ] event激活时给handler传递参数 +- [x] event激活时给handler传递参数 - [ ] handler能拿到所有handlers的结果(按照处理权重) - [x] 随时注册 -- [ ] 删除event +- [ ] 删除event - [ ] 必要性? - [ ] 能够更改prompt - [ ] 能够更改llm_response From 6d3e9fd3d4a51cffaf9603f777add9aa9ff72a49 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 27 Aug 2025 22:18:22 +0800 Subject: [PATCH 10/18] =?UTF-8?q?feat=EF=BC=9A=E8=AE=B0=E5=BF=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E9=87=8D=E5=87=BA=E6=B1=9F=E6=B9=96=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=BA=86=E5=8D=B3=E6=97=B6=E8=AE=B0=E5=BF=86=E5=92=8C?= =?UTF-8?q?=E5=AE=9A=E6=9C=9F=E8=AE=B0=E5=BF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelogs/changelog.md | 14 + src/chat/heart_flow/heartFC_chat.py | 10 +- src/chat/memory_system/Hippocampus.py | 277 +++++++++--------- src/chat/memory_system/instant_memory.py | 254 ---------------- src/chat/replyer/default_generator.py | 17 +- src/chat/utils/utils.py | 76 +++++ src/common/database/database_model.py | 17 -- src/config/official_configs.py | 14 +- src/plugins/built_in/memory/_manifest.json | 34 +++ src/plugins/built_in/memory/build_memory.py | 134 +++++++++ src/plugins/built_in/memory/plugin.py | 58 ++++ .../built_in/plugin_management/_manifest.json | 2 +- src/plugins/built_in/relation/relation.py | 23 +- template/bot_config_template.toml | 37 +-- 14 files changed, 481 insertions(+), 486 deletions(-) delete mode 100644 src/chat/memory_system/instant_memory.py create mode 100644 src/plugins/built_in/memory/_manifest.json create mode 100644 src/plugins/built_in/memory/build_memory.py create mode 100644 src/plugins/built_in/memory/plugin.py diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 99fcd682..da96cb72 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,10 +1,24 @@ # Changelog + +TODO:回复频率动态控制 + +## [0.10.2] - 2025-8-24 +### 🌟 主要功能更改 +- 记忆系统重新启用,更好更优秀 +- 更好的event系统 +- 为空回复添加重试机制 + +### 细节功能更改 +- 修复tts插件可能的复读问题 + + ## [0.10.1] - 2025-8-24 ### 🌟 主要功能更改 - planner现在改为大小核结构,移除激活阶段,提高回复速度和动作调用精准度 - 优化关系的表现的效率 +### 细节功能更改 - 优化识图的表现 - 为planner添加单独控制的提示词 - 修复激活值计算异常的BUG diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 8680392a..18aec433 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -385,12 +385,6 @@ class HeartFChatting: async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): await self.expression_learner.trigger_learning_for_chat() - # # 记忆构建:为当前chat_id构建记忆 - # try: - # await hippocampus_manager.build_memory_for_chat(self.stream_id) - # except Exception as e: - # logger.error(f"{self.log_prefix} 记忆构建失败: {e}") - available_actions: Dict[str, ActionInfo] = {} if random.random() > self.focus_value_control.get_current_focus_value() and mode == ChatMode.FOCUS: # 如果激活度没有激活,并且聊天活跃度低,有可能不进行plan,相当于不在电脑前,不进行认真思考 @@ -445,8 +439,8 @@ class HeartFChatting: available_actions=available_actions, ) - for action in action_to_use_info: - print(action.action_type) + # for action in action_to_use_info: + # print(action.action_type) # 3. 并行执行所有动作 action_tasks = [ diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index 1b15d717..bb8d6c5c 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -18,6 +18,7 @@ from src.config.config import global_config, model_config from src.common.data_models.database_data_model import DatabaseMessages from src.common.database.database_model import GraphNodes, GraphEdges # Peewee Models导入 from src.common.logger import get_logger +from src.chat.utils.utils import cut_key_words from src.chat.utils.chat_message_builder import ( build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive, @@ -98,19 +99,23 @@ class MemoryGraph: current_weight = self.G.nodes[concept].get("weight", 0.0) self.G.nodes[concept]["weight"] = current_weight + 1.0 logger.debug(f"节点 {concept} 记忆整合成功,权重增加到 {current_weight + 1.0}") + logger.info(f"节点 {concept} 记忆内容已更新:{integrated_memory}") except Exception as e: logger.error(f"LLM整合记忆失败: {e}") # 降级到简单连接 new_memory_str = f"{existing_memory} | {memory}" self.G.nodes[concept]["memory_items"] = new_memory_str + logger.info(f"节点 {concept} 记忆内容已简单拼接并更新:{new_memory_str}") else: new_memory_str = str(memory) self.G.nodes[concept]["memory_items"] = new_memory_str + logger.info(f"节点 {concept} 记忆内容已直接更新:{new_memory_str}") else: self.G.nodes[concept]["memory_items"] = str(memory) # 如果节点存在但没有memory_items,说明是第一次添加memory,设置created_time if "created_time" not in self.G.nodes[concept]: self.G.nodes[concept]["created_time"] = current_time + logger.info(f"节点 {concept} 创建新记忆:{str(memory)}") # 更新最后修改时间 self.G.nodes[concept]["last_modified"] = current_time else: @@ -122,6 +127,7 @@ class MemoryGraph: created_time=current_time, # 添加创建时间 last_modified=current_time, ) # 添加最后修改时间 + logger.info(f"新节点 {concept} 已添加,记忆内容已写入:{str(memory)}") def get_dot(self, concept): # 检查节点是否存在于图中 @@ -402,9 +408,7 @@ class Hippocampus: text_length = len(text) topic_num: int | list[int] = 0 - words = jieba.cut(text) - keywords_lite = [word for word in words if len(word) > 1] - keywords_lite = list(set(keywords_lite)) + keywords_lite = cut_key_words(text) if keywords_lite: logger.debug(f"提取关键词极简版: {keywords_lite}") @@ -1159,6 +1163,131 @@ class ParahippocampalGyrus: return compressed_memory, similar_topics_dict + def get_similar_topics_from_keywords( + self, + keywords: list[str] | str, + top_k: int = 3, + threshold: float = 0.7, + ) -> dict[str, list[tuple[str, float]]]: + """基于输入的关键词,返回每个关键词对应的相似主题列表。 + + Args: + keywords: 关键词列表或以逗号/空格/顿号分隔的字符串。 + top_k: 每个关键词返回的相似主题数量上限。 + threshold: 相似度阈值,低于该值的主题将被过滤。 + + Returns: + dict[str, list[tuple[str, float]]]: {keyword: [(topic, similarity), ...]} + """ + # 规范化输入为列表[str] + if isinstance(keywords, str): + # 支持中英文逗号、顿号、空格分隔 + parts = ( + keywords.replace(",", ",").replace("、", ",").replace(" ", ",").strip(", ") + ) + keyword_list = [p.strip() for p in parts.split(",") if p.strip()] + else: + keyword_list = [k.strip() for k in keywords if isinstance(k, str) and k.strip()] + + if not keyword_list: + return {} + + existing_topics = list(self.memory_graph.G.nodes()) + result: dict[str, list[tuple[str, float]]] = {} + + for kw in keyword_list: + kw_words = set(jieba.cut(kw)) + similar_topics: list[tuple[str, float]] = [] + + for topic in existing_topics: + topic_words = set(jieba.cut(topic)) + all_words = kw_words | topic_words + if not all_words: + continue + v1 = [1 if w in kw_words else 0 for w in all_words] + v2 = [1 if w in topic_words else 0 for w in all_words] + sim = cosine_similarity(v1, v2) + if sim >= threshold: + similar_topics.append((topic, sim)) + + similar_topics.sort(key=lambda x: x[1], reverse=True) + result[kw] = similar_topics[:top_k] + + return result + + async def add_memory_with_similar( + self, + memory_item: str, + similar_topics_dict: dict[str, list[tuple[str, float]]], + ) -> bool: + """将单条记忆内容与相似主题写入记忆网络并同步数据库。 + + 按 build_memory_for_chat 的方式:为 similar_topics_dict 的每个键作为主题添加节点内容, + 并与其相似主题建立连接,连接强度为 int(similarity * 10)。 + + Args: + memory_item: 记忆内容字符串,将作为每个主题节点的 memory_items。 + similar_topics_dict: {topic: [(similar_topic, similarity), ...]} + + Returns: + bool: 是否成功执行添加与同步。 + """ + try: + if not memory_item or not isinstance(memory_item, str): + return False + + if not similar_topics_dict or not isinstance(similar_topics_dict, dict): + return False + + current_time = time.time() + + # 为每个主题写入节点 + for topic, similar_list in similar_topics_dict.items(): + if not topic or not isinstance(topic, str): + continue + + await self.hippocampus.memory_graph.add_dot(topic, memory_item, self.hippocampus) + + # 连接相似主题 + if isinstance(similar_list, list): + for item in similar_list: + try: + similar_topic, similarity = item + except Exception: + continue + if not isinstance(similar_topic, str): + continue + if topic == similar_topic: + continue + # 强度按 build_memory_for_chat 的规则 + strength = int(max(0.0, float(similarity)) * 10) if similarity is not None else 0 + if strength <= 0: + continue + # 确保相似主题节点存在(如果没有,也可以只建立边,networkx会创建节点,但需初始化属性) + if similar_topic not in self.memory_graph.G: + # 创建一个空的相似主题节点,避免悬空边,memory_items 为空字符串 + self.memory_graph.G.add_node( + similar_topic, + memory_items="", + weight=1.0, + created_time=current_time, + last_modified=current_time, + ) + self.memory_graph.G.add_edge( + topic, + similar_topic, + strength=strength, + created_time=current_time, + last_modified=current_time, + ) + + # 同步数据库 + await self.hippocampus.entorhinal_cortex.sync_memory_to_db() + return True + except Exception as e: + logger.error(f"添加记忆节点失败: {e}") + return False + async def operation_forget_topic(self, percentage=0.005): start_time = time.time() logger.info("[遗忘] 开始检查数据库...") @@ -1325,7 +1454,6 @@ class HippocampusManager: logger.info(f""" -------------------------------- 记忆系统参数配置: - 构建频率: {global_config.memory.memory_build_frequency}秒|压缩率: {global_config.memory.memory_compress_rate} 遗忘间隔: {global_config.memory.forget_memory_interval}秒|遗忘比例: {global_config.memory.memory_forget_percentage}|遗忘: {global_config.memory.memory_forget_time}小时之后 记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count} --------------------------------""") # noqa: E501 @@ -1343,61 +1471,6 @@ class HippocampusManager: raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) - async def build_memory_for_chat(self, chat_id: str): - """为指定chat_id构建记忆(在heartFC_chat.py中调用)""" - if not self._initialized: - raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") - - try: - # 检查是否需要构建记忆 - logger.info(f"为 {chat_id} 构建记忆") - if memory_segment_manager.check_and_build_memory_for_chat(chat_id): - logger.info(f"为 {chat_id} 构建记忆,需要构建记忆") - messages = memory_segment_manager.get_messages_for_memory_build(chat_id, 50) - - build_probability = 0.3 * global_config.memory.memory_build_frequency - - if messages and random.random() < build_probability: - logger.info(f"为 {chat_id} 构建记忆,消息数量: {len(messages)}") - - # 调用记忆压缩和构建 - ( - compressed_memory, - similar_topics_dict, - ) = await self._hippocampus.parahippocampal_gyrus.memory_compress( - messages, global_config.memory.memory_compress_rate - ) - - # 添加记忆节点 - current_time = time.time() - for topic, memory in compressed_memory: - await self._hippocampus.memory_graph.add_dot(topic, memory, self._hippocampus) - - # 连接相似主题 - if topic in similar_topics_dict: - similar_topics = similar_topics_dict[topic] - for similar_topic, similarity in similar_topics: - if topic != similar_topic: - strength = int(similarity * 10) - self._hippocampus.memory_graph.G.add_edge( - topic, - similar_topic, - strength=strength, - created_time=current_time, - last_modified=current_time, - ) - - # 同步到数据库 - await self._hippocampus.entorhinal_cortex.sync_memory_to_db() - logger.info(f"为 {chat_id} 构建记忆完成") - return True - - except Exception as e: - logger.error(f"为 {chat_id} 构建记忆失败: {e}") - return False - - return False - async def get_memory_from_topic( self, valid_keywords: list[str], max_memory_num: int = 3, max_memory_length: int = 2, max_depth: int = 3 ) -> list: @@ -1441,89 +1514,3 @@ class HippocampusManager: # 创建全局实例 hippocampus_manager = HippocampusManager() - - -# 在Hippocampus类中添加新的记忆构建管理器 -class MemoryBuilder: - """记忆构建器 - - 为每个chat_id维护消息缓存和触发机制,类似ExpressionLearner - """ - - def __init__(self, chat_id: str): - self.chat_id = chat_id - self.last_update_time: float = time.time() - self.last_processed_time: float = 0.0 - - def should_trigger_memory_build(self) -> bool: - # sourcery skip: assign-if-exp, boolean-if-exp-identity, reintroduce-else - """检查是否应该触发记忆构建""" - current_time = time.time() - - # 检查时间间隔 - time_diff = current_time - self.last_update_time - if time_diff < 600 / global_config.memory.memory_build_frequency: - return False - - # 检查消息数量 - - recent_messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_update_time, - timestamp_end=current_time, - ) - - logger.info(f"最近消息数量: {len(recent_messages)},间隔时间: {time_diff}") - - if not recent_messages or len(recent_messages) < 30 / global_config.memory.memory_build_frequency: - return False - - return True - - def get_messages_for_memory_build(self, threshold: int = 25) -> List[DatabaseMessages]: - """获取用于记忆构建的消息""" - current_time = time.time() - - messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_update_time, - timestamp_end=current_time, - limit=threshold, - ) - if messages: - # 更新最后处理时间 - self.last_processed_time = current_time - self.last_update_time = current_time - - return messages or [] - - -class MemorySegmentManager: - """记忆段管理器 - - 管理所有chat_id的MemoryBuilder实例,自动检查和触发记忆构建 - """ - - def __init__(self): - self.builders: Dict[str, MemoryBuilder] = {} - - def get_or_create_builder(self, chat_id: str) -> MemoryBuilder: - """获取或创建指定chat_id的MemoryBuilder""" - if chat_id not in self.builders: - self.builders[chat_id] = MemoryBuilder(chat_id) - return self.builders[chat_id] - - def check_and_build_memory_for_chat(self, chat_id: str) -> bool: - """检查指定chat_id是否需要构建记忆,如果需要则返回True""" - builder = self.get_or_create_builder(chat_id) - return builder.should_trigger_memory_build() - - def get_messages_for_memory_build(self, chat_id: str, threshold: int = 25) -> List[DatabaseMessages]: - """获取指定chat_id用于记忆构建的消息""" - if chat_id not in self.builders: - return [] - return self.builders[chat_id].get_messages_for_memory_build(threshold) - - -# 创建全局实例 -memory_segment_manager = MemorySegmentManager() diff --git a/src/chat/memory_system/instant_memory.py b/src/chat/memory_system/instant_memory.py deleted file mode 100644 index f8e91b5c..00000000 --- a/src/chat/memory_system/instant_memory.py +++ /dev/null @@ -1,254 +0,0 @@ -# -*- coding: utf-8 -*- -import time -import re -import json -import ast -import traceback - -from json_repair import repair_json -from datetime import datetime, timedelta - -from src.llm_models.utils_model import LLMRequest -from src.common.logger import get_logger -from src.common.database.database_model import Memory # Peewee Models导入 -from src.config.config import model_config, global_config - - -logger = get_logger(__name__) - - -class MemoryItem: - def __init__(self, memory_id: str, chat_id: str, memory_text: str, keywords: list[str]): - self.memory_id = memory_id - self.chat_id = chat_id - self.memory_text: str = memory_text - self.keywords: list[str] = keywords - self.create_time: float = time.time() - self.last_view_time: float = time.time() - - -class MemoryManager: - def __init__(self): - # self.memory_items:list[MemoryItem] = [] - pass - - -class InstantMemory: - def __init__(self, chat_id): - self.chat_id = chat_id - self.last_view_time = time.time() - self.summary_model = LLMRequest( - model_set=model_config.model_task_config.utils, - request_type="memory.summary", - ) - - async def if_need_build(self, text: str): - prompt = f""" -请判断以下内容中是否有值得记忆的信息,如果有,请输出1,否则输出0 -{text} -请只输出1或0就好 - """ - - try: - response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5) - if global_config.debug.show_prompt: - print(prompt) - print(response) - - return "1" in response - except Exception as e: - logger.error(f"判断是否需要记忆出现错误:{str(e)} {traceback.format_exc()}") - return False - - async def build_memory(self, text): - prompt = f""" - 以下内容中存在值得记忆的信息,请你从中总结出一段值得记忆的信息,并输出 - {text} - 请以json格式输出一段概括的记忆内容和关键词 - {{ - "memory_text": "记忆内容", - "keywords": "关键词,用/划分" - }} - """ - try: - response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5) - # print(prompt) - # print(response) - if not response: - return None - try: - repaired = repair_json(response) - result = json.loads(repaired) - memory_text = result.get("memory_text", "") - keywords = result.get("keywords", "") - if isinstance(keywords, str): - keywords_list = [k.strip() for k in keywords.split("/") if k.strip()] - elif isinstance(keywords, list): - keywords_list = keywords - else: - keywords_list = [] - return {"memory_text": memory_text, "keywords": keywords_list} - except Exception as parse_e: - logger.error(f"解析记忆json失败:{str(parse_e)} {traceback.format_exc()}") - return None - except Exception as e: - logger.error(f"构建记忆出现错误:{str(e)} {traceback.format_exc()}") - return None - - async def create_and_store_memory(self, text: str): - if_need = await self.if_need_build(text) - if if_need: - logger.info(f"需要记忆:{text}") - memory = await self.build_memory(text) - if memory and memory.get("memory_text"): - memory_id = f"{self.chat_id}_{time.time()}" - memory_item = MemoryItem( - memory_id=memory_id, - chat_id=self.chat_id, - memory_text=memory["memory_text"], - keywords=memory.get("keywords", []), - ) - await self.store_memory(memory_item) - else: - logger.info(f"不需要记忆:{text}") - - async def store_memory(self, memory_item: MemoryItem): - memory = Memory( - memory_id=memory_item.memory_id, - chat_id=memory_item.chat_id, - memory_text=memory_item.memory_text, - keywords=memory_item.keywords, - create_time=memory_item.create_time, - last_view_time=memory_item.last_view_time, - ) - memory.save() - - async def get_memory(self, target: str): - from json_repair import repair_json - - prompt = f""" -请根据以下发言内容,判断是否需要提取记忆 -{target} -请用json格式输出,包含以下字段: -其中,time的要求是: -可以选择具体日期时间,格式为YYYY-MM-DD HH:MM:SS,或者大致时间,格式为YYYY-MM-DD -可以选择相对时间,例如:今天,昨天,前天,5天前,1个月前 -可以选择留空进行模糊搜索 -{{ - "need_memory": 1, - "keywords": "希望获取的记忆关键词,用/划分", - "time": "希望获取的记忆大致时间" -}} -请只输出json格式,不要输出其他多余内容 -""" - try: - response, _ = await self.summary_model.generate_response_async(prompt, temperature=0.5) - if global_config.debug.show_prompt: - print(prompt) - print(response) - if not response: - return None - try: - repaired = repair_json(response) - result = json.loads(repaired) - # 解析keywords - keywords = result.get("keywords", "") - if isinstance(keywords, str): - keywords_list = [k.strip() for k in keywords.split("/") if k.strip()] - elif isinstance(keywords, list): - keywords_list = keywords - else: - keywords_list = [] - # 解析time为时间段 - time_str = result.get("time", "").strip() - start_time, end_time = self._parse_time_range(time_str) - logger.info(f"start_time: {start_time}, end_time: {end_time}") - # 检索包含关键词的记忆 - memories_set = set() - if start_time and end_time: - start_ts = start_time.timestamp() - end_ts = end_time.timestamp() - query = Memory.select().where( - (Memory.chat_id == self.chat_id) - & (Memory.create_time >= start_ts) # type: ignore - & (Memory.create_time < end_ts) # type: ignore - ) - else: - query = Memory.select().where(Memory.chat_id == self.chat_id) - - for mem in query: - # 对每条记忆 - mem_keywords = mem.keywords or "" - parsed = ast.literal_eval(mem_keywords) - if isinstance(parsed, list): - mem_keywords = [str(k).strip() for k in parsed if str(k).strip()] - else: - mem_keywords = [] - # logger.info(f"mem_keywords: {mem_keywords}") - # logger.info(f"keywords_list: {keywords_list}") - for kw in keywords_list: - # logger.info(f"kw: {kw}") - # logger.info(f"kw in mem_keywords: {kw in mem_keywords}") - if kw in mem_keywords: - # logger.info(f"mem.memory_text: {mem.memory_text}") - memories_set.add(mem.memory_text) - break - return list(memories_set) - except Exception as parse_e: - logger.error(f"解析记忆json失败:{str(parse_e)} {traceback.format_exc()}") - return None - except Exception as e: - logger.error(f"获取记忆出现错误:{str(e)} {traceback.format_exc()}") - return None - - def _parse_time_range(self, time_str): - # sourcery skip: extract-duplicate-method, use-contextlib-suppress - """ - 支持解析如下格式: - - 具体日期时间:YYYY-MM-DD HH:MM:SS - - 具体日期:YYYY-MM-DD - - 相对时间:今天,昨天,前天,N天前,N个月前 - - 空字符串:返回(None, None) - """ - now = datetime.now() - if not time_str: - return 0, now - time_str = time_str.strip() - # 具体日期时间 - try: - dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") - return dt, dt + timedelta(hours=1) - except Exception: - pass - # 具体日期 - try: - dt = datetime.strptime(time_str, "%Y-%m-%d") - return dt, dt + timedelta(days=1) - except Exception: - pass - # 相对时间 - if time_str == "今天": - start = now.replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if time_str == "昨天": - start = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if time_str == "前天": - start = (now - timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if m := re.match(r"(\d+)天前", time_str): - days = int(m.group(1)) - start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - if m := re.match(r"(\d+)个月前", time_str): - months = int(m.group(1)) - # 近似每月30天 - start = (now - timedelta(days=months * 30)).replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return start, end - # 其他无法解析 - return 0, now diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 1db4efa6..51477d89 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -26,7 +26,6 @@ from src.chat.utils.chat_message_builder import ( ) from src.chat.express.expression_selector import expression_selector from src.chat.memory_system.memory_activator import MemoryActivator -from src.chat.memory_system.instant_memory import InstantMemory from src.mood.mood_manager import mood_manager from src.person_info.person_info import Person, is_person_known from src.plugin_system.base.component_types import ActionInfo, EventType @@ -147,7 +146,6 @@ class DefaultReplyer: self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id) self.heart_fc_sender = HeartFCSender() self.memory_activator = MemoryActivator() - self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id) from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 @@ -375,20 +373,11 @@ class DefaultReplyer: instant_memory = None - # running_memories = await self.memory_activator.activate_memory_with_chat_history( - # target_message=target, chat_history=chat_history - # ) + running_memories = await self.memory_activator.activate_memory_with_chat_history( + target_message=target, chat_history=chat_history + ) running_memories = None - if global_config.memory.enable_instant_memory: - chat_history_str = build_readable_messages( - messages=chat_history, replace_bot_name=True, timestamp_mode="normal" - ) - asyncio.create_task(self.instant_memory.create_and_store_memory(chat_history_str)) - - instant_memory = await self.instant_memory.get_memory(target) - logger.info(f"即时记忆:{instant_memory}") - if not running_memories: return "" diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index b489e1e7..88562fff 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -834,3 +834,79 @@ def parse_keywords_string(keywords_input) -> list[str]: # 如果没有分隔符,返回单个关键词 return [keywords_str] if keywords_str else [] + + + + +def cut_key_words(concept_name: str) -> list[str]: + """对概念名称进行jieba分词,并过滤掉关键词列表中的关键词""" + concept_name_tokens = list(jieba.cut(concept_name)) + + # 定义常见连词、停用词与标点 + conjunctions = { + "和", "与", "及", "跟", "以及", "并且", "而且", "或", "或者", "并" + } + stop_words = { + "的", "了", "呢", "吗", "吧", "啊", "哦", "恩", "嗯", "呀", "嘛", "哇", + "在", "是", "很", "也", "又", "就", "都", "还", "更", "最", "被", "把", + "给", "对", "和", "与", "及", "跟", "并", "而且", "或者", "或", "以及" + } + chinese_punctuations = set(",。!?、;:()【】《》“”‘’—…·-——,.!?;:()[]<>'\"/\\") + + # 清理空白并初步过滤纯标点 + cleaned_tokens = [] + for tok in concept_name_tokens: + t = tok.strip() + if not t: + continue + # 去除纯标点 + if all(ch in chinese_punctuations for ch in t): + continue + cleaned_tokens.append(t) + + # 合并连词两侧的词(仅当两侧都存在且不是标点/停用词时) + merged_tokens = [] + i = 0 + n = len(cleaned_tokens) + while i < n: + tok = cleaned_tokens[i] + if tok in conjunctions and merged_tokens and i + 1 < n: + left = merged_tokens[-1] + right = cleaned_tokens[i + 1] + # 左右都需要是有效词 + if left and right \ + and left not in conjunctions and right not in conjunctions \ + and left not in stop_words and right not in stop_words \ + and not all(ch in chinese_punctuations for ch in left) \ + and not all(ch in chinese_punctuations for ch in right): + # 合并为一个新词,并替换掉左侧与跳过右侧 + combined = f"{left}{tok}{right}" + merged_tokens[-1] = combined + i += 2 + continue + # 常规推进 + merged_tokens.append(tok) + i += 1 + + # 二次过滤:去除停用词、单字符纯标点与无意义项 + result_tokens = [] + seen = set() + # ban_words = set(getattr(global_config.memory, "memory_ban_words", []) or []) + for tok in merged_tokens: + if tok in conjunctions: + # 独立连词丢弃 + continue + if tok in stop_words: + continue + # if tok in ban_words: + # continue + if all(ch in chinese_punctuations for ch in tok): + continue + if tok.strip() == "": + continue + if tok not in seen: + seen.add(tok) + result_tokens.append(tok) + + filtered_concept_name_tokens = result_tokens + return filtered_concept_name_tokens \ No newline at end of file diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index 8a6ea8cb..330bfa7d 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -298,19 +298,6 @@ class GroupInfo(BaseModel): # database = db # 继承自 BaseModel table_name = "group_info" - -class Memory(BaseModel): - memory_id = TextField(index=True) - chat_id = TextField(null=True) - memory_text = TextField(null=True) - keywords = TextField(null=True) - create_time = FloatField(null=True) - last_view_time = FloatField(null=True) - - class Meta: - table_name = "memory" - - class Expression(BaseModel): """ 用于存储表达风格的模型。 @@ -377,7 +364,6 @@ def create_tables(): Expression, GraphNodes, # 添加图节点表 GraphEdges, # 添加图边表 - Memory, ActionRecords, # 添加 ActionRecords 到初始化列表 ] ) @@ -403,7 +389,6 @@ def initialize_database(sync_constraints=False): OnlineTime, PersonInfo, Expression, - Memory, GraphNodes, GraphEdges, ActionRecords, # 添加 ActionRecords 到初始化列表 @@ -501,7 +486,6 @@ def sync_field_constraints(): OnlineTime, PersonInfo, Expression, - Memory, GraphNodes, GraphEdges, ActionRecords, @@ -680,7 +664,6 @@ def check_field_constraints(): OnlineTime, PersonInfo, Expression, - Memory, GraphNodes, GraphEdges, ActionRecords, diff --git a/src/config/official_configs.py b/src/config/official_configs.py index c99c5dad..7d9d950b 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -60,9 +60,6 @@ class RelationshipConfig(ConfigBase): enable_relationship: bool = True """是否启用关系系统""" - relation_frequency: int = 1 - """关系频率,麦麦构建关系的速度""" - @dataclass class ChatConfig(ConfigBase): @@ -336,14 +333,8 @@ class MemoryConfig(ConfigBase): enable_memory: bool = True """是否启用记忆系统""" - - memory_build_frequency: int = 1 - """记忆构建频率(秒)""" - memory_compress_rate: float = 0.1 - """记忆压缩率""" - - forget_memory_interval: int = 1000 + forget_memory_interval: int = 1500 """记忆遗忘间隔(秒)""" memory_forget_time: int = 24 @@ -355,9 +346,6 @@ class MemoryConfig(ConfigBase): memory_ban_words: list[str] = field(default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"]) """不允许记忆的词列表""" - enable_instant_memory: bool = True - """是否启用即时记忆""" - @dataclass class MoodConfig(ConfigBase): diff --git a/src/plugins/built_in/memory/_manifest.json b/src/plugins/built_in/memory/_manifest.json new file mode 100644 index 00000000..08a58540 --- /dev/null +++ b/src/plugins/built_in/memory/_manifest.json @@ -0,0 +1,34 @@ +{ + "manifest_version": 1, + "name": "Memory Build组件", + "version": "1.0.0", + "description": "可以构建和管理记忆", + "author": { + "name": "Mai", + "url": "https://github.com/MaiM-with-u" + }, + "license": "GPL-v3.0-or-later", + + "host_application": { + "min_version": "0.10.1" + }, + "homepage_url": "https://github.com/MaiM-with-u/maibot", + "repository_url": "https://github.com/MaiM-with-u/maibot", + "keywords": ["memory", "build", "built-in"], + "categories": ["Memory"], + + "default_locale": "zh-CN", + "locales_path": "_locales", + + "plugin_info": { + "is_built_in": true, + "plugin_type": "action_provider", + "components": [ + { + "type": "build_memory", + "name": "build_memory", + "description": "构建记忆" + } + ] + } +} diff --git a/src/plugins/built_in/memory/build_memory.py b/src/plugins/built_in/memory/build_memory.py new file mode 100644 index 00000000..939f6c23 --- /dev/null +++ b/src/plugins/built_in/memory/build_memory.py @@ -0,0 +1,134 @@ +from typing import Tuple + +from src.common.logger import get_logger +from src.config.config import global_config +from src.chat.utils.prompt_builder import Prompt +from src.plugin_system import BaseAction, ActionActivationType +from src.chat.memory_system.Hippocampus import hippocampus_manager +from src.chat.utils.utils import cut_key_words + +logger = get_logger("memory") + + +def init_prompt(): + Prompt( + """ +以下是一些记忆条目的分类: +---------------------- +{category_list} +---------------------- +每一个分类条目类型代表了你对用户:"{person_name}"的印象的一个类别 + +现在,你有一条对 {person_name} 的新记忆内容: +{memory_point} + +请判断该记忆内容是否属于上述分类,请给出分类的名称。 +如果不属于上述分类,请输出一个合适的分类名称,对新记忆内容进行概括。要求分类名具有概括性。 +注意分类数一般不超过5个 +请严格用json格式输出,不要输出任何其他内容: +{{ + "category": "分类名称" +}} """, + "relation_category", + ) + + Prompt( + """ +以下是有关{category}的现有记忆: +---------------------- +{memory_list} +---------------------- + +现在,你有一条对 {person_name} 的新记忆内容: +{memory_point} + +请判断该新记忆内容是否已经存在于现有记忆中,你可以对现有进行进行以下修改: +注意,一般来说记忆内容不超过5个,且记忆文本不应太长 + +1.新增:当记忆内容不存在于现有记忆,且不存在矛盾,请用json格式输出: +{{ + "new_memory": "需要新增的记忆内容" +}} +2.加深印象:如果这个新记忆已经存在于现有记忆中,在内容上与现有记忆类似,请用json格式输出: +{{ + "memory_id": 1, #请输出你认为需要加深印象的,与新记忆内容类似的,已经存在的记忆的序号 + "integrate_memory": "加深后的记忆内容,合并内容类似的新记忆和旧记忆" +}} +3.整合:如果这个新记忆与现有记忆产生矛盾,请你结合其他记忆进行整合,用json格式输出: +{{ + "memory_id": 1, #请输出你认为需要整合的,与新记忆存在矛盾的,已经存在的记忆的序号 + "integrate_memory": "整合后的记忆内容,合并内容矛盾的新记忆和旧记忆" +}} + +现在,请你根据情况选出合适的修改方式,并输出json,不要输出其他内容: +""", + "relation_category_update", + ) + + +class BuildMemoryAction(BaseAction): + """关系动作 - 构建关系""" + + activation_type = ActionActivationType.LLM_JUDGE + parallel_action = True + + # 动作基本信息 + action_name = "build_memory" + action_description = "了解对于某个概念或者某件事的记忆,并存储下来,在之后的聊天中,你可以根据这条记忆来获取相关信息" + + # 动作参数定义 + action_parameters = { + "concept_name": "需要了解或记忆的概念或事件的名称", + "concept_description": "需要了解或记忆的概念或事件的描述,需要具体且明确", + } + + # 动作使用场景 + action_require = [ + "了解对于某个概念或者某件事的记忆,并存储下来,在之后的聊天中,你可以根据这条记忆来获取相关信息", + "有你不了解的概念", + "有人要求你记住某个概念或者事件", + "你对某件事或概念有新的理解,或产生了兴趣", + ] + + # 关联类型 + associated_types = ["text"] + + async def execute(self) -> Tuple[bool, str]: + """执行关系动作""" + + try: + # 1. 获取构建关系的原因 + concept_description = self.action_data.get("concept_description", "") + logger.info(f"{self.log_prefix} 添加记忆原因: {self.reasoning}") + concept_name = self.action_data.get("concept_name", "") + # 2. 获取目标用户信息 + + + + # 对 concept_name 进行jieba分词 + concept_name_tokens = cut_key_words(concept_name) + # logger.info(f"{self.log_prefix} 对 concept_name 进行分词结果: {concept_name_tokens}") + + filtered_concept_name_tokens = [ + token for token in concept_name_tokens if all(keyword not in token for keyword in global_config.memory.memory_ban_words) + ] + + if not filtered_concept_name_tokens: + logger.warning(f"{self.log_prefix} 过滤后的概念名称列表为空,跳过添加记忆") + return False, "过滤后的概念名称列表为空,跳过添加记忆" + + similar_topics_dict = hippocampus_manager.get_hippocampus().parahippocampal_gyrus.get_similar_topics_from_keywords(filtered_concept_name_tokens) + await hippocampus_manager.get_hippocampus().parahippocampal_gyrus.add_memory_with_similar(concept_description, similar_topics_dict) + + + + return True, f"成功添加记忆: {concept_name}" + + except Exception as e: + logger.error(f"{self.log_prefix} 构建记忆时出错: {e}") + return False, f"构建记忆时出错: {e}" + + + +# 还缺一个关系的太多遗忘和对应的提取 +init_prompt() diff --git a/src/plugins/built_in/memory/plugin.py b/src/plugins/built_in/memory/plugin.py new file mode 100644 index 00000000..8eaaf900 --- /dev/null +++ b/src/plugins/built_in/memory/plugin.py @@ -0,0 +1,58 @@ +from typing import List, Tuple, Type + +# 导入新插件系统 +from src.plugin_system import BasePlugin, register_plugin, ComponentInfo +from src.plugin_system.base.config_types import ConfigField + +# 导入依赖的系统组件 +from src.common.logger import get_logger + +from src.plugins.built_in.memory.build_memory import BuildMemoryAction + +logger = get_logger("relation_actions") + + +@register_plugin +class MemoryBuildPlugin(BasePlugin): + """关系动作插件 + + 系统内置插件,提供基础的聊天交互功能: + - Reply: 回复动作 + - NoReply: 不回复动作 + - Emoji: 表情动作 + + 注意:插件基本信息优先从_manifest.json文件中读取 + """ + + # 插件基本信息 + plugin_name: str = "memory_build" # 内部标识符 + enable_plugin: bool = True + dependencies: list[str] = [] # 插件依赖列表 + python_dependencies: list[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" + + # 配置节描述 + config_section_descriptions = { + "plugin": "插件启用配置", + "components": "核心组件启用配置", + } + + # 配置Schema定义 + config_schema: dict = { + "plugin": { + "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), + "config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"), + }, + "components": { + "memory_max_memory_num": ConfigField(type=int, default=10, description="记忆最大数量"), + }, + } + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + """返回插件包含的组件列表""" + + # --- 根据配置注册组件 --- + components = [] + components.append((BuildMemoryAction.get_action_info(), BuildMemoryAction)) + + return components diff --git a/src/plugins/built_in/plugin_management/_manifest.json b/src/plugins/built_in/plugin_management/_manifest.json index f394b867..a0175d77 100644 --- a/src/plugins/built_in/plugin_management/_manifest.json +++ b/src/plugins/built_in/plugin_management/_manifest.json @@ -9,7 +9,7 @@ }, "license": "GPL-v3.0-or-later", "host_application": { - "min_version": "0.9.1" + "min_version": "0.10.1" }, "homepage_url": "https://github.com/MaiM-with-u/maibot", "repository_url": "https://github.com/MaiM-with-u/maibot", diff --git a/src/plugins/built_in/relation/relation.py b/src/plugins/built_in/relation/relation.py index bab9090d..1f6f0d0f 100644 --- a/src/plugins/built_in/relation/relation.py +++ b/src/plugins/built_in/relation/relation.py @@ -1,6 +1,7 @@ import json from json_repair import repair_json from typing import Tuple +import time from src.common.logger import get_logger from src.config.config import global_config @@ -79,16 +80,6 @@ class BuildRelationAction(BaseAction): action_name = "build_relation" action_description = "了解对于某人的记忆,并添加到你对对方的印象中" - # LLM判断提示词 - llm_judge_prompt = """ - 判定是否需要使用关系动作,添加对于某人的记忆: - 1. 对方与你的交互让你对其有新记忆 - 2. 对方有提到其个人信息,包括喜好,身份,等等 - 3. 对方希望你记住对方的信息 - - 请回答"是"或"否"。 - """ - # 动作参数定义 action_parameters = {"person_name": "需要了解或记忆的人的名称", "impression": "需要了解的对某人的记忆或印象"} @@ -109,13 +100,17 @@ class BuildRelationAction(BaseAction): try: # 1. 获取构建关系的原因 impression = self.action_data.get("impression", "") - logger.info(f"{self.log_prefix} 添加记忆原因: {self.reasoning}") + logger.info(f"{self.log_prefix} 添加关系印象原因: {self.reasoning}") person_name = self.action_data.get("person_name", "") # 2. 获取目标用户信息 person = Person(person_name=person_name) if not person.is_known: logger.warning(f"{self.log_prefix} 用户 {person_name} 不存在,跳过添加记忆") return False, f"用户 {person_name} 不存在,跳过添加记忆" + + person.last_know = time.time() + person.know_times += 1 + person.sync_to_database() category_list = person.get_all_category() if not category_list: @@ -195,6 +190,8 @@ class BuildRelationAction(BaseAction): # 新记忆 person.memory_points.append(f"{category}:{new_memory}:1.0") person.sync_to_database() + + logger.info(f"{self.log_prefix} 为{person.person_name}新增记忆点: {new_memory}") return True, f"为{person.person_name}新增记忆点: {new_memory}" elif memory_id and integrate_memory: @@ -204,12 +201,14 @@ class BuildRelationAction(BaseAction): del_count = person.del_memory(category, memory_content) if del_count > 0: - logger.info(f"{self.log_prefix} 删除记忆点: {memory_content}") + # logger.info(f"{self.log_prefix} 删除记忆点: {memory_content}") memory_weight = get_weight_from_memory(memory) person.memory_points.append(f"{category}:{integrate_memory}:{memory_weight + 1.0}") person.sync_to_database() + logger.info(f"{self.log_prefix} 更新{person.person_name}的记忆点: {memory_content} -> {integrate_memory}") + return True, f"更新{person.person_name}的记忆点: {memory_content} -> {integrate_memory}" else: diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 97eb5b4a..3805051c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.1" +version = "6.7.2" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -65,8 +65,6 @@ focus_value = 0.5 max_context_size = 20 # 上下文长度 -interest_rate_mode = "fast" #激活值计算模式,可选fast或者accurate - planner_size = 2.5 # 副规划器大小,越小,麦麦的动作执行能力越精细,但是消耗更多token,调大可以缓解429类错误 mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复 @@ -102,22 +100,8 @@ talk_frequency_adjust = [ # - 后续元素是"时间,频率"格式,表示从该时间开始使用该活跃度,直到下一个时间点 # - 优先级:特定聊天流配置 > 全局配置 > 默认 talk_frequency - [relationship] enable_relationship = true # 是否启用关系系统 -relation_frequency = 1 # 关系频率,麦麦构建关系的频率 - -[message_receive] -# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息 -ban_words = [ - # "403","张三" - ] - -ban_msgs_regex = [ - # 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改 - #"https?://[^\\s]+", # 匹配https链接 - #"\\d{4}-\\d{2}-\\d{2}", # 匹配日期 -] [tool] enable_tool = false # 是否在普通聊天中启用工具 @@ -138,21 +122,30 @@ filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合 [memory] enable_memory = true # 是否启用记忆系统 -memory_build_frequency = 1 # 记忆构建频率 越高,麦麦学习越多 -memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 -forget_memory_interval = 3000 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 +forget_memory_interval = 1500 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 memory_forget_time = 48 #多长时间后的记忆会被遗忘 单位小时 memory_forget_percentage = 0.008 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 -enable_instant_memory = false # 是否启用即时记忆,测试功能,可能存在未知问题 - #不希望记忆的词,已经记忆的不会受到影响,需要手动清理 memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ] [voice] enable_asr = false # 是否启用语音识别,启用后麦麦可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s +[message_receive] +# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息 +ban_words = [ + # "403","张三" + ] + +ban_msgs_regex = [ + # 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改 + #"https?://[^\\s]+", # 匹配https链接 + #"\\d{4}-\\d{2}-\\d{2}", # 匹配日期 +] + + [lpmm_knowledge] # lpmm知识库配置 enable = false # 是否启用lpmm知识库 rag_synonym_search_top_k = 10 # 同义词搜索TopK From c384276afc1cbcc918a3ae9f8e87d7e3ffa527a3 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Wed, 27 Aug 2025 22:18:37 +0800 Subject: [PATCH 11/18] Update config.py --- src/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.py b/src/config/config.py index bb12b1d3..80e275f8 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -56,7 +56,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.10.1" +MMC_VERSION = "0.10.2-snapshot.1" def get_key_comment(toml_table, key): From 82e5a710c315e7bd11f053d0923c567395a1466c Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Thu, 28 Aug 2025 23:44:14 +0800 Subject: [PATCH 12/18] =?UTF-8?q?action=E7=9A=84reply=5Fmessage=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E4=B8=BA=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B=EF=BC=8C?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4typing=E4=BB=A5=E5=8F=8A=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/planner_actions/action_manager.py | 2 +- src/chat/utils/utils_image.py | 3 +-- src/llm_models/utils_model.py | 23 +++++++++++----------- src/person_info/person_info.py | 2 +- src/plugin_system/__init__.py | 17 ++++++++++++++++ src/plugin_system/base/base_action.py | 13 +++++------- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index b4587474..1de033bf 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -84,7 +84,7 @@ class ActionManager: log_prefix=log_prefix, shutting_down=shutting_down, plugin_config=plugin_config, - action_message=action_message.flatten() if action_message else None, + action_message=action_message, ) logger.debug(f"创建Action实例成功: {action_name}") diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index 2bec09be..3c9c51e9 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -4,7 +4,6 @@ import time import hashlib import uuid import io -import asyncio import numpy as np from typing import Optional, Tuple @@ -177,7 +176,7 @@ class ImageManager: emotion_prompt, temperature=0.3, max_tokens=50 ) - if emotion_result is None: + if not emotion_result: logger.warning("LLM未能生成情感标签,使用详细描述的前几个词") # 降级处理:从详细描述中提取关键词 import jieba diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 7ab76969..529c52b0 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -156,19 +156,19 @@ class LLMRequest: """ # 请求体构建 start_time = time.time() - + message_builder = MessageBuilder() message_builder.add_text_content(prompt) messages = [message_builder.build()] - + tool_built = self._build_tool_options(tools) - + # 模型选择 model_info, api_provider, client = self._select_model() - + # 请求并处理返回值 logger.debug(f"LLM选择耗时: {model_info.name} {time.time() - start_time}") - + response = await self._execute_request( api_provider=api_provider, client=client, @@ -179,8 +179,7 @@ class LLMRequest: max_tokens=max_tokens, tool_options=tool_built, ) - - + content = response.content reasoning_content = response.reasoning_content or "" tool_calls = response.tool_calls @@ -188,7 +187,7 @@ class LLMRequest: if not reasoning_content and content: content, extracted_reasoning = self._extract_reasoning(content) reasoning_content = extracted_reasoning - + if usage := response.usage: llm_usage_recorder.record_usage_to_database( model_info=model_info, @@ -199,7 +198,7 @@ class LLMRequest: time_cost=time.time() - start_time, ) - return content, (reasoning_content, model_info.name, tool_calls) + return content or "", (reasoning_content, model_info.name, tool_calls) async def get_embedding(self, embedding_input: str) -> Tuple[List[float], str]: """获取嵌入向量 @@ -248,11 +247,11 @@ class LLMRequest: ) model_info = model_config.get_model_info(least_used_model_name) api_provider = model_config.get_provider(model_info.api_provider) - + # 对于嵌入任务,强制创建新的客户端实例以避免事件循环问题 - force_new_client = (self.request_type == "embedding") + force_new_client = self.request_type == "embedding" client = client_registry.get_client_class_instance(api_provider, force_new=force_new_client) - + logger.debug(f"选择请求模型: {model_info.name}") total_tokens, penalty, usage_penalty = self.model_usage[model_info.name] self.model_usage[model_info.name] = (total_tokens, penalty, usage_penalty + 1) # 增加使用惩罚值防止连续使用 diff --git a/src/person_info/person_info.py b/src/person_info/person_info.py index 3b4c1af6..584af8b8 100644 --- a/src/person_info/person_info.py +++ b/src/person_info/person_info.py @@ -241,7 +241,7 @@ class Person: self.name_reason: Optional[str] = None self.know_times = 0 self.know_since = None - self.last_know = None + self.last_know: Optional[float] = None self.memory_points = [] # 初始化性格特征相关字段 diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 45b8de9b..535b25d4 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -53,6 +53,15 @@ from .apis import ( get_logger, ) +from src.common.data_models.database_data_model import ( + DatabaseMessages, + DatabaseUserInfo, + DatabaseGroupInfo, + DatabaseChatInfo, +) +from src.common.data_models.info_data_model import TargetPersonInfo, ActionPlannerInfo +from src.common.data_models.llm_data_model import LLMGenerationDataModel + __version__ = "2.0.0" @@ -103,4 +112,12 @@ __all__ = [ # "ManifestGenerator", # "validate_plugin_manifest", # "generate_plugin_manifest", + # 数据模型 + "DatabaseMessages", + "DatabaseUserInfo", + "DatabaseGroupInfo", + "DatabaseChatInfo", + "TargetPersonInfo", + "ActionPlannerInfo", + "LLMGenerationDataModel" ] diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index cd686edb..b6882d85 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -39,7 +39,7 @@ class BaseAction(ABC): chat_stream: ChatStream, log_prefix: str = "", plugin_config: Optional[dict] = None, - action_message: Optional[dict] = None, + action_message: Optional["DatabaseMessages"] = None, **kwargs, ): # sourcery skip: hoist-similar-statement-from-if, merge-else-if-into-elif, move-assign-in-block, swap-if-else-branches, swap-nested-ifs @@ -114,16 +114,13 @@ class BaseAction(ABC): if self.action_message: self.has_action_message = True - else: - self.action_message = {} - if self.has_action_message: if self.action_name != "no_action": - self.group_id = str(self.action_message.get("chat_info_group_id", None)) - self.group_name = self.action_message.get("chat_info_group_name", None) + self.group_id = str(self.action_message.chat_info.group_info.group_id if self.action_message.chat_info.group_info else None) + self.group_name = self.action_message.chat_info.group_info.group_name if self.action_message.chat_info.group_info else None - self.user_id = str(self.action_message.get("user_id", None)) - self.user_nickname = self.action_message.get("user_nickname", None) + self.user_id = str(self.action_message.user_info.user_id) + self.user_nickname = self.action_message.user_info.user_nickname if self.group_id: self.is_group = True self.target_id = self.group_id From 4bee6002ffb7bb0e42856642235f46607cd8156b Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Thu, 28 Aug 2025 23:46:55 +0800 Subject: [PATCH 13/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dstr(None)=E5=BC=95?= =?UTF-8?q?=E5=8F=91=E7=9A=84=E9=94=99=E8=AF=AF=E7=90=86=E8=A7=A3=E4=B8=8E?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/base/base_action.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index b6882d85..0e58885b 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -76,15 +76,19 @@ class BaseAction(ABC): self.action_require: list[str] = getattr(self.__class__, "action_require", []).copy() # 设置激活类型实例属性(从类属性复制,提供默认值) - self.focus_activation_type = getattr(self.__class__, "focus_activation_type", ActionActivationType.ALWAYS) #已弃用 + self.focus_activation_type = getattr( + self.__class__, "focus_activation_type", ActionActivationType.ALWAYS + ) # 已弃用 """FOCUS模式下的激活类型""" - self.normal_activation_type = getattr(self.__class__, "normal_activation_type", ActionActivationType.ALWAYS) #已弃用 + self.normal_activation_type = getattr( + self.__class__, "normal_activation_type", ActionActivationType.ALWAYS + ) # 已弃用 """NORMAL模式下的激活类型""" self.activation_type = getattr(self.__class__, "activation_type", self.focus_activation_type) """激活类型""" self.random_activation_probability: float = getattr(self.__class__, "random_activation_probability", 0.0) """当激活类型为RANDOM时的概率""" - self.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "") #已弃用 + self.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "") # 已弃用 """协助LLM进行判断的Prompt""" self.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy() """激活类型为KEYWORD时的KEYWORDS列表""" @@ -116,8 +120,16 @@ class BaseAction(ABC): self.has_action_message = True if self.action_name != "no_action": - self.group_id = str(self.action_message.chat_info.group_info.group_id if self.action_message.chat_info.group_info else None) - self.group_name = self.action_message.chat_info.group_info.group_name if self.action_message.chat_info.group_info else None + self.group_id = ( + str(self.action_message.chat_info.group_info.group_id) + if self.action_message.chat_info.group_info + else None + ) + self.group_name = ( + self.action_message.chat_info.group_info.group_name + if self.action_message.chat_info.group_info + else None + ) self.user_id = str(self.action_message.user_info.user_id) self.user_nickname = self.action_message.user_info.user_nickname From aa1155cc5b81d98ddcee8af855cad650ffb8a7b0 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sun, 31 Aug 2025 12:30:11 +0800 Subject: [PATCH 14/18] =?UTF-8?q?=E8=A7=A3=E5=86=B3UnboundLocalError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/memory_system/Hippocampus.py | 1 + src/chat/message_receive/message.py | 42 ++++++++++---------- src/chat/planner_actions/planner.py | 7 +--- src/chat/replyer/default_generator.py | 22 +++++----- src/chat/utils/chat_message_builder.py | 33 ++++++++------- src/llm_models/model_client/gemini_client.py | 2 + src/main.py | 19 +++------ src/plugin_system/apis/send_api.py | 5 +-- src/plugin_system/core/component_registry.py | 1 + 9 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index bb8d6c5c..82901a91 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -1117,6 +1117,7 @@ class ParahippocampalGyrus: # 4. 创建所有话题的摘要生成任务 tasks: List[Tuple[str, Coroutine[Any, Any, Tuple[str, Tuple[str, str, List | None]]]]] = [] + topic_what_prompt: str = "" for topic in filtered_topics: # 调用修改后的 topic_what,不再需要 time_info topic_what_prompt = self.hippocampus.topic_what(input_text, topic) diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 66a1c029..b8167844 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -84,7 +84,7 @@ class Message(MessageBase): return await self._process_single_segment(segment) # type: ignore @abstractmethod - async def _process_single_segment(self, segment): + async def _process_single_segment(self, segment) -> str: pass @@ -353,44 +353,44 @@ class MessageProcessBase(Message): self.thinking_time = round(time.time() - self.thinking_start_time, 2) return self.thinking_time - async def _process_single_segment(self, seg: Seg) -> str | None: + async def _process_single_segment(self, segment: Seg) -> str: """处理单个消息段 Args: - seg: 要处理的消息段 + segment: 要处理的消息段 Returns: str: 处理后的文本 """ try: - if seg.type == "text": - return seg.data # type: ignore - elif seg.type == "image": + if segment.type == "text": + return segment.data # type: ignore + elif segment.type == "image": # 如果是base64图片数据 - if isinstance(seg.data, str): - return await get_image_manager().get_image_description(seg.data) + if isinstance(segment.data, str): + return await get_image_manager().get_image_description(segment.data) return "[图片,网卡了加载不出来]" - elif seg.type == "emoji": - if isinstance(seg.data, str): - return await get_image_manager().get_emoji_tag(seg.data) + elif segment.type == "emoji": + if isinstance(segment.data, str): + return await get_image_manager().get_emoji_tag(segment.data) return "[表情,网卡了加载不出来]" - elif seg.type == "voice": - if isinstance(seg.data, str): - return await get_voice_text(seg.data) + elif segment.type == "voice": + if isinstance(segment.data, str): + return await get_voice_text(segment.data) return "[发了一段语音,网卡了加载不出来]" - elif seg.type == "at": - return f"[@{seg.data}]" - elif seg.type == "reply": + elif segment.type == "at": + return f"[@{segment.data}]" + elif segment.type == "reply": if self.reply and hasattr(self.reply, "processed_plain_text"): # print(f"self.reply.processed_plain_text: {self.reply.processed_plain_text}") # print(f"reply: {self.reply}") return f"[回复<{self.reply.message_info.user_info.user_nickname}:{self.reply.message_info.user_info.user_id}> 的消息:{self.reply.processed_plain_text}]" # type: ignore - return None + return "" else: - return f"[{seg.type}:{str(seg.data)}]" + return f"[{segment.type}:{str(segment.data)}]" except Exception as e: - logger.error(f"处理消息段失败: {str(e)}, 类型: {seg.type}, 数据: {seg.data}") - return f"[处理失败的{seg.type}消息]" + logger.error(f"处理消息段失败: {str(e)}, 类型: {segment.type}, 数据: {segment.data}") + return f"[处理失败的{segment.type}消息]" def _generate_detailed_text(self) -> str: """生成详细文本,包含时间和用户信息""" diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 61bc0675..b5d00e87 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -510,7 +510,7 @@ class ActionPlanner: ) self.last_obs_time_mark = time.time() - + all_sub_planner_results: List[ActionPlannerInfo] = [] # 防止Unbound try: sub_planner_actions: Dict[str, ActionInfo] = {} @@ -581,7 +581,6 @@ class ActionPlanner: sub_plan_results = await asyncio.gather(*sub_plan_tasks) # 收集所有结果 - all_sub_planner_results: List[ActionPlannerInfo] = [] for sub_result in sub_plan_results: all_sub_planner_results.extend(sub_result) @@ -726,9 +725,7 @@ class ActionPlanner: action_str = "" for action_planner_info in actions: action_str += f"{action_planner_info.action_type} " - logger.info( - f"{self.log_prefix}大脑小脑决定执行{len(actions)}个动作: {action_str}" - ) + logger.info(f"{self.log_prefix}大脑小脑决定执行{len(actions)}个动作: {action_str}") else: # 如果为假,只返回副规划器的结果 actions = self._filter_no_actions(all_sub_planner_results) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 51477d89..410fe17c 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -651,9 +651,11 @@ class DefaultReplyer: action_name = action_plan_info.action_type if action_name == "reply": continue + action_description: str = "无描述" + reasoning: str = "无原因" if action := available_actions.get(action_name): - action_description = action.description or "无描述" - reasoning = action_plan_info.reasoning or "无原因" + action_description = action.description or action_description + reasoning = action_plan_info.reasoning or reasoning chosen_action_descriptions += f"- {action_name}: {action_description},原因:{reasoning}\n" @@ -705,22 +707,22 @@ class DefaultReplyer: is_group_chat = bool(chat_stream.group_info) platform = chat_stream.platform + user_id = "用户ID" + person_name = "用户" + sender = "用户" + target = "消息" + if reply_message: user_id = reply_message.user_info.user_id person = Person(platform=platform, user_id=user_id) person_name = person.person_name or user_id sender = person_name target = reply_message.processed_plain_text - else: - person_name = "用户" - sender = "用户" - target = "消息" + mood_prompt: str = "" if global_config.mood.enable_mood: chat_mood = mood_manager.get_mood_by_chat_id(chat_id) mood_prompt = chat_mood.mood_state - else: - mood_prompt = "" target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) @@ -939,9 +941,7 @@ class DefaultReplyer: else: chat_target_name = "对方" if self.chat_target_info: - chat_target_name = ( - self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" - ) + chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" chat_target_1 = await global_prompt_manager.format_prompt( "chat_target_private1", sender_name=chat_target_name ) diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index 2dbb19a1..1bd72c85 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -203,18 +203,21 @@ def get_actions_by_timestamp_with_chat( query = query.order_by(ActionRecords.time.asc()) actions = list(query) - return [DatabaseActionRecords( - action_id=action.action_id, - time=action.time, - action_name=action.action_name, - action_data=action.action_data, - action_done=action.action_done, - action_build_into_prompt=action.action_build_into_prompt, - action_prompt_display=action.action_prompt_display, - chat_id=action.chat_id, - chat_info_stream_id=action.chat_info_stream_id, - chat_info_platform=action.chat_info_platform, - ) for action in actions] + return [ + DatabaseActionRecords( + action_id=action.action_id, + time=action.time, + action_name=action.action_name, + action_data=action.action_data, + action_done=action.action_done, + action_build_into_prompt=action.action_build_into_prompt, + action_prompt_display=action.action_prompt_display, + chat_id=action.chat_id, + chat_info_stream_id=action.chat_info_stream_id, + chat_info_platform=action.chat_info_platform, + ) + for action in actions + ] def get_actions_by_timestamp_with_chat_inclusive( @@ -474,7 +477,7 @@ def _build_readable_messages_internal( truncated_content = content if 0 < limit < original_len: - truncated_content = f"{content[:limit]}{replace_content}" + truncated_content = f"{content[:limit]}{replace_content}" # pyright: ignore[reportPossiblyUnboundVariable] detailed_message.append((timestamp, name, truncated_content, is_action)) else: @@ -544,7 +547,7 @@ def build_pic_mapping_info(pic_id_mapping: Dict[str, str]) -> str: return "\n".join(mapping_lines) -def build_readable_actions(actions: List[DatabaseActionRecords],mode:str="relative") -> str: +def build_readable_actions(actions: List[DatabaseActionRecords], mode: str = "relative") -> str: """ 将动作列表转换为可读的文本格式。 格式: 在()分钟前,你使用了(action_name),具体内容是:(action_prompt_display) @@ -585,6 +588,8 @@ def build_readable_actions(actions: List[DatabaseActionRecords],mode:str="relati action_time_struct = time.localtime(action_time) time_str = time.strftime("%H:%M:%S", action_time_struct) time_ago_str = f"在{time_str}" + else: + raise ValueError(f"Unsupported mode: {mode}") line = f"{time_ago_str},你使用了“{action_name}”,具体内容是:“{action_prompt_display}”" output_lines.append(line) diff --git a/src/llm_models/model_client/gemini_client.py b/src/llm_models/model_client/gemini_client.py index 67f9a300..e58466d1 100644 --- a/src/llm_models/model_client/gemini_client.py +++ b/src/llm_models/model_client/gemini_client.py @@ -86,6 +86,8 @@ def _convert_messages( role = "model" elif message.role == RoleType.User: role = "user" + else: + raise ValueError(f"Unsupported role: {message.role}") # 添加Content if isinstance(message.content, str): diff --git a/src/main.py b/src/main.py index bc90e056..7e14ff7f 100644 --- a/src/main.py +++ b/src/main.py @@ -37,10 +37,9 @@ logger = get_logger("main") class MainSystem: def __init__(self): # 根据配置条件性地初始化记忆系统 + self.hippocampus_manager = None if global_config.memory.enable_memory: self.hippocampus_manager = hippocampus_manager - else: - self.hippocampus_manager = None # 使用消息API替代直接的FastAPI实例 self.app: MessageServer = get_global_api() @@ -81,12 +80,12 @@ class MainSystem: # 启动API服务器 # start_api_server() # logger.info("API服务器启动成功") - + # 启动LPMM lpmm_start_up() # 加载所有actions,包括默认的和插件的 - plugin_manager.load_all_plugins() + plugin_manager.load_all_plugins() # 初始化表情管理器 get_emoji_manager().initialize() @@ -114,16 +113,14 @@ class MainSystem: # 将bot.py中的chat_bot.message_process消息处理函数注册到api.py的消息处理基类中 self.app.register_message_handler(chat_bot.message_process) - + await check_and_run_migrations() - # 触发 ON_START 事件 from src.plugin_system.core.events_manager import events_manager from src.plugin_system.base.component_types import EventType - await events_manager.handle_mai_events( - event_type=EventType.ON_START - ) + + await events_manager.handle_mai_events(event_type=EventType.ON_START) # logger.info("已触发 ON_START 事件") try: init_time = int(1000 * (time.time() - init_start_time)) @@ -162,8 +159,6 @@ class MainSystem: logger.info("[记忆遗忘] 记忆遗忘完成") - - async def main(): """主函数""" system = MainSystem() @@ -175,5 +170,3 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) - - \ No newline at end of file diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 4bdab41e..21f764cd 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -99,6 +99,8 @@ async def _send_to_target( # 创建消息段 message_segment = Seg(type=message_type, data=content) # type: ignore + reply_to_platform_id = "" + anchor_message: Union["MessageRecv", None] = None if reply_message: anchor_message = message_dict_to_message_recv(reply_message.flatten()) if anchor_message: @@ -107,9 +109,6 @@ async def _send_to_target( reply_to_platform_id = ( f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" ) - else: - reply_to_platform_id = "" - anchor_message = None # 构建发送消息对象 bot_message = MessageSending( diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 59a03b73..19fda27e 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -124,6 +124,7 @@ class ComponentRegistry: self._components_classes[namespaced_name] = component_class # 根据组件类型进行特定注册(使用原始名称) + ret = False match component_type: case ComponentType.ACTION: assert isinstance(component_info, ActionInfo) From a11e65f794bbc183045c55481ea89ebf1c69d669 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 31 Aug 2025 12:35:01 +0800 Subject: [PATCH 15/18] =?UTF-8?q?feat:=E5=A4=A7=E5=B9=85=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=B5=81=E6=8E=A7=E5=88=B6=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E7=B2=BE=E5=87=86=E7=AE=80=E6=B4=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 + changelogs/changelog.md | 8 +- .../frequency_control/focus_value_control.py | 23 +- .../frequency_control/frequency_control.py | 275 +++++++++++++++ .../talk_frequency_control.py | 22 +- src/chat/heart_flow/heartFC_chat.py | 313 +++++++----------- .../heart_flow/heartflow_message_processor.py | 18 +- src/chat/message_receive/message.py | 2 + src/chat/message_receive/storage.py | 6 + src/chat/planner_actions/planner.py | 16 +- src/chat/utils/utils.py | 30 +- src/common/data_models/database_data_model.py | 7 + src/common/database/database_model.py | 3 +- src/common/logger.py | 8 +- src/config/config.py | 2 +- src/config/official_configs.py | 8 +- src/plugin_system/apis/frequency_api.py | 15 +- template/bot_config_template.toml | 10 +- 18 files changed, 458 insertions(+), 313 deletions(-) create mode 100644 src/chat/frequency_control/frequency_control.py diff --git a/README.md b/README.md index 9b057508..403b42e0 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ > - QQ 机器人存在被限制风险,请自行了解,谨慎使用。 > - 由于程序处于开发中,可能消耗较多 token。 +## 麦麦MC项目(早期开发) +[让麦麦玩MC](https://github.com/MaiM-with-u/Maicraft) + +交流群:1058573197 + ## 💬 讨论 **技术交流群:** diff --git a/changelogs/changelog.md b/changelogs/changelog.md index da96cb72..a26088c7 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,15 +1,15 @@ # Changelog -TODO:回复频率动态控制 - -## [0.10.2] - 2025-8-24 +## [0.10.2] - 2025-8-31 ### 🌟 主要功能更改 +- 大幅优化了聊天逻辑,更易配置 - 记忆系统重新启用,更好更优秀 - 更好的event系统 -- 为空回复添加重试机制 +- 现在支持提及100%回复 ### 细节功能更改 +- 为空回复添加重试机制 - 修复tts插件可能的复读问题 diff --git a/src/chat/frequency_control/focus_value_control.py b/src/chat/frequency_control/focus_value_control.py index 290dcc9e..be820760 100644 --- a/src/chat/frequency_control/focus_value_control.py +++ b/src/chat/frequency_control/focus_value_control.py @@ -3,26 +3,7 @@ from src.config.config import global_config from src.chat.frequency_control.utils import parse_stream_config_to_chat_id -class FocusValueControl: - def __init__(self, chat_id: str): - self.chat_id = chat_id - self.focus_value_adjust: float = 1 - - def get_current_focus_value(self) -> float: - return get_current_focus_value(self.chat_id) * self.focus_value_adjust - - -class FocusValueControlManager: - def __init__(self): - self.focus_value_controls: dict[str, FocusValueControl] = {} - - def get_focus_value_control(self, chat_id: str) -> FocusValueControl: - if chat_id not in self.focus_value_controls: - self.focus_value_controls[chat_id] = FocusValueControl(chat_id) - return self.focus_value_controls[chat_id] - - -def get_current_focus_value(chat_id: Optional[str] = None) -> float: +def get_config_base_focus_value(chat_id: Optional[str] = None) -> float: """ 根据当前时间和聊天流获取对应的 focus_value """ @@ -139,5 +120,3 @@ def get_global_focus_value() -> Optional[float]: return None - -focus_value_control = FocusValueControlManager() diff --git a/src/chat/frequency_control/frequency_control.py b/src/chat/frequency_control/frequency_control.py new file mode 100644 index 00000000..63fb0e33 --- /dev/null +++ b/src/chat/frequency_control/frequency_control.py @@ -0,0 +1,275 @@ +import time +from typing import Optional, Dict, List +from src.plugin_system.apis import message_api +from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager +from src.common.logger import get_logger +from src.config.config import global_config +from src.chat.frequency_control.talk_frequency_control import get_config_base_talk_frequency +from src.chat.frequency_control.focus_value_control import get_config_base_focus_value + +logger = get_logger("frequency_control") + + +class FrequencyControl: + """ + 频率控制类,可以根据最近时间段的发言数量和发言人数动态调整频率 + """ + + def __init__(self, chat_id: str): + self.chat_id = chat_id + self.chat_stream: ChatStream = get_chat_manager().get_stream(self.chat_id) + if not self.chat_stream: + raise ValueError(f"无法找到聊天流: {self.chat_id}") + self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]" + # 发言频率调整值 + self.talk_frequency_adjust: float = 1.0 + self.talk_frequency_external_adjust: float = 1.0 + # 专注度调整值 + self.focus_value_adjust: float = 1.0 + self.focus_value_external_adjust: float = 1.0 + + # 动态调整相关参数 + self.last_update_time = time.time() + self.update_interval = 60 # 每60秒更新一次 + + # 历史数据缓存 + self._message_count_cache = 0 + self._user_count_cache = 0 + self._last_cache_time = 0 + self._cache_duration = 30 # 缓存30秒 + + # 调整参数 + self.min_adjust = 0.3 # 最小调整值 + self.max_adjust = 2.0 # 最大调整值 + + # 基准值(可根据实际情况调整) + self.base_message_count = 5 # 基准消息数量 + self.base_user_count = 3 # 基准用户数量 + + # 平滑因子 + self.smoothing_factor = 0.3 + + + def get_dynamic_talk_frequency_adjust(self) -> float: + """ + 获取纯动态调整值(不包含配置文件基础值) + + Returns: + float: 动态调整值 + """ + self._update_talk_frequency_adjust() + return self.talk_frequency_adjust + + def get_dynamic_focus_value_adjust(self) -> float: + """ + 获取纯动态调整值(不包含配置文件基础值) + + Returns: + float: 动态调整值 + """ + self._update_focus_value_adjust() + return self.focus_value_adjust + + def _update_talk_frequency_adjust(self): + """ + 更新发言频率调整值 + 适合人少话多的时候:人少但消息多,提高回复频率 + """ + current_time = time.time() + + # 检查是否需要更新 + if current_time - self.last_update_time < self.update_interval: + return + + try: + # 获取最近30分钟的数据(发言频率更敏感) + recent_messages = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_stream.stream_id, + start_time=current_time - 1800, # 30分钟前 + end_time=current_time, + filter_mai=True, + filter_command=True + ) + + # 计算消息数量和用户数量 + message_count = len(recent_messages) + user_ids = set() + for msg in recent_messages: + if msg.user_info and msg.user_info.user_id: + user_ids.add(msg.user_info.user_id) + user_count = len(user_ids) + + # 发言频率调整逻辑:人少话多时提高回复频率 + if user_count > 0: + # 计算人均消息数 + messages_per_user = message_count / user_count + # 基准人均消息数 + base_messages_per_user = self.base_message_count / self.base_user_count if self.base_user_count > 0 else 1.0 + + # 如果人均消息数高,说明活跃度高,提高回复频率 + if messages_per_user > base_messages_per_user: + # 人少话多:提高回复频率 + target_talk_adjust = min(self.max_adjust, messages_per_user / base_messages_per_user) + else: + # 活跃度一般:保持正常 + target_talk_adjust = 1.0 + else: + target_talk_adjust = 1.0 + + # 限制调整范围 + target_talk_adjust = max(self.min_adjust, min(self.max_adjust, target_talk_adjust)) + + # 平滑调整 + self.talk_frequency_adjust = ( + self.talk_frequency_adjust * (1 - self.smoothing_factor) + + target_talk_adjust * self.smoothing_factor + ) + + logger.info( + f"{self.log_prefix} 发言频率调整更新: " + f"消息数={message_count}, 用户数={user_count}, " + f"人均消息数={message_count/user_count if user_count > 0 else 0:.2f}, " + f"调整值={self.talk_frequency_adjust:.2f}" + ) + + except Exception as e: + logger.error(f"{self.log_prefix} 更新发言频率调整值时出错: {e}") + + def _update_focus_value_adjust(self): + """ + 更新专注度调整值 + 适合人多话多的时候:人多且消息多,提高专注度(LLM消耗更多,但回复更精准) + """ + current_time = time.time() + + # 检查是否需要更新 + if current_time - self.last_update_time < self.update_interval: + return + + try: + # 获取最近1小时的数据 + recent_messages = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_stream.stream_id, + start_time=current_time - 3600, # 1小时前 + end_time=current_time, + filter_mai=True, + filter_command=True + ) + + # 计算消息数量和用户数量 + message_count = len(recent_messages) + user_ids = set() + for msg in recent_messages: + if msg.user_info and msg.user_info.user_id: + user_ids.add(msg.user_info.user_id) + user_count = len(user_ids) + + # 专注度调整逻辑:人多话多时提高专注度 + if user_count > 0 and self.base_user_count > 0: + # 计算用户活跃度比率 + user_ratio = user_count / self.base_user_count + # 计算消息活跃度比率 + message_ratio = message_count / self.base_message_count if self.base_message_count > 0 else 1.0 + + # 如果用户多且消息多,提高专注度 + if user_ratio > 1.2 and message_ratio > 1.2: + # 人多话多:提高专注度,消耗更多LLM资源但回复更精准 + target_focus_adjust = min(self.max_adjust, (user_ratio + message_ratio) / 2) + elif user_ratio > 1.5: + # 用户特别多:适度提高专注度 + target_focus_adjust = min(self.max_adjust, 1.0 + (user_ratio - 1.0) * 0.3) + else: + # 正常情况:保持默认专注度 + target_focus_adjust = 1.0 + else: + target_focus_adjust = 1.0 + + # 限制调整范围 + target_focus_adjust = max(self.min_adjust, min(self.max_adjust, target_focus_adjust)) + + # 平滑调整 + self.focus_value_adjust = ( + self.focus_value_adjust * (1 - self.smoothing_factor) + + target_focus_adjust * self.smoothing_factor + ) + + logger.info( + f"{self.log_prefix} 专注度调整更新: " + f"消息数={message_count}, 用户数={user_count}, " + f"用户比率={user_count/self.base_user_count if self.base_user_count > 0 else 0:.2f}, " + f"消息比率={message_count/self.base_message_count if self.base_message_count > 0 else 0:.2f}, " + f"调整值={self.focus_value_adjust:.2f}" + ) + + except Exception as e: + logger.error(f"{self.log_prefix} 更新专注度调整值时出错: {e}") + + def get_final_talk_frequency(self) -> float: + return get_config_base_talk_frequency(self.chat_stream.stream_id) * self.get_dynamic_talk_frequency_adjust() * self.talk_frequency_external_adjust + + def get_final_focus_value(self) -> float: + return get_config_base_focus_value(self.chat_stream.stream_id) * self.get_dynamic_focus_value_adjust() * self.focus_value_external_adjust + + + def set_adjustment_parameters( + self, + min_adjust: Optional[float] = None, + max_adjust: Optional[float] = None, + base_message_count: Optional[int] = None, + base_user_count: Optional[int] = None, + smoothing_factor: Optional[float] = None, + update_interval: Optional[int] = None + ): + """ + 设置调整参数 + + Args: + min_adjust: 最小调整值 + max_adjust: 最大调整值 + base_message_count: 基准消息数量 + base_user_count: 基准用户数量 + smoothing_factor: 平滑因子 + update_interval: 更新间隔(秒) + """ + if min_adjust is not None: + self.min_adjust = max(0.1, min_adjust) + if max_adjust is not None: + self.max_adjust = max(1.0, max_adjust) + if base_message_count is not None: + self.base_message_count = max(1, base_message_count) + if base_user_count is not None: + self.base_user_count = max(1, base_user_count) + if smoothing_factor is not None: + self.smoothing_factor = max(0.0, min(1.0, smoothing_factor)) + if update_interval is not None: + self.update_interval = max(10, update_interval) + + +class FrequencyControlManager: + """ + 频率控制管理器,管理多个聊天流的频率控制实例 + """ + + def __init__(self): + self.frequency_control_dict: Dict[str, FrequencyControl] = {} + + def get_or_create_frequency_control(self, chat_id: str) -> FrequencyControl: + """ + 获取或创建指定聊天流的频率控制实例 + + Args: + chat_id: 聊天流ID + + Returns: + FrequencyControl: 频率控制实例 + """ + if chat_id not in self.frequency_control_dict: + self.frequency_control_dict[chat_id] = FrequencyControl(chat_id) + return self.frequency_control_dict[chat_id] + +# 创建全局实例 +frequency_control_manager = FrequencyControlManager() + + + + diff --git a/src/chat/frequency_control/talk_frequency_control.py b/src/chat/frequency_control/talk_frequency_control.py index ad81fbd8..11728e26 100644 --- a/src/chat/frequency_control/talk_frequency_control.py +++ b/src/chat/frequency_control/talk_frequency_control.py @@ -3,26 +3,7 @@ from src.config.config import global_config from src.chat.frequency_control.utils import parse_stream_config_to_chat_id -class TalkFrequencyControl: - def __init__(self, chat_id: str): - self.chat_id = chat_id - self.talk_frequency_adjust: float = 1 - - def get_current_talk_frequency(self) -> float: - return get_current_talk_frequency(self.chat_id) * self.talk_frequency_adjust - - -class TalkFrequencyControlManager: - def __init__(self): - self.talk_frequency_controls = {} - - def get_talk_frequency_control(self, chat_id: str) -> TalkFrequencyControl: - if chat_id not in self.talk_frequency_controls: - self.talk_frequency_controls[chat_id] = TalkFrequencyControl(chat_id) - return self.talk_frequency_controls[chat_id] - - -def get_current_talk_frequency(chat_id: Optional[str] = None) -> float: +def get_config_base_talk_frequency(chat_id: Optional[str] = None) -> float: """ 根据当前时间和聊天流获取对应的 talk_frequency @@ -145,4 +126,3 @@ def get_global_frequency() -> Optional[float]: return None -talk_frequency_control = TalkFrequencyControlManager() diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 18aec433..33b73427 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -18,8 +18,7 @@ from src.chat.planner_actions.action_modifier import ActionModifier from src.chat.planner_actions.action_manager import ActionManager from src.chat.heart_flow.hfc_utils import CycleDetail from src.chat.heart_flow.hfc_utils import send_typing, stop_typing -from src.chat.frequency_control.talk_frequency_control import talk_frequency_control -from src.chat.frequency_control.focus_value_control import focus_value_control +from src.chat.frequency_control.frequency_control import frequency_control_manager from src.chat.express.expression_learner import expression_learner_manager from src.person_info.person_info import Person from src.plugin_system.base.component_types import ChatMode, EventType, ActionInfo @@ -85,8 +84,7 @@ class HeartFChatting: self.expression_learner = expression_learner_manager.get_expression_learner(self.stream_id) - self.talk_frequency_control = talk_frequency_control.get_talk_frequency_control(self.stream_id) - self.focus_value_control = focus_value_control.get_focus_value_control(self.stream_id) + self.frequency_control = frequency_control_manager.get_or_create_frequency_control(self.stream_id) self.action_manager = ActionManager() self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager) @@ -101,15 +99,8 @@ class HeartFChatting: self._cycle_counter = 0 self._current_cycle_detail: CycleDetail = None # type: ignore - self.reply_timeout_count = 0 - self.plan_timeout_count = 0 - self.last_read_time = time.time() - 10 - self.focus_energy = 1 - self.no_action_consecutive = 0 - # 最近三次no_action的新消息兴趣度记录 - self.recent_interest_records: deque = deque(maxlen=3) async def start(self): """检查是否需要启动主循环,如果未激活则启动。""" @@ -187,87 +178,14 @@ class HeartFChatting: f"耗时: {self._current_cycle_detail.end_time - self._current_cycle_detail.start_time:.1f}秒, " # type: ignore f"选择动作: {action_type}" + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") ) - - def _determine_form_type(self) -> None: - """判断使用哪种形式的no_action""" - # 如果连续no_action次数少于3次,使用waiting形式 - if self.no_action_consecutive <= 3: - self.focus_energy = 1 - else: - # 计算最近三次记录的兴趣度总和 - total_recent_interest = sum(self.recent_interest_records) - - # 计算调整后的阈值 - adjusted_threshold = 1 / self.talk_frequency_control.get_current_talk_frequency() - - logger.info( - f"{self.log_prefix} 最近三次兴趣度总和: {total_recent_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}" - ) - - # 如果兴趣度总和小于阈值,进入breaking形式 - if total_recent_interest < adjusted_threshold: - logger.info(f"{self.log_prefix} 兴趣度不足,进入休息") - self.focus_energy = random.randint(3, 6) - else: - logger.info(f"{self.log_prefix} 兴趣度充足,等待新消息") - self.focus_energy = 1 - - async def _should_process_messages(self, new_message: List["DatabaseMessages"]) -> tuple[bool, float]: - """ - 判断是否应该处理消息 - - Args: - new_message: 新消息列表 - mode: 当前聊天模式 - - Returns: - bool: 是否应该处理消息 - """ - new_message_count = len(new_message) - talk_frequency = self.talk_frequency_control.get_current_talk_frequency() - - modified_exit_count_threshold = self.focus_energy * 0.5 / talk_frequency - modified_exit_interest_threshold = 1.5 / talk_frequency + + async def caculate_interest_value(self, recent_messages_list: List["DatabaseMessages"]) -> float: total_interest = 0.0 - for msg in new_message: + for msg in recent_messages_list: interest_value = msg.interest_value if interest_value is not None and msg.processed_plain_text: total_interest += float(interest_value) - - if new_message_count >= modified_exit_count_threshold: - self.recent_interest_records.append(total_interest) - logger.info( - f"{self.log_prefix} 累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold:.1f}),结束等待" - ) - # logger.info(self.last_read_time) - # logger.info(new_message) - return True, total_interest / new_message_count if new_message_count > 0 else 0.0 - - # 检查累计兴趣值 - if new_message_count > 0: - # 只在兴趣值变化时输出log - if not hasattr(self, "_last_accumulated_interest") or total_interest != self._last_accumulated_interest: - logger.info( - f"{self.log_prefix} 休息中,新消息:{new_message_count}条,累计兴趣值: {total_interest:.2f}, 活跃度: {talk_frequency:.1f}" - ) - self._last_accumulated_interest = total_interest - - if total_interest >= modified_exit_interest_threshold: - # 记录兴趣度到列表 - self.recent_interest_records.append(total_interest) - logger.info( - f"{self.log_prefix} 累计兴趣值达到{total_interest:.2f}(>{modified_exit_interest_threshold:.1f}),结束等待" - ) - return True, total_interest / new_message_count if new_message_count > 0 else 0.0 - - # 每10秒输出一次等待状态 - if int(time.time() - self.last_read_time) > 0 and int(time.time() - self.last_read_time) % 15 == 0: - logger.debug( - f"{self.log_prefix} 已等待{time.time() - self.last_read_time:.0f}秒,累计{new_message_count}条消息,累计兴趣{total_interest:.1f},继续等待..." - ) - await asyncio.sleep(0.5) - - return False, 0.0 + return total_interest / len(recent_messages_list) async def _loopbody(self): recent_messages_list = message_api.get_messages_by_time_in_chat( @@ -279,16 +197,13 @@ class HeartFChatting: filter_mai=True, filter_command=True, ) - # 统一的消息处理逻辑 - should_process, interest_value = await self._should_process_messages(recent_messages_list) - - if should_process: + + if recent_messages_list: self.last_read_time = time.time() - await self._observe(interest_value=interest_value) - + await self._observe(interest_value=await self.caculate_interest_value(recent_messages_list),recent_messages_list=recent_messages_list) else: # Normal模式:消息数量不足,等待 - await asyncio.sleep(0.5) + await asyncio.sleep(0.2) return True return True @@ -342,8 +257,7 @@ class HeartFChatting: return loop_info, reply_text, cycle_timers - async def _observe(self, interest_value: float = 0.0) -> bool: - action_type = "no_action" + async def _observe(self, interest_value: float = 0.0,recent_messages_list: List["DatabaseMessages"] = []) -> bool: reply_text = "" # 初始化reply_text变量,避免UnboundLocalError # 使用sigmoid函数将interest_value转换为概率 @@ -362,22 +276,28 @@ class HeartFChatting: normal_mode_probability = ( calculate_normal_mode_probability(interest_value) * 2 - * self.talk_frequency_control.get_current_talk_frequency() + * self.frequency_control.get_final_talk_frequency() ) + + #对呼唤名字进行增幅 + for msg in recent_messages_list: + if msg.reply_probability_boost is not None and msg.reply_probability_boost > 0.0: + normal_mode_probability += msg.reply_probability_boost + if global_config.chat.mentioned_bot_reply and msg.is_mentioned: + normal_mode_probability += global_config.chat.mentioned_bot_reply + if global_config.chat.at_bot_inevitable_reply and msg.is_at: + normal_mode_probability += global_config.chat.at_bot_inevitable_reply + - # 根据概率决定使用哪种模式 + # 根据概率决定使用直接回复 + interest_triggerd = False + focus_triggerd = False + if random.random() < normal_mode_probability: - mode = ChatMode.NORMAL + interest_triggerd = True logger.info( - f"{self.log_prefix} 有兴趣({interest_value:.2f}),在{normal_mode_probability * 100:.0f}%概率下选择回复" + f"{self.log_prefix} 有新消息,在{normal_mode_probability * 100:.0f}%概率下选择回复" ) - else: - mode = ChatMode.FOCUS - - # 创建新的循环信息 - cycle_timers, thinking_id = self.start_cycle() - - logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") if s4u_config.enable_s4u: await send_typing() @@ -386,16 +306,21 @@ class HeartFChatting: await self.expression_learner.trigger_learning_for_chat() available_actions: Dict[str, ActionInfo] = {} - if random.random() > self.focus_value_control.get_current_focus_value() and mode == ChatMode.FOCUS: - # 如果激活度没有激活,并且聊天活跃度低,有可能不进行plan,相当于不在电脑前,不进行认真思考 - action_to_use_info = [ - ActionPlannerInfo( - action_type="no_action", - reasoning="专注不足", - action_data={}, - ) - ] - else: + + #如果兴趣度不足以激活 + if not interest_triggerd: + #看看专注值够不够 + if random.random() < self.frequency_control.get_final_focus_value(): + #专注值足够,仍然进入正式思考 + focus_triggerd = True #都没触发,路边 + + + # 任意一种触发都行 + if interest_triggerd or focus_triggerd: + # 进入正式思考模式 + cycle_timers, thinking_id = self.start_cycle() + logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") + # 第一步:动作检查 with Timer("动作检查", cycle_timers): try: @@ -433,103 +358,93 @@ class HeartFChatting: ): return False with Timer("规划器", cycle_timers): + # 根据不同触发,进入不同plan + if focus_triggerd: + mode = ChatMode.FOCUS + else: + mode = ChatMode.NORMAL + action_to_use_info, _ = await self.action_planner.plan( mode=mode, loop_start_time=self.last_read_time, available_actions=available_actions, ) - # for action in action_to_use_info: - # print(action.action_type) + # 3. 并行执行所有动作 + action_tasks = [ + asyncio.create_task( + self._execute_action(action, action_to_use_info, thinking_id, available_actions, cycle_timers) + ) + for action in action_to_use_info + ] - # 3. 并行执行所有动作 - action_tasks = [ - asyncio.create_task( - self._execute_action(action, action_to_use_info, thinking_id, available_actions, cycle_timers) - ) - for action in action_to_use_info - ] + # 并行执行所有任务 + results = await asyncio.gather(*action_tasks, return_exceptions=True) - # 并行执行所有任务 - results = await asyncio.gather(*action_tasks, return_exceptions=True) + # 处理执行结果 + reply_loop_info = None + reply_text_from_reply = "" + action_success = False + action_reply_text = "" + action_command = "" - # 处理执行结果 - reply_loop_info = None - reply_text_from_reply = "" - action_success = False - action_reply_text = "" - action_command = "" + for i, result in enumerate(results): + if isinstance(result, BaseException): + logger.error(f"{self.log_prefix} 动作执行异常: {result}") + continue - for i, result in enumerate(results): - if isinstance(result, BaseException): - logger.error(f"{self.log_prefix} 动作执行异常: {result}") - continue + _cur_action = action_to_use_info[i] + if result["action_type"] != "reply": + action_success = result["success"] + action_reply_text = result["reply_text"] + action_command = result.get("command", "") + elif result["action_type"] == "reply": + if result["success"]: + reply_loop_info = result["loop_info"] + reply_text_from_reply = result["reply_text"] + else: + logger.warning(f"{self.log_prefix} 回复动作执行失败") - _cur_action = action_to_use_info[i] - if result["action_type"] != "reply": - action_success = result["success"] - action_reply_text = result["reply_text"] - action_command = result.get("command", "") - elif result["action_type"] == "reply": - if result["success"]: - reply_loop_info = result["loop_info"] - reply_text_from_reply = result["reply_text"] - else: - logger.warning(f"{self.log_prefix} 回复动作执行失败") - - # 构建最终的循环信息 - if reply_loop_info: - # 如果有回复信息,使用回复的loop_info作为基础 - loop_info = reply_loop_info - # 更新动作执行信息 - loop_info["loop_action_info"].update( - { - "action_taken": action_success, - "command": action_command, - "taken_time": time.time(), + # 构建最终的循环信息 + if reply_loop_info: + # 如果有回复信息,使用回复的loop_info作为基础 + loop_info = reply_loop_info + # 更新动作执行信息 + loop_info["loop_action_info"].update( + { + "action_taken": action_success, + "command": action_command, + "taken_time": time.time(), + } + ) + reply_text = reply_text_from_reply + else: + # 没有回复信息,构建纯动作的loop_info + loop_info = { + "loop_plan_info": { + "action_result": action_to_use_info, + }, + "loop_action_info": { + "action_taken": action_success, + "reply_text": action_reply_text, + "command": action_command, + "taken_time": time.time(), + }, } - ) - reply_text = reply_text_from_reply - else: - # 没有回复信息,构建纯动作的loop_info - loop_info = { - "loop_plan_info": { - "action_result": action_to_use_info, - }, - "loop_action_info": { - "action_taken": action_success, - "reply_text": action_reply_text, - "command": action_command, - "taken_time": time.time(), - }, - } - reply_text = action_reply_text + reply_text = action_reply_text + + + self.end_cycle(loop_info, cycle_timers) + self.print_cycle_info(cycle_timers) - if s4u_config.enable_s4u: - await stop_typing() - await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text) + """S4U内容,暂时保留""" + if s4u_config.enable_s4u: + await stop_typing() + await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text) + """S4U内容,暂时保留""" - self.end_cycle(loop_info, cycle_timers) - self.print_cycle_info(cycle_timers) - - # await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", "")) - - action_type = action_to_use_info[0].action_type if action_to_use_info else "no_action" - - # 管理no_action计数器:当执行了非no_action动作时,重置计数器 - if action_type != "no_action": - # no_action逻辑已集成到heartFC_chat.py中,直接重置计数器 - self.recent_interest_records.clear() - self.no_action_consecutive = 0 - logger.debug(f"{self.log_prefix} 执行了{action_type}动作,重置no_action计数器") return True - if action_type == "no_action": - self.no_action_consecutive += 1 - self._determine_form_type() - - return True - async def _main_chat_loop(self): """主循环,持续进行计划并可能回复消息,直到被外部取消。""" try: diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index b1dccdaf..ac424c66 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -32,10 +32,10 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]: Returns: Tuple[float, bool, list[str]]: (兴趣度, 是否被提及, 关键词) """ - if message.is_picid: + if message.is_picid or message.is_emoji: return 0.0, [] - is_mentioned, _ = is_mentioned_bot_in_message(message) + is_mentioned,is_at,reply_probability_boost = is_mentioned_bot_in_message(message) interested_rate = 0.0 with Timer("记忆激活"): @@ -79,17 +79,13 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]: # 确保在范围内 base_interest = min(max(base_interest, 0.01), 0.3) - interested_rate += base_interest - - if is_mentioned: - interest_increase_on_mention = 2 - interested_rate += interest_increase_on_mention - - message.interest_value = interested_rate + message.interest_value = base_interest message.is_mentioned = is_mentioned - - return interested_rate, keywords + message.is_at = is_at + message.reply_probability_boost = reply_probability_boost + + return base_interest, keywords class HeartFCMessageReceiver: diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 66a1c029..5f97e7c8 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -108,6 +108,8 @@ class MessageRecv(Message): self.has_picid = False self.is_voice = False self.is_mentioned = None + self.is_at = False + self.reply_probability_boost = 0.0 self.is_notify = False self.is_command = False diff --git a/src/chat/message_receive/storage.py b/src/chat/message_receive/storage.py index c9de76ec..3d84f270 100644 --- a/src/chat/message_receive/storage.py +++ b/src/chat/message_receive/storage.py @@ -56,6 +56,8 @@ class MessageStorage: filtered_display_message = "" interest_value = 0 is_mentioned = False + is_at = False + reply_probability_boost = 0.0 reply_to = message.reply_to priority_mode = "" priority_info = {} @@ -70,6 +72,8 @@ class MessageStorage: filtered_display_message = "" interest_value = message.interest_value is_mentioned = message.is_mentioned + is_at = message.is_at + reply_probability_boost = message.reply_probability_boost reply_to = "" priority_mode = message.priority_mode priority_info = message.priority_info @@ -100,6 +104,8 @@ class MessageStorage: # Flattened chat_info reply_to=reply_to, is_mentioned=is_mentioned, + is_at=is_at, + reply_probability_boost=reply_probability_boost, chat_info_stream_id=chat_info_dict.get("stream_id"), chat_info_platform=chat_info_dict.get("platform"), chat_info_user_platform=user_info_from_chat.get("platform"), diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 61bc0675..f9e02b08 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -65,7 +65,6 @@ def init_prompt(): 动作描述:参与聊天回复,发送文本进行表达 - 你想要闲聊或者随便附和 - 有人提到了你,但是你还没有回应 -- {mentioned_bonus} - 如果你刚刚进行了回复,不要对同一个话题重复回应 {{ "action": "reply", @@ -93,7 +92,6 @@ def init_prompt(): 现在,最新的聊天消息引起了你的兴趣,你想要对其中的消息进行回复,回复标准如下: - 你想要闲聊或者随便附和 - 有人提到了你,但是你还没有回应 -- {mentioned_bonus} - 如果你刚刚进行了回复,不要对同一个话题重复回应 你之前的动作记录: @@ -465,7 +463,7 @@ class ActionPlanner: ) ) - logger.info(f"{self.log_prefix}副规划器返回了{len(action_planner_infos)}个action") + logger.debug(f"{self.log_prefix}副规划器返回了{len(action_planner_infos)}个action") return action_planner_infos async def plan( @@ -553,7 +551,7 @@ class ActionPlanner: for i, (action_name, action_info) in enumerate(action_items): sub_planner_lists[i % sub_planner_num].append((action_name, action_info)) - logger.info( + logger.debug( f"{self.log_prefix}成功将{sub_planner_actions_num}个actions分配到{sub_planner_num}个子列表中" ) for i, action_list in enumerate(sub_planner_lists): @@ -585,7 +583,7 @@ class ActionPlanner: for sub_result in sub_plan_results: all_sub_planner_results.extend(sub_result) - logger.info(f"{self.log_prefix}所有副规划器共返回了{len(all_sub_planner_results)}个action") + logger.info(f"{self.log_prefix}小脑决定执行{len(all_sub_planner_results)}个动作") # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- prompt, message_id_list = await self.build_planner_prompt( @@ -777,12 +775,6 @@ class ActionPlanner: else: actions_before_now_block = "" - mentioned_bonus = "" - if global_config.chat.mentioned_bot_inevitable_reply: - mentioned_bonus = "\n- 有人提到你" - if global_config.chat.at_bot_inevitable_reply: - mentioned_bonus = "\n- 有人提到你,或者at你" - chat_context_description = "你现在正在一个群聊中" chat_target_name = None if not is_group_chat and chat_target_info: @@ -838,7 +830,6 @@ class ActionPlanner: chat_context_description=chat_context_description, chat_content_block=chat_content_block, actions_before_now_block=actions_before_now_block, - mentioned_bonus=mentioned_bonus, # action_options_text=action_options_block, moderation_prompt=moderation_prompt_block, name_block=name_block, @@ -850,7 +841,6 @@ class ActionPlanner: time_block=time_block, chat_context_description=chat_context_description, chat_content_block=chat_content_block, - mentioned_bonus=mentioned_bonus, moderation_prompt=moderation_prompt_block, name_block=name_block, actions_before_now_block=actions_before_now_block, diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 88562fff..79b18906 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -43,15 +43,15 @@ def db_message_to_str(message_dict: dict) -> str: return result -def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: +def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, bool, float]: """检查消息是否提到了机器人""" - keywords = [global_config.bot.nickname] - nicknames = global_config.bot.alias_names + keywords = [global_config.bot.nickname] + list(global_config.bot.alias_names) reply_probability = 0.0 is_at = False is_mentioned = False - if message.is_mentioned is not None: - return bool(message.is_mentioned), message.is_mentioned + + # 这部分怎么处理啊啊啊啊 + #我觉得可以给消息加一个 reply_probability_boost字段 if ( message.message_info.additional_config is not None and message.message_info.additional_config.get("is_mentioned") is not None @@ -59,18 +59,15 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: try: reply_probability = float(message.message_info.additional_config.get("is_mentioned")) # type: ignore is_mentioned = True - return is_mentioned, reply_probability + return is_mentioned, is_at, reply_probability except Exception as e: logger.warning(str(e)) logger.warning( f"消息中包含不合理的设置 is_mentioned: {message.message_info.additional_config.get('is_mentioned')}" ) - if global_config.bot.nickname in message.processed_plain_text: - is_mentioned = True - - for alias_name in global_config.bot.alias_names: - if alias_name in message.processed_plain_text: + for keyword in keywords: + if keyword in message.processed_plain_text: is_mentioned = True # 判断是否被@ @@ -78,10 +75,6 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: is_at = True is_mentioned = True - # print(f"message.processed_plain_text: {message.processed_plain_text}") - # print(f"is_mentioned: {is_mentioned}") - # print(f"is_at: {is_at}") - if is_at and global_config.chat.at_bot_inevitable_reply: reply_probability = 1.0 logger.debug("被@,回复概率设置为100%") @@ -104,13 +97,10 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]: for keyword in keywords: if keyword in message_content: is_mentioned = True - for nickname in nicknames: - if nickname in message_content: - is_mentioned = True - if is_mentioned and global_config.chat.mentioned_bot_inevitable_reply: + if is_mentioned and global_config.chat.mentioned_bot_reply: reply_probability = 1.0 logger.debug("被提及,回复概率设置为100%") - return is_mentioned, reply_probability + return is_mentioned, is_at, reply_probability async def get_embedding(text, request_type="embedding") -> Optional[List[float]]: diff --git a/src/common/data_models/database_data_model.py b/src/common/data_models/database_data_model.py index b752cbb7..bf4a5f52 100644 --- a/src/common/data_models/database_data_model.py +++ b/src/common/data_models/database_data_model.py @@ -67,6 +67,8 @@ class DatabaseMessages(BaseDataModel): key_words: Optional[str] = None, key_words_lite: Optional[str] = None, is_mentioned: Optional[bool] = None, + is_at: Optional[bool] = None, + reply_probability_boost: Optional[float] = None, processed_plain_text: Optional[str] = None, display_message: Optional[str] = None, priority_mode: Optional[str] = None, @@ -104,6 +106,9 @@ class DatabaseMessages(BaseDataModel): self.key_words_lite = key_words_lite self.is_mentioned = is_mentioned + self.is_at = is_at + self.reply_probability_boost = reply_probability_boost + self.processed_plain_text = processed_plain_text self.display_message = display_message @@ -171,6 +176,8 @@ class DatabaseMessages(BaseDataModel): "key_words": self.key_words, "key_words_lite": self.key_words_lite, "is_mentioned": self.is_mentioned, + "is_at": self.is_at, + "reply_probability_boost": self.reply_probability_boost, "processed_plain_text": self.processed_plain_text, "display_message": self.display_message, "priority_mode": self.priority_mode, diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index 330bfa7d..14ce741d 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -137,7 +137,8 @@ class Messages(BaseModel): key_words_lite = TextField(null=True) is_mentioned = BooleanField(null=True) - + is_at = BooleanField(null=True) + reply_probability_boost = DoubleField(null=True) # 从 chat_info 扁平化而来的字段 chat_info_stream_id = TextField() chat_info_platform = TextField() diff --git a/src/common/logger.py b/src/common/logger.py index d2872b4e..ab0fd849 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -355,6 +355,7 @@ MODULE_COLORS = { # 核心模块 "main": "\033[1;97m", # 亮白色+粗体 (主程序) + "memory": "\033[38;5;34m", # 天蓝色 "config": "\033[93m", # 亮黄色 "common": "\033[95m", # 亮紫色 @@ -366,10 +367,9 @@ MODULE_COLORS = { "llm_models": "\033[36m", # 青色 "remote": "\033[38;5;242m", # 深灰色,更不显眼 "planner": "\033[36m", - "memory": "\033[38;5;117m", # 天蓝色 - "hfc": "\033[38;5;81m", # 稍微暗一些的青色,保持可读 - "action_manager": "\033[38;5;208m", # 橙色,不与replyer重复 - # 关系系统 + + + "relation": "\033[38;5;139m", # 柔和的紫色,不刺眼 # 聊天相关模块 "normal_chat": "\033[38;5;81m", # 亮蓝绿色 diff --git a/src/config/config.py b/src/config/config.py index 80e275f8..99b8d00b 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -56,7 +56,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.10.2-snapshot.1" +MMC_VERSION = "0.10.2-snapshot.2" def get_key_comment(toml_table, key): diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 7d9d950b..4d3758ce 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -71,14 +71,14 @@ class ChatConfig(ConfigBase): interest_rate_mode: Literal["fast", "accurate"] = "fast" """兴趣值计算模式,fast为快速计算,accurate为精确计算""" - mentioned_bot_inevitable_reply: bool = False - """提及 bot 必然回复""" + mentioned_bot_reply: float = 1 + """提及 bot 必然回复,1为100%回复,0为不额外增幅""" planner_size: float = 1.5 """副规划器大小,越小,麦麦的动作执行能力越精细,但是消耗更多token,调大可以缓解429类错误""" - at_bot_inevitable_reply: bool = False - """@bot 必然回复""" + at_bot_inevitable_reply: float = 1 + """@bot 必然回复,1为100%回复,0为不额外增幅""" talk_frequency: float = 0.5 """回复频率阈值""" diff --git a/src/plugin_system/apis/frequency_api.py b/src/plugin_system/apis/frequency_api.py index 0b0fe3cf..448050b9 100644 --- a/src/plugin_system/apis/frequency_api.py +++ b/src/plugin_system/apis/frequency_api.py @@ -1,27 +1,26 @@ from src.common.logger import get_logger -from src.chat.frequency_control.focus_value_control import focus_value_control -from src.chat.frequency_control.talk_frequency_control import talk_frequency_control +from src.chat.frequency_control.frequency_control import frequency_control_manager logger = get_logger("frequency_api") def get_current_focus_value(chat_id: str) -> float: - return focus_value_control.get_focus_value_control(chat_id).get_current_focus_value() + return frequency_control_manager.get_or_create_frequency_control(chat_id).get_final_focus_value() def get_current_talk_frequency(chat_id: str) -> float: - return talk_frequency_control.get_talk_frequency_control(chat_id).get_current_talk_frequency() + return frequency_control_manager.get_or_create_frequency_control(chat_id).get_final_talk_frequency() def set_focus_value_adjust(chat_id: str, focus_value_adjust: float) -> None: - focus_value_control.get_focus_value_control(chat_id).focus_value_adjust = focus_value_adjust + frequency_control_manager.get_or_create_frequency_control(chat_id).focus_value_external_adjust = focus_value_adjust def set_talk_frequency_adjust(chat_id: str, talk_frequency_adjust: float) -> None: - talk_frequency_control.get_talk_frequency_control(chat_id).talk_frequency_adjust = talk_frequency_adjust + frequency_control_manager.get_or_create_frequency_control(chat_id).talk_frequency_external_adjust = talk_frequency_adjust def get_focus_value_adjust(chat_id: str) -> float: - return focus_value_control.get_focus_value_control(chat_id).focus_value_adjust + return frequency_control_manager.get_or_create_frequency_control(chat_id).focus_value_external_adjust def get_talk_frequency_adjust(chat_id: str) -> float: - return talk_frequency_control.get_talk_frequency_control(chat_id).talk_frequency_adjust + return frequency_control_manager.get_or_create_frequency_control(chat_id).talk_frequency_external_adjust diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 3805051c..d2e9a007 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "6.7.2" +version = "6.8.0" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -59,17 +59,17 @@ expression_groups = [ [chat] #麦麦的聊天设置 talk_frequency = 0.5 -# 麦麦活跃度,越高,麦麦回复越多,范围0-1 +# 麦麦活跃度,越高,麦麦越容易回复,范围0-1 focus_value = 0.5 # 麦麦的专注度,越高越容易持续连续对话,可能消耗更多token, 范围0-1 +mentioned_bot_reply = 1 # 提及时,回复概率增幅,1为100%回复,0为不额外增幅 +at_bot_inevitable_reply = 1 # at时,回复概率增幅,1为100%回复,0为不额外增幅 + max_context_size = 20 # 上下文长度 planner_size = 2.5 # 副规划器大小,越小,麦麦的动作执行能力越精细,但是消耗更多token,调大可以缓解429类错误 -mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复 -at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复 - focus_value_adjust = [ ["", "8:00,1", "12:00,0.8", "18:00,1", "01:00,0.3"], ["qq:114514:group", "12:20,0.6", "16:10,0.5", "20:10,0.8", "00:10,0.3"], From 83999fb20b8d064f12432fdabc991071c6b89a5a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 31 Aug 2025 12:48:48 +0800 Subject: [PATCH 16/18] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=A2=91=E7=8E=87=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frequency_control/frequency_control.py | 205 ++++++++++++++++-- src/chat/heart_flow/heartFC_chat.py | 11 +- 2 files changed, 189 insertions(+), 27 deletions(-) diff --git a/src/chat/frequency_control/frequency_control.py b/src/chat/frequency_control/frequency_control.py index 63fb0e33..95c691aa 100644 --- a/src/chat/frequency_control/frequency_control.py +++ b/src/chat/frequency_control/frequency_control.py @@ -13,13 +13,19 @@ logger = get_logger("frequency_control") class FrequencyControl: """ 频率控制类,可以根据最近时间段的发言数量和发言人数动态调整频率 + + 特点: + - 发言频率调整:基于最近10分钟的数据,评估单位为"消息数/10分钟" + - 专注度调整:基于最近10分钟的数据,评估单位为"消息数/10分钟" + - 历史基准值:基于最近一周的数据,按小时统计,每小时都有独立的基准值 + - 统一标准:两个调整都使用10分钟窗口,确保逻辑一致性和响应速度 """ def __init__(self, chat_id: str): self.chat_id = chat_id self.chat_stream: ChatStream = get_chat_manager().get_stream(self.chat_id) if not self.chat_stream: - raise ValueError(f"无法找到聊天流: {self.chat_id}") + raise ValueError(f"无法找到聊天流: {chat_id}") self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]" # 发言频率调整值 self.talk_frequency_adjust: float = 1.0 @@ -42,13 +48,139 @@ class FrequencyControl: self.min_adjust = 0.3 # 最小调整值 self.max_adjust = 2.0 # 最大调整值 - # 基准值(可根据实际情况调整) - self.base_message_count = 5 # 基准消息数量 - self.base_user_count = 3 # 基准用户数量 + # 动态基准值(将根据历史数据计算) + self.base_message_count = 5 # 默认基准消息数量,将被动态更新 + self.base_user_count = 3 # 默认基准用户数量,将被动态更新 # 平滑因子 self.smoothing_factor = 0.3 + + # 历史数据相关参数 + self._last_historical_update = 0 + self._historical_update_interval = 3600 # 每小时更新一次历史基准值 + self._historical_days = 7 # 使用最近7天的数据计算基准值 + + # 按小时统计的历史基准值 + self._hourly_baseline = { + 'messages': {}, # {0-23: 平均消息数} + 'users': {} # {0-23: 平均用户数} + } + + # 初始化24小时的默认基准值 + for hour in range(24): + self._hourly_baseline['messages'][hour] = 5.0 + self._hourly_baseline['users'][hour] = 3.0 + def _update_historical_baseline(self): + """ + 更新基于历史数据的基准值 + 使用最近一周的数据,按小时统计平均消息数量和用户数量 + """ + current_time = time.time() + + # 检查是否需要更新历史基准值 + if current_time - self._last_historical_update < self._historical_update_interval: + return + + try: + # 计算一周前的时间戳 + week_ago = current_time - (self._historical_days * 24 * 3600) + + # 获取最近一周的消息数据 + historical_messages = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_stream.stream_id, + start_time=week_ago, + end_time=current_time, + filter_mai=True, + filter_command=True + ) + + if historical_messages: + # 按小时统计消息数和用户数 + hourly_stats = {hour: {'messages': [], 'users': set()} for hour in range(24)} + + for msg in historical_messages: + # 获取消息的小时(UTC时间) + msg_time = time.localtime(msg.time) + msg_hour = msg_time.tm_hour + + # 统计消息数 + hourly_stats[msg_hour]['messages'].append(msg) + + # 统计用户数 + if msg.user_info and msg.user_info.user_id: + hourly_stats[msg_hour]['users'].add(msg.user_info.user_id) + + # 计算每个小时的平均值(基于一周的数据) + for hour in range(24): + # 计算该小时的平均消息数(一周内该小时的总消息数 / 7天) + total_messages = len(hourly_stats[hour]['messages']) + avg_messages = total_messages / self._historical_days + + # 计算该小时的平均用户数(一周内该小时的总用户数 / 7天) + total_users = len(hourly_stats[hour]['users']) + avg_users = total_users / self._historical_days + + # 使用平滑更新更新基准值 + self._hourly_baseline['messages'][hour] = ( + self._hourly_baseline['messages'][hour] * 0.7 + avg_messages * 0.3 + ) + self._hourly_baseline['users'][hour] = ( + self._hourly_baseline['users'][hour] * 0.7 + avg_users * 0.3 + ) + + # 确保基准值不为0 + self._hourly_baseline['messages'][hour] = max(1.0, self._hourly_baseline['messages'][hour]) + self._hourly_baseline['users'][hour] = max(1.0, self._hourly_baseline['users'][hour]) + + # 更新整体基准值(用于兼容性) + overall_avg_messages = sum(self._hourly_baseline['messages'].values()) / 24 + overall_avg_users = sum(self._hourly_baseline['users'].values()) / 24 + + self.base_message_count = overall_avg_messages + self.base_user_count = overall_avg_users + + logger.info( + f"{self.log_prefix} 历史基准值更新完成: " + f"整体平均消息数={overall_avg_messages:.2f}, 整体平均用户数={overall_avg_users:.2f}" + ) + + # 记录几个关键时段的基准值 + key_hours = [8, 12, 18, 22] # 早、中、晚、夜 + for hour in key_hours: + # 计算该小时平均每10分钟的消息数和用户数 + hourly_10min_messages = self._hourly_baseline['messages'][hour] / 6 # 1小时 = 6个10分钟 + hourly_10min_users = self._hourly_baseline['users'][hour] / 6 + logger.info( + f"{self.log_prefix} {hour}时基准值: " + f"消息数={self._hourly_baseline['messages'][hour]:.2f}/小时 " + f"({hourly_10min_messages:.2f}/10分钟), " + f"用户数={self._hourly_baseline['users'][hour]:.2f}/小时 " + f"({hourly_10min_users:.2f}/10分钟)" + ) + + else: + # 如果没有历史数据,使用默认值 + logger.info(f"{self.log_prefix} 无历史数据,使用默认基准值") + + except Exception as e: + logger.error(f"{self.log_prefix} 更新历史基准值时出错: {e}") + # 出错时保持原有基准值不变 + + self._last_historical_update = current_time + + def _get_current_hour_baseline(self) -> tuple[float, float]: + """ + 获取当前小时的基准值 + + Returns: + tuple: (基准消息数, 基准用户数) + """ + current_hour = time.localtime().tm_hour + return ( + self._hourly_baseline['messages'][current_hour], + self._hourly_baseline['users'][current_hour] + ) def get_dynamic_talk_frequency_adjust(self) -> float: """ @@ -81,11 +213,14 @@ class FrequencyControl: if current_time - self.last_update_time < self.update_interval: return + # 先更新历史基准值 + self._update_historical_baseline() + try: - # 获取最近30分钟的数据(发言频率更敏感) + # 获取最近10分钟的数据(发言频率更敏感) recent_messages = message_api.get_messages_by_time_in_chat( chat_id=self.chat_stream.stream_id, - start_time=current_time - 1800, # 30分钟前 + start_time=current_time - 600, # 10分钟前 end_time=current_time, filter_mai=True, filter_command=True @@ -99,12 +234,19 @@ class FrequencyControl: user_ids.add(msg.user_info.user_id) user_count = len(user_ids) + # 获取当前小时的基准值 + current_hour_base_messages, current_hour_base_users = self._get_current_hour_baseline() + + # 计算当前小时平均每10分钟的基准值 + current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 + current_hour_10min_users = current_hour_base_users / 6 + # 发言频率调整逻辑:人少话多时提高回复频率 if user_count > 0: - # 计算人均消息数 + # 计算人均消息数(10分钟窗口) messages_per_user = message_count / user_count - # 基准人均消息数 - base_messages_per_user = self.base_message_count / self.base_user_count if self.base_user_count > 0 else 1.0 + # 使用当前小时每10分钟的基准人均消息数 + base_messages_per_user = current_hour_10min_messages / current_hour_10min_users if current_hour_10min_users > 0 else 1.0 # 如果人均消息数高,说明活跃度高,提高回复频率 if messages_per_user > base_messages_per_user: @@ -126,9 +268,11 @@ class FrequencyControl: ) logger.info( - f"{self.log_prefix} 发言频率调整更新: " + f"{self.log_prefix} 发言频率调整更新(10分钟窗口): " f"消息数={message_count}, 用户数={user_count}, " f"人均消息数={message_count/user_count if user_count > 0 else 0:.2f}, " + f"当前小时基准值(消息:{current_hour_base_messages:.2f}/小时, {current_hour_10min_messages:.2f}/10分钟, " + f"用户:{current_hour_base_users:.2f}/小时, {current_hour_10min_users:.2f}/10分钟), " f"调整值={self.talk_frequency_adjust:.2f}" ) @@ -147,10 +291,10 @@ class FrequencyControl: return try: - # 获取最近1小时的数据 + # 获取最近10分钟的数据(与发言频率保持一致) recent_messages = message_api.get_messages_by_time_in_chat( chat_id=self.chat_stream.stream_id, - start_time=current_time - 3600, # 1小时前 + start_time=current_time - 600, # 10分钟前 end_time=current_time, filter_mai=True, filter_command=True @@ -164,12 +308,19 @@ class FrequencyControl: user_ids.add(msg.user_info.user_id) user_count = len(user_ids) + # 获取当前小时的基准值 + current_hour_base_messages, current_hour_base_users = self._get_current_hour_baseline() + + # 计算当前小时平均每10分钟的基准值 + current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 + current_hour_10min_users = current_hour_base_users / 6 + # 专注度调整逻辑:人多话多时提高专注度 - if user_count > 0 and self.base_user_count > 0: - # 计算用户活跃度比率 - user_ratio = user_count / self.base_user_count - # 计算消息活跃度比率 - message_ratio = message_count / self.base_message_count if self.base_message_count > 0 else 1.0 + if user_count > 0 and current_hour_10min_users > 0: + # 计算用户活跃度比率(基于10分钟数据) + user_ratio = user_count / current_hour_10min_users + # 计算消息活跃度比率(基于10分钟数据) + message_ratio = message_count / current_hour_10min_messages if current_hour_10min_messages > 0 else 1.0 # 如果用户多且消息多,提高专注度 if user_ratio > 1.2 and message_ratio > 1.2: @@ -193,11 +344,17 @@ class FrequencyControl: target_focus_adjust * self.smoothing_factor ) + # 计算当前小时平均每10分钟的基准值 + current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 + current_hour_10min_users = current_hour_base_users / 6 + logger.info( - f"{self.log_prefix} 专注度调整更新: " + f"{self.log_prefix} 专注度调整更新(10分钟窗口): " f"消息数={message_count}, 用户数={user_count}, " - f"用户比率={user_count/self.base_user_count if self.base_user_count > 0 else 0:.2f}, " - f"消息比率={message_count/self.base_message_count if self.base_message_count > 0 else 0:.2f}, " + f"用户比率={user_count/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}, " + f"消息比率={message_count/current_hour_10min_messages if current_hour_10min_messages > 0 else 0:.2f}, " + f"当前小时基准值(消息:{current_hour_base_messages:.2f}/小时, {current_hour_10min_messages:.2f}/10分钟, " + f"用户:{current_hour_base_users:.2f}/小时, {current_hour_10min_users:.2f}/10分钟), " f"调整值={self.focus_value_adjust:.2f}" ) @@ -218,7 +375,9 @@ class FrequencyControl: base_message_count: Optional[int] = None, base_user_count: Optional[int] = None, smoothing_factor: Optional[float] = None, - update_interval: Optional[int] = None + update_interval: Optional[int] = None, + historical_update_interval: Optional[int] = None, + historical_days: Optional[int] = None ): """ 设置调整参数 @@ -243,6 +402,10 @@ class FrequencyControl: self.smoothing_factor = max(0.0, min(1.0, smoothing_factor)) if update_interval is not None: self.update_interval = max(10, update_interval) + if historical_update_interval is not None: + self._historical_update_interval = max(300, historical_update_interval) # 最少5分钟 + if historical_days is not None: + self._historical_days = max(1, min(30, historical_days)) # 1-30天之间 class FrequencyControlManager: diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 33b73427..289752d0 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -322,12 +322,11 @@ class HeartFChatting: logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") # 第一步:动作检查 - with Timer("动作检查", cycle_timers): - try: - await self.action_modifier.modify_actions() - available_actions = self.action_manager.get_using_actions() - except Exception as e: - logger.error(f"{self.log_prefix} 动作修改失败: {e}") + try: + await self.action_modifier.modify_actions() + available_actions = self.action_manager.get_using_actions() + except Exception as e: + logger.error(f"{self.log_prefix} 动作修改失败: {e}") # 执行planner is_group_chat, chat_target_info, _ = self.action_planner.get_necessary_info() From 4bd484f8e41517705cb954c98f447e53b56d0f80 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 31 Aug 2025 12:59:26 +0800 Subject: [PATCH 17/18] =?UTF-8?q?=E8=83=BD=E5=A2=9E=E9=AB=98=E4=B9=9F?= =?UTF-8?q?=E8=83=BD=E9=99=8D=E4=BD=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelogs/changelog.md | 2 +- .../frequency_control/frequency_control.py | 113 +++++++++++++----- 2 files changed, 81 insertions(+), 34 deletions(-) diff --git a/changelogs/changelog.md b/changelogs/changelog.md index a26088c7..5970904a 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -3,7 +3,7 @@ ## [0.10.2] - 2025-8-31 ### 🌟 主要功能更改 -- 大幅优化了聊天逻辑,更易配置 +- 大幅优化了聊天逻辑,更易配置,动态控制 - 记忆系统重新启用,更好更优秀 - 更好的event系统 - 现在支持提及100%回复 diff --git a/src/chat/frequency_control/frequency_control.py b/src/chat/frequency_control/frequency_control.py index 95c691aa..5b16f8bb 100644 --- a/src/chat/frequency_control/frequency_control.py +++ b/src/chat/frequency_control/frequency_control.py @@ -17,8 +17,10 @@ class FrequencyControl: 特点: - 发言频率调整:基于最近10分钟的数据,评估单位为"消息数/10分钟" - 专注度调整:基于最近10分钟的数据,评估单位为"消息数/10分钟" - - 历史基准值:基于最近一周的数据,按小时统计,每小时都有独立的基准值 + - 历史基准值:基于最近一周的数据,按小时统计,每小时都有独立的基准值(需要至少50条历史消息) - 统一标准:两个调整都使用10分钟窗口,确保逻辑一致性和响应速度 + - 双向调整:根据活跃度高低,既能提高也能降低频率和专注度 + - 数据充足性检查:当历史数据不足50条时,不更新基准值;当基准值为默认值时,不进行动态调整 """ def __init__(self, chat_id: str): @@ -95,7 +97,7 @@ class FrequencyControl: filter_command=True ) - if historical_messages: + if historical_messages and len(historical_messages) >= 50: # 按小时统计消息数和用户数 hourly_stats = {hour: {'messages': [], 'users': set()} for hour in range(24)} @@ -159,9 +161,12 @@ class FrequencyControl: f"({hourly_10min_users:.2f}/10分钟)" ) + elif historical_messages and len(historical_messages) < 50: + # 历史数据不足50条,不更新基准值 + logger.info(f"{self.log_prefix} 历史数据不足50条({len(historical_messages)}条),不更新基准值") else: - # 如果没有历史数据,使用默认值 - logger.info(f"{self.log_prefix} 无历史数据,使用默认基准值") + # 如果没有历史数据,不更新基准值 + logger.info(f"{self.log_prefix} 无历史数据,不更新基准值") except Exception as e: logger.error(f"{self.log_prefix} 更新历史基准值时出错: {e}") @@ -241,21 +246,31 @@ class FrequencyControl: current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 current_hour_10min_users = current_hour_base_users / 6 - # 发言频率调整逻辑:人少话多时提高回复频率 - if user_count > 0: - # 计算人均消息数(10分钟窗口) - messages_per_user = message_count / user_count - # 使用当前小时每10分钟的基准人均消息数 - base_messages_per_user = current_hour_10min_messages / current_hour_10min_users if current_hour_10min_users > 0 else 1.0 - - # 如果人均消息数高,说明活跃度高,提高回复频率 - if messages_per_user > base_messages_per_user: - # 人少话多:提高回复频率 - target_talk_adjust = min(self.max_adjust, messages_per_user / base_messages_per_user) + # 发言频率调整逻辑:根据活跃度双向调整 + # 检查是否有足够的数据进行分析 + if user_count > 0 and message_count >= 2: # 至少需要2条消息才能进行有意义的分析 + # 检查历史基准值是否有效(不是默认值) + if current_hour_base_messages > 5.0 or current_hour_base_users > 3.0: + # 计算人均消息数(10分钟窗口) + messages_per_user = message_count / user_count + # 使用当前小时每10分钟的基准人均消息数 + base_messages_per_user = current_hour_10min_messages / current_hour_10min_users if current_hour_10min_users > 0 else 1.0 + + # 双向调整逻辑 + if messages_per_user > base_messages_per_user * 1.2: + # 活跃度很高:提高回复频率 + target_talk_adjust = min(self.max_adjust, messages_per_user / base_messages_per_user) + elif messages_per_user < base_messages_per_user * 0.8: + # 活跃度很低:降低回复频率 + target_talk_adjust = max(self.min_adjust, messages_per_user / base_messages_per_user) + else: + # 活跃度正常:保持正常 + target_talk_adjust = 1.0 else: - # 活跃度一般:保持正常 + # 历史基准值不足,不调整 target_talk_adjust = 1.0 else: + # 数据不足:不调整 target_talk_adjust = 1.0 # 限制调整范围 @@ -267,13 +282,24 @@ class FrequencyControl: target_talk_adjust * self.smoothing_factor ) + # 判断调整方向 + if target_talk_adjust > 1.0: + adjust_direction = "提高" + elif target_talk_adjust < 1.0: + adjust_direction = "降低" + else: + if current_hour_base_messages <= 5.0 and current_hour_base_users <= 3.0: + adjust_direction = "不调整(历史数据不足)" + else: + adjust_direction = "保持" + logger.info( f"{self.log_prefix} 发言频率调整更新(10分钟窗口): " f"消息数={message_count}, 用户数={user_count}, " f"人均消息数={message_count/user_count if user_count > 0 else 0:.2f}, " f"当前小时基准值(消息:{current_hour_base_messages:.2f}/小时, {current_hour_10min_messages:.2f}/10分钟, " f"用户:{current_hour_base_users:.2f}/小时, {current_hour_10min_users:.2f}/10分钟), " - f"调整值={self.talk_frequency_adjust:.2f}" + f"调整方向={adjust_direction}, 目标调整值={target_talk_adjust:.2f}, 最终调整值={self.talk_frequency_adjust:.2f}" ) except Exception as e: @@ -315,24 +341,34 @@ class FrequencyControl: current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 current_hour_10min_users = current_hour_base_users / 6 - # 专注度调整逻辑:人多话多时提高专注度 - if user_count > 0 and current_hour_10min_users > 0: - # 计算用户活跃度比率(基于10分钟数据) - user_ratio = user_count / current_hour_10min_users - # 计算消息活跃度比率(基于10分钟数据) - message_ratio = message_count / current_hour_10min_messages if current_hour_10min_messages > 0 else 1.0 - - # 如果用户多且消息多,提高专注度 - if user_ratio > 1.2 and message_ratio > 1.2: - # 人多话多:提高专注度,消耗更多LLM资源但回复更精准 - target_focus_adjust = min(self.max_adjust, (user_ratio + message_ratio) / 2) - elif user_ratio > 1.5: - # 用户特别多:适度提高专注度 - target_focus_adjust = min(self.max_adjust, 1.0 + (user_ratio - 1.0) * 0.3) + # 专注度调整逻辑:根据活跃度双向调整 + # 检查是否有足够的数据进行分析 + if user_count > 0 and current_hour_10min_users > 0 and message_count >= 2: + # 检查历史基准值是否有效(不是默认值) + if current_hour_base_messages > 5.0 or current_hour_base_users > 3.0: + # 计算用户活跃度比率(基于10分钟数据) + user_ratio = user_count / current_hour_10min_users + # 计算消息活跃度比率(基于10分钟数据) + message_ratio = message_count / current_hour_10min_messages if current_hour_10min_messages > 0 else 1.0 + + # 双向调整逻辑 + if user_ratio > 1.3 and message_ratio > 1.3: + # 活跃度很高:提高专注度,消耗更多LLM资源但回复更精准 + target_focus_adjust = min(self.max_adjust, (user_ratio + message_ratio) / 2) + elif user_ratio > 1.1 and message_ratio > 1.1: + # 活跃度较高:适度提高专注度 + target_focus_adjust = min(self.max_adjust, 1.0 + (user_ratio + message_ratio - 2.0) * 0.2) + elif user_ratio < 0.7 or message_ratio < 0.7: + # 活跃度很低:降低专注度,节省LLM资源 + target_focus_adjust = max(self.min_adjust, min(user_ratio, message_ratio)) + else: + # 正常情况:保持默认专注度 + target_focus_adjust = 1.0 else: - # 正常情况:保持默认专注度 + # 历史基准值不足,不调整 target_focus_adjust = 1.0 else: + # 数据不足:不调整 target_focus_adjust = 1.0 # 限制调整范围 @@ -348,6 +384,17 @@ class FrequencyControl: current_hour_10min_messages = current_hour_base_messages / 6 # 1小时 = 6个10分钟 current_hour_10min_users = current_hour_base_users / 6 + # 判断调整方向 + if target_focus_adjust > 1.0: + adjust_direction = "提高" + elif target_focus_adjust < 1.0: + adjust_direction = "降低" + else: + if current_hour_base_messages <= 5.0 and current_hour_base_users <= 3.0: + adjust_direction = "不调整(历史数据不足)" + else: + adjust_direction = "保持" + logger.info( f"{self.log_prefix} 专注度调整更新(10分钟窗口): " f"消息数={message_count}, 用户数={user_count}, " @@ -355,7 +402,7 @@ class FrequencyControl: f"消息比率={message_count/current_hour_10min_messages if current_hour_10min_messages > 0 else 0:.2f}, " f"当前小时基准值(消息:{current_hour_base_messages:.2f}/小时, {current_hour_10min_messages:.2f}/10分钟, " f"用户:{current_hour_base_users:.2f}/小时, {current_hour_10min_users:.2f}/10分钟), " - f"调整值={self.focus_value_adjust:.2f}" + f"调整方向={adjust_direction}, 目标调整值={target_focus_adjust:.2f}, 最终调整值={self.focus_value_adjust:.2f}" ) except Exception as e: From 2ea686a4f87746a014e100dea0925ecf41e7ed4d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 31 Aug 2025 15:43:50 +0800 Subject: [PATCH 18/18] Update frequency_control.py --- .../frequency_control/frequency_control.py | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/src/chat/frequency_control/frequency_control.py b/src/chat/frequency_control/frequency_control.py index 5b16f8bb..a71a171f 100644 --- a/src/chat/frequency_control/frequency_control.py +++ b/src/chat/frequency_control/frequency_control.py @@ -21,6 +21,7 @@ class FrequencyControl: - 统一标准:两个调整都使用10分钟窗口,确保逻辑一致性和响应速度 - 双向调整:根据活跃度高低,既能提高也能降低频率和专注度 - 数据充足性检查:当历史数据不足50条时,不更新基准值;当基准值为默认值时,不进行动态调整 + - 基准值更新:直接使用新计算的周均值,无平滑更新 """ def __init__(self, chat_id: str): @@ -59,7 +60,7 @@ class FrequencyControl: # 历史数据相关参数 self._last_historical_update = 0 - self._historical_update_interval = 3600 # 每小时更新一次历史基准值 + self._historical_update_interval = 600 # 每十分钟更新一次历史基准值 self._historical_days = 7 # 使用最近7天的数据计算基准值 # 按小时统计的历史基准值 @@ -70,8 +71,8 @@ class FrequencyControl: # 初始化24小时的默认基准值 for hour in range(24): - self._hourly_baseline['messages'][hour] = 5.0 - self._hourly_baseline['users'][hour] = 3.0 + self._hourly_baseline['messages'][hour] = 0.0 + self._hourly_baseline['users'][hour] = 0.0 def _update_historical_baseline(self): """ @@ -117,27 +118,22 @@ class FrequencyControl: for hour in range(24): # 计算该小时的平均消息数(一周内该小时的总消息数 / 7天) total_messages = len(hourly_stats[hour]['messages']) - avg_messages = total_messages / self._historical_days - - # 计算该小时的平均用户数(一周内该小时的总用户数 / 7天) total_users = len(hourly_stats[hour]['users']) - avg_users = total_users / self._historical_days - # 使用平滑更新更新基准值 - self._hourly_baseline['messages'][hour] = ( - self._hourly_baseline['messages'][hour] * 0.7 + avg_messages * 0.3 - ) - self._hourly_baseline['users'][hour] = ( - self._hourly_baseline['users'][hour] * 0.7 + avg_users * 0.3 - ) - - # 确保基准值不为0 - self._hourly_baseline['messages'][hour] = max(1.0, self._hourly_baseline['messages'][hour]) - self._hourly_baseline['users'][hour] = max(1.0, self._hourly_baseline['users'][hour]) + # 只计算有消息的时段,没有消息的时段设为0 + if total_messages > 0: + avg_messages = total_messages / self._historical_days + avg_users = total_users / self._historical_days + self._hourly_baseline['messages'][hour] = avg_messages + self._hourly_baseline['users'][hour] = avg_users + else: + # 没有消息的时段设为0,表示该时段不活跃 + self._hourly_baseline['messages'][hour] = 0.0 + self._hourly_baseline['users'][hour] = 0.0 - # 更新整体基准值(用于兼容性) - overall_avg_messages = sum(self._hourly_baseline['messages'].values()) / 24 - overall_avg_users = sum(self._hourly_baseline['users'].values()) / 24 + # 更新整体基准值(用于兼容性)- 基于原始数据计算,不受max(1.0)限制影响 + overall_avg_messages = sum(len(hourly_stats[hour]['messages']) for hour in range(24)) / (24 * self._historical_days) + overall_avg_users = sum(len(hourly_stats[hour]['users']) for hour in range(24)) / (24 * self._historical_days) self.base_message_count = overall_avg_messages self.base_user_count = overall_avg_users @@ -249,8 +245,8 @@ class FrequencyControl: # 发言频率调整逻辑:根据活跃度双向调整 # 检查是否有足够的数据进行分析 if user_count > 0 and message_count >= 2: # 至少需要2条消息才能进行有意义的分析 - # 检查历史基准值是否有效(不是默认值) - if current_hour_base_messages > 5.0 or current_hour_base_users > 3.0: + # 检查历史基准值是否有效(该时段有活跃度) + if current_hour_base_messages > 0.0 and current_hour_base_users > 0.0: # 计算人均消息数(10分钟窗口) messages_per_user = message_count / user_count # 使用当前小时每10分钟的基准人均消息数 @@ -288,18 +284,16 @@ class FrequencyControl: elif target_talk_adjust < 1.0: adjust_direction = "降低" else: - if current_hour_base_messages <= 5.0 and current_hour_base_users <= 3.0: - adjust_direction = "不调整(历史数据不足)" + if current_hour_base_messages <= 0.0 or current_hour_base_users <= 0.0: + adjust_direction = "不调整(该时段无活跃度)" else: adjust_direction = "保持" logger.info( - f"{self.log_prefix} 发言频率调整更新(10分钟窗口): " - f"消息数={message_count}, 用户数={user_count}, " - f"人均消息数={message_count/user_count if user_count > 0 else 0:.2f}, " - f"当前小时基准值(消息:{current_hour_base_messages:.2f}/小时, {current_hour_10min_messages:.2f}/10分钟, " - f"用户:{current_hour_base_users:.2f}/小时, {current_hour_10min_users:.2f}/10分钟), " - f"调整方向={adjust_direction}, 目标调整值={target_talk_adjust:.2f}, 最终调整值={self.talk_frequency_adjust:.2f}" + f"{self.log_prefix} 发言频率调整: " + f"当前: {message_count}消息/{user_count}用户, 人均: {message_count/user_count if user_count > 0 else 0:.2f}消息/用户, " + f"基准: {current_hour_10min_messages:.2f}消息/{current_hour_10min_users:.2f}用户,人均:{current_hour_10min_messages/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}消息/用户, " + f"调整: {adjust_direction} → {target_talk_adjust:.2f} → {self.talk_frequency_adjust:.2f}" ) except Exception as e: @@ -344,8 +338,8 @@ class FrequencyControl: # 专注度调整逻辑:根据活跃度双向调整 # 检查是否有足够的数据进行分析 if user_count > 0 and current_hour_10min_users > 0 and message_count >= 2: - # 检查历史基准值是否有效(不是默认值) - if current_hour_base_messages > 5.0 or current_hour_base_users > 3.0: + # 检查历史基准值是否有效(该时段有活跃度) + if current_hour_base_messages > 0.0 and current_hour_base_users > 0.0: # 计算用户活跃度比率(基于10分钟数据) user_ratio = user_count / current_hour_10min_users # 计算消息活跃度比率(基于10分钟数据) @@ -390,19 +384,17 @@ class FrequencyControl: elif target_focus_adjust < 1.0: adjust_direction = "降低" else: - if current_hour_base_messages <= 5.0 and current_hour_base_users <= 3.0: - adjust_direction = "不调整(历史数据不足)" + if current_hour_base_messages <= 0.0 or current_hour_base_users <= 0.0: + adjust_direction = "不调整(该时段无活跃度)" else: adjust_direction = "保持" logger.info( - f"{self.log_prefix} 专注度调整更新(10分钟窗口): " - f"消息数={message_count}, 用户数={user_count}, " - f"用户比率={user_count/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}, " - f"消息比率={message_count/current_hour_10min_messages if current_hour_10min_messages > 0 else 0:.2f}, " - f"当前小时基准值(消息:{current_hour_base_messages:.2f}/小时, {current_hour_10min_messages:.2f}/10分钟, " - f"用户:{current_hour_base_users:.2f}/小时, {current_hour_10min_users:.2f}/10分钟), " - f"调整方向={adjust_direction}, 目标调整值={target_focus_adjust:.2f}, 最终调整值={self.focus_value_adjust:.2f}" + f"{self.log_prefix} 专注度调整(10分钟): " + f"当前: {message_count}消息/{user_count}用户,人均:{message_count/user_count if user_count > 0 else 0:.2f}消息/用户, " + f"基准: {current_hour_10min_messages:.2f}消息/{current_hour_10min_users:.2f}用户,人均:{current_hour_10min_messages/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}消息/用户, " + f"比率: 用户{user_count/current_hour_10min_users if current_hour_10min_users > 0 else 0:.2f}x, 消息{message_count/current_hour_10min_messages if current_hour_10min_messages > 0 else 0:.2f}x, " + f"调整: {adjust_direction} → {target_focus_adjust:.2f} → {self.focus_value_adjust:.2f}" ) except Exception as e: