From 0e2465075e1b7167ffdefc0cc71ff26ccf34e22c Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Sun, 1 Jun 2025 18:21:34 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0vtb=5Faction?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=90=9E=E6=B6=88=E6=81=AF=EF=BC=8C?= =?UTF-8?q?ruff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/070configexe.py | 300 ++++++++++-------- src/chat/focus_chat/heartFC_Cycleinfo.py | 39 ++- src/chat/focus_chat/heartFC_chat.py | 18 +- .../focus_chat/planners/modify_actions.py | 25 +- src/chat/focus_chat/planners/planner.py | 23 +- .../observation/actions_observation.py | 2 +- .../observation/chatting_observation.py | 4 +- .../observation/hfcloop_observation.py | 20 +- .../heart_flow/observation/observation.py | 2 +- .../observation/structure_observation.py | 2 +- .../observation/working_observation.py | 8 +- src/config/official_configs.py | 8 +- src/llm_models/utils_model.py | 2 +- src/plugins/vtb_action/__init__.py | 0 src/plugins/vtb_action/actions/__init__.py | 1 + src/plugins/vtb_action/actions/vtb_action.py | 74 +++++ 16 files changed, 326 insertions(+), 202 deletions(-) create mode 100644 src/plugins/vtb_action/__init__.py create mode 100644 src/plugins/vtb_action/actions/__init__.py create mode 100644 src/plugins/vtb_action/actions/vtb_action.py diff --git a/scripts/070configexe.py b/scripts/070configexe.py index be1f56e42..d66e857c0 100644 --- a/scripts/070configexe.py +++ b/scripts/070configexe.py @@ -8,6 +8,7 @@ import threading import time import sys + class ConfigEditor: def __init__(self, root): self.root = root @@ -21,10 +22,10 @@ class ConfigEditor: # 加载配置 self.load_config() - + # 加载环境变量 self.load_env_vars() - + # 自动保存相关 self.last_save_time = time.time() self.save_timer = None @@ -114,40 +115,40 @@ class ConfigEditor: env_path = self.config.get("inner", {}).get("env_file", ".env") if not os.path.isabs(env_path): env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), env_path) - + if not os.path.exists(env_path): print(f"环境文件不存在: {env_path}") return - + # 读取环境文件 - with open(env_path, 'r', encoding='utf-8') as f: + with open(env_path, "r", encoding="utf-8") as f: env_content = f.read() - + # 解析环境变量 env_vars = {} - for line in env_content.split('\n'): + for line in env_content.split("\n"): line = line.strip() - if not line or line.startswith('#'): + if not line or line.startswith("#"): continue - - if '=' in line: - key, value = line.split('=', 1) + + if "=" in line: + key, value = line.split("=", 1) key = key.strip() value = value.strip() - + # 检查是否是目标变量 - if key.endswith('_BASE_URL') or key.endswith('_KEY'): + if key.endswith("_BASE_URL") or key.endswith("_KEY"): # 提取前缀(去掉_BASE_URL或_KEY) - prefix = key[:-9] if key.endswith('_BASE_URL') else key[:-4] + prefix = key[:-9] if key.endswith("_BASE_URL") else key[:-4] if prefix not in env_vars: env_vars[prefix] = {} env_vars[prefix][key] = value - + # 将解析的环境变量添加到配置中 - if 'env_vars' not in self.config: - self.config['env_vars'] = {} - self.config['env_vars'].update(env_vars) - + if "env_vars" not in self.config: + self.config["env_vars"] = {} + self.config["env_vars"].update(env_vars) + except Exception as e: print(f"加载环境变量失败: {str(e)}") @@ -156,11 +157,11 @@ class ConfigEditor: version = self.config.get("inner", {}).get("version", "未知版本") version_frame = ttk.Frame(self.main_frame) version_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) - + # 添加配置按钮 config_button = ttk.Button(version_frame, text="配置路径", command=self.open_path_config) config_button.pack(side=tk.LEFT, padx=5) - + version_label = ttk.Label(version_frame, text=f"麦麦版本:{version}", font=("微软雅黑", 10, "bold")) version_label.pack(side=tk.LEFT, padx=5) @@ -175,13 +176,22 @@ class ConfigEditor: # 添加快捷设置节 self.tree.insert("", "end", text="快捷设置", values=("quick_settings",)) - + # 添加env_vars节,显示为"配置你的模型APIKEY" self.tree.insert("", "end", text="配置你的模型APIKEY", values=("env_vars",)) - + # 只显示bot_config.toml实际存在的section for section in self.config: - if section not in ("inner", "env_vars", "telemetry", "experimental", "maim_message", "keyword_reaction", "message_receive", "relationship"): + if section not in ( + "inner", + "env_vars", + "telemetry", + "experimental", + "maim_message", + "keyword_reaction", + "message_receive", + "relationship", + ): section_trans = self.translations.get("sections", {}).get(section, {}) section_name = section_trans.get("name", section) self.tree.insert("", "end", text=section_name, values=(section,)) @@ -196,7 +206,7 @@ class ConfigEditor: # 创建编辑区标题 # self.editor_title = ttk.Label(self.editor_frame, text="") # self.editor_title.pack(fill=tk.X) - + # 创建编辑区内容 self.editor_content = ttk.Frame(self.editor_frame) self.editor_content.pack(fill=tk.BOTH, expand=True) @@ -245,15 +255,15 @@ class ConfigEditor: # --- 修改开始: 改进翻译查找逻辑 --- full_config_path_key = ".".join(path + [key]) # 例如 "chinese_typo.enable" - + model_item_translations = { "name": ("模型名称", "模型的唯一标识或名称"), "provider": ("模型提供商", "模型API的提供商"), "pri_in": ("输入价格", "模型输入的价格/消耗"), "pri_out": ("输出价格", "模型输出的价格/消耗"), - "temp": ("模型温度", "控制模型输出的多样性") + "temp": ("模型温度", "控制模型输出的多样性"), } - + item_name_to_display = key # 默认显示原始键名 item_desc_to_display = "" # 默认无描述 @@ -294,9 +304,15 @@ class ConfigEditor: # 判断parent是不是self.content_frame if parent == self.content_frame: # 主界面 - if hasattr(self, 'current_section') and self.current_section and self.current_section != "quick_settings": - self.create_section_widgets(parent, self.current_section, self.config[self.current_section], [self.current_section]) - elif hasattr(self, 'current_section') and self.current_section == "quick_settings": + if ( + hasattr(self, "current_section") + and self.current_section + and self.current_section != "quick_settings" + ): + self.create_section_widgets( + parent, self.current_section, self.config[self.current_section], [self.current_section] + ) + elif hasattr(self, "current_section") and self.current_section == "quick_settings": self.create_quick_settings_widgets() else: # 弹窗Tab @@ -318,15 +334,17 @@ class ConfigEditor: desc_row = 1 if item_desc_to_display: desc_label = ttk.Label(frame, text=item_desc_to_display, foreground="gray", font=("微软雅黑", 10)) - desc_label.grid(row=desc_row, column=0, columnspan=content_col_offset_for_star + 1, sticky=tk.W, padx=5, pady=(0, 4)) - widget_row = desc_row + 1 # 内容控件在描述下方 + desc_label.grid( + row=desc_row, column=0, columnspan=content_col_offset_for_star + 1, sticky=tk.W, padx=5, pady=(0, 4) + ) + widget_row = desc_row + 1 # 内容控件在描述下方 else: widget_row = desc_row # 内容控件直接在第二行 # 配置内容控件(第三行或第二行) if path[0] == "inner": value_label = ttk.Label(frame, text=str(value), font=("微软雅黑", 16)) - value_label.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star +1, sticky=tk.W, padx=5) + value_label.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star + 1, sticky=tk.W, padx=5) return if isinstance(value, bool): @@ -341,7 +359,7 @@ class ConfigEditor: # 数字使用数字输入框 var = tk.StringVar(value=str(value)) entry = ttk.Entry(frame, textvariable=var, font=("微软雅黑", 16)) - entry.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star +1, sticky=tk.W+tk.E, padx=5) + entry.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star + 1, sticky=tk.W + tk.E, padx=5) var.trace_add("write", lambda *args: self.on_value_changed()) self.widgets[tuple(path + [key])] = var widget_type = "number" @@ -380,7 +398,7 @@ class ConfigEditor: else: # 其他类型(字符串等)使用普通文本框 var = tk.StringVar(value=str(value)) - + # 特殊处理provider字段 full_path = ".".join(path + [key]) if key == "provider" and full_path.startswith("model."): @@ -397,37 +415,45 @@ class ConfigEditor: if f"{prefix}_BASE_URL" in values and f"{prefix}_KEY" in values: providers.append(prefix) # print(f"添加provider: {prefix}") - + # print(f"最终providers列表: {providers}") if providers: # 创建模型名称标签(大字体) - model_name = var.get() if var.get() else providers[0] - section_translations = { - "model.utils": "麦麦组件模型", - "model.utils_small": "小型麦麦组件模型", - "model.memory_summary": "记忆概括模型", - "model.vlm": "图像识别模型", - "model.embedding": "嵌入模型", - "model.normal_chat_1": "普通聊天:主要聊天模型", - "model.normal_chat_2": "普通聊天:次要聊天模型", - "model.focus_working_memory": "专注模式:工作记忆模型", - "model.focus_chat_mind": "专注模式:聊天思考模型", - "model.focus_tool_use": "专注模式:工具调用模型", - "model.focus_planner": "专注模式:决策模型", - "model.focus_expressor": "专注模式:表达器模型", - "model.focus_self_recognize": "专注模式:自我识别模型" - } + # model_name = var.get() if var.get() else providers[0] + # section_translations = { + # "model.utils": "麦麦组件模型", + # "model.utils_small": "小型麦麦组件模型", + # "model.memory_summary": "记忆概括模型", + # "model.vlm": "图像识别模型", + # "model.embedding": "嵌入模型", + # "model.normal_chat_1": "普通聊天:主要聊天模型", + # "model.normal_chat_2": "普通聊天:次要聊天模型", + # "model.focus_working_memory": "专注模式:工作记忆模型", + # "model.focus_chat_mind": "专注模式:聊天思考模型", + # "model.focus_tool_use": "专注模式:工具调用模型", + # "model.focus_planner": "专注模式:决策模型", + # "model.focus_expressor": "专注模式:表达器模型", + # "model.focus_self_recognize": "专注模式:自我识别模型" + # } # 获取当前节的名称 # current_section = ".".join(path[:-1]) # 去掉最后一个key # section_name = section_translations.get(current_section, current_section) - + # 创建节名称标签(大字体) # section_label = ttk.Label(frame, text="11", font=("微软雅黑", 24, "bold")) # section_label.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star +1, sticky=tk.W, padx=5, pady=(0, 5)) - + # 创建下拉菜单(小字体) - combo = ttk.Combobox(frame, textvariable=var, values=providers, font=("微软雅黑", 12), state="readonly") - combo.grid(row=widget_row + 1, column=0, columnspan=content_col_offset_for_star +1, sticky=tk.W+tk.E, padx=5) + combo = ttk.Combobox( + frame, textvariable=var, values=providers, font=("微软雅黑", 12), state="readonly" + ) + combo.grid( + row=widget_row + 1, + column=0, + columnspan=content_col_offset_for_star + 1, + sticky=tk.W + tk.E, + padx=5, + ) combo.bind("<>", lambda e: self.on_value_changed()) self.widgets[tuple(path + [key])] = var widget_type = "provider" @@ -436,14 +462,18 @@ class ConfigEditor: # 如果没有可用的provider,使用普通文本框 # print(f"没有可用的provider,使用普通文本框") entry = ttk.Entry(frame, textvariable=var, font=("微软雅黑", 16)) - entry.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star +1, sticky=tk.W+tk.E, padx=5) + entry.grid( + row=widget_row, column=0, columnspan=content_col_offset_for_star + 1, sticky=tk.W + tk.E, padx=5 + ) var.trace_add("write", lambda *args: self.on_value_changed()) self.widgets[tuple(path + [key])] = var widget_type = "text" else: # 普通文本框 entry = ttk.Entry(frame, textvariable=var, font=("微软雅黑", 16)) - entry.grid(row=widget_row, column=0, columnspan=content_col_offset_for_star +1, sticky=tk.W+tk.E, padx=5) + entry.grid( + row=widget_row, column=0, columnspan=content_col_offset_for_star + 1, sticky=tk.W + tk.E, padx=5 + ) var.trace_add("write", lambda *args: self.on_value_changed()) self.widgets[tuple(path + [key])] = var widget_type = "text" @@ -468,7 +498,7 @@ class ConfigEditor: "model.focus_tool_use": "工具调用模型", "model.focus_planner": "决策模型", "model.focus_expressor": "表达器模型", - "model.focus_self_recognize": "自我识别模型" + "model.focus_self_recognize": "自我识别模型", } section_trans = self.translations.get("sections", {}).get(full_section_path, {}) section_name = section_trans.get("name") or section_translations.get(full_section_path) or section @@ -490,7 +520,7 @@ class ConfigEditor: else: desc_label = ttk.Label(section_frame, text=section_desc, foreground="gray", font=("微软雅黑", 10)) desc_label.pack(side=tk.LEFT, padx=5) - + # 为每个配置项创建对应的控件 for key, value in data.items(): if isinstance(value, dict): @@ -518,7 +548,7 @@ class ConfigEditor: section = self.tree.item(selection[0])["values"][0] # 使用values中的原始节名 self.current_section = section - + # 清空编辑器 for widget in self.content_frame.winfo_children(): widget.destroy() @@ -557,7 +587,7 @@ class ConfigEditor: # 创建描述标签 if setting.get("description"): - desc_label = ttk.Label(frame, text=setting['description'], foreground="gray", font=("微软雅黑", 10)) + desc_label = ttk.Label(frame, text=setting["description"], foreground="gray", font=("微软雅黑", 10)) desc_label.pack(fill=tk.X, padx=5, pady=(0, 2)) # 根据类型创建不同的控件 @@ -575,14 +605,14 @@ class ConfigEditor: value = str(value) if value is not None else "" var = tk.StringVar(value=value) entry = ttk.Entry(frame, textvariable=var, width=40, font=("微软雅黑", 12)) - entry.pack(fill=tk.X, padx=5, pady=(0,5)) + entry.pack(fill=tk.X, padx=5, pady=(0, 5)) var.trace_add("write", lambda *args, p=path, v=var: self.on_quick_setting_changed(p, v)) elif setting_type == "number": value = str(value) if value is not None else "0" var = tk.StringVar(value=value) entry = ttk.Entry(frame, textvariable=var, width=10, font=("微软雅黑", 12)) - entry.pack(fill=tk.X, padx=5, pady=(0,5)) + entry.pack(fill=tk.X, padx=5, pady=(0, 5)) var.trace_add("write", lambda *args, p=path, v=var: self.on_quick_setting_changed(p, v)) elif setting_type == "list": @@ -659,7 +689,7 @@ class ConfigEditor: # 获取所有控件的值 for path, widget in self.widgets.items(): # 跳过 env_vars 的控件赋值(只用于.env,不写回config) - if len(path) >= 2 and path[0] == 'env_vars': + if len(path) >= 2 and path[0] == "env_vars": continue value = self.get_widget_value(widget) current = self.config @@ -669,11 +699,11 @@ class ConfigEditor: current[final_key] = value # === 只保存 TOML,不包含 env_vars === - env_vars = self.config.pop('env_vars', None) + env_vars = self.config.pop("env_vars", None) with open(self.config_path, "wb") as f: tomli_w.dump(self.config, f) if env_vars is not None: - self.config['env_vars'] = env_vars + self.config["env_vars"] = env_vars # === 保存 env_vars 到 .env 文件(只覆盖特定key,其他内容保留) === env_path = self.editor_config["config"].get("env_file", ".env") @@ -687,7 +717,7 @@ class ConfigEditor: # 2. 收集所有目标key的新值(直接从widgets取) new_env_dict = {} for path, widget in self.widgets.items(): - if len(path) == 2 and path[0] == 'env_vars': + if len(path) == 2 and path[0] == "env_vars": k = path[1] if k.endswith("_BASE_URL") or k.endswith("_KEY"): new_env_dict[k] = self.get_widget_value(widget) @@ -715,15 +745,15 @@ class ConfigEditor: # === 保存完 .env 后,同步 widgets 的值回 self.config['env_vars'] === for path, widget in self.widgets.items(): - if len(path) == 2 and path[0] == 'env_vars': + if len(path) == 2 and path[0] == "env_vars": prefix_key = path[1] if prefix_key.endswith("_BASE_URL") or prefix_key.endswith("_KEY"): prefix = prefix_key[:-9] if prefix_key.endswith("_BASE_URL") else prefix_key[:-4] - if 'env_vars' not in self.config: - self.config['env_vars'] = {} - if prefix not in self.config['env_vars']: - self.config['env_vars'][prefix] = {} - self.config['env_vars'][prefix][prefix_key] = self.get_widget_value(widget) + if "env_vars" not in self.config: + self.config["env_vars"] = {} + if prefix not in self.config["env_vars"]: + self.config["env_vars"][prefix] = {} + self.config["env_vars"][prefix][prefix_key] = self.get_widget_value(widget) self.last_save_time = time.time() self.pending_save = False @@ -862,62 +892,60 @@ class ConfigEditor: """创建环境变量组""" frame = ttk.Frame(parent) frame.pack(fill=tk.X, padx=5, pady=2) - + # 创建组标题 title_frame = ttk.Frame(frame) title_frame.pack(fill=tk.X, pady=(5, 0)) - + title_label = ttk.Label(title_frame, text=f"API配置组: {prefix}", font=("微软雅黑", 16, "bold")) title_label.pack(side=tk.LEFT, padx=5) - + # 删除按钮 - del_button = ttk.Button(title_frame, text="删除组", - command=lambda: self.delete_env_var_group(prefix)) + del_button = ttk.Button(title_frame, text="删除组", command=lambda: self.delete_env_var_group(prefix)) del_button.pack(side=tk.RIGHT, padx=5) - + # 创建BASE_URL输入框 base_url_frame = ttk.Frame(frame) base_url_frame.pack(fill=tk.X, padx=5, pady=2) - + base_url_label = ttk.Label(base_url_frame, text="BASE_URL:", font=("微软雅黑", 12)) base_url_label.pack(side=tk.LEFT, padx=5) - + base_url_var = tk.StringVar(value=values.get(f"{prefix}_BASE_URL", "")) base_url_entry = ttk.Entry(base_url_frame, textvariable=base_url_var, font=("微软雅黑", 12)) base_url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) base_url_var.trace_add("write", lambda *args: self.on_value_changed()) - + # 创建KEY输入框 key_frame = ttk.Frame(frame) key_frame.pack(fill=tk.X, padx=5, pady=2) - + key_label = ttk.Label(key_frame, text="API KEY:", font=("微软雅黑", 12)) key_label.pack(side=tk.LEFT, padx=5) - + key_var = tk.StringVar(value=values.get(f"{prefix}_KEY", "")) key_entry = ttk.Entry(key_frame, textvariable=key_var, font=("微软雅黑", 12)) key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) key_var.trace_add("write", lambda *args: self.on_value_changed()) - + # 存储变量引用 self.widgets[tuple(path + [f"{prefix}_BASE_URL"])] = base_url_var self.widgets[tuple(path + [f"{prefix}_KEY"])] = key_var - + # 添加分隔线 - separator = ttk.Separator(frame, orient='horizontal') + separator = ttk.Separator(frame, orient="horizontal") separator.pack(fill=tk.X, pady=5) def create_env_vars_section(self, parent: ttk.Frame) -> None: """创建环境变量编辑区""" # 创建添加新组的按钮 - add_button = ttk.Button(parent, text="添加新的API配置组", - command=self.add_new_env_var_group) + add_button = ttk.Button(parent, text="添加新的API配置组", command=self.add_new_env_var_group) add_button.pack(pady=10) - + # 创建现有组的编辑区 - if 'env_vars' in self.config: - for prefix, values in self.config['env_vars'].items(): - self.create_env_var_group(parent, prefix, values, ['env_vars']) + if "env_vars" in self.config: + for prefix, values in self.config["env_vars"].items(): + self.create_env_var_group(parent, prefix, values, ["env_vars"]) def add_new_env_var_group(self): """添加新的环境变量组""" @@ -925,42 +953,39 @@ class ConfigEditor: dialog = tk.Toplevel(self.root) dialog.title("添加新的API配置组") dialog.geometry("400x200") - + # 创建输入框架 frame = ttk.Frame(dialog, padding="10") frame.pack(fill=tk.BOTH, expand=True) - + # 前缀输入 prefix_label = ttk.Label(frame, text="API前缀名称:", font=("微软雅黑", 12)) prefix_label.pack(pady=5) - + prefix_var = tk.StringVar() prefix_entry = ttk.Entry(frame, textvariable=prefix_var, font=("微软雅黑", 12)) prefix_entry.pack(fill=tk.X, pady=5) - + # 确认按钮 def on_confirm(): prefix = prefix_var.get().strip() if prefix: - if 'env_vars' not in self.config: - self.config['env_vars'] = {} - self.config['env_vars'][prefix] = { - f"{prefix}_BASE_URL": "", - f"{prefix}_KEY": "" - } + if "env_vars" not in self.config: + self.config["env_vars"] = {} + self.config["env_vars"][prefix] = {f"{prefix}_BASE_URL": "", f"{prefix}_KEY": ""} # 刷新显示 self.refresh_env_vars_section() self.on_value_changed() dialog.destroy() - + confirm_button = ttk.Button(frame, text="确认", command=on_confirm) confirm_button.pack(pady=10) def delete_env_var_group(self, prefix: str): """删除环境变量组""" if messagebox.askyesno("确认", f"确定要删除 {prefix} 配置组吗?"): - if 'env_vars' in self.config: - del self.config['env_vars'][prefix] + if "env_vars" in self.config: + del self.config["env_vars"][prefix] # 刷新显示 self.refresh_env_vars_section() self.on_value_changed() @@ -971,7 +996,7 @@ class ConfigEditor: for widget in self.content_frame.winfo_children(): widget.destroy() self.widgets.clear() - + # 重新创建编辑区 self.create_env_vars_section(self.content_frame) @@ -980,10 +1005,10 @@ class ConfigEditor: dialog = tk.Toplevel(self.root) dialog.title("高级选项") dialog.geometry("700x800") - + notebook = ttk.Notebook(dialog) notebook.pack(fill=tk.BOTH, expand=True) - + # 遥测栏 if "telemetry" in self.config: telemetry_frame = ttk.Frame(notebook) @@ -1003,7 +1028,9 @@ class ConfigEditor: if "message_receive" in self.config: recv_frame = ttk.Frame(notebook) notebook.add(recv_frame, text="消息接收") - self.create_section_widgets(recv_frame, "message_receive", self.config["message_receive"], ["message_receive"]) + self.create_section_widgets( + recv_frame, "message_receive", self.config["message_receive"], ["message_receive"] + ) # 关系栏 if "relationship" in self.config: rel_frame = ttk.Frame(notebook) @@ -1015,96 +1042,95 @@ class ConfigEditor: dialog = tk.Toplevel(self.root) dialog.title("配置路径") dialog.geometry("600x200") - + # 创建输入框架 frame = ttk.Frame(dialog, padding="10") frame.pack(fill=tk.BOTH, expand=True) - + # bot_config.toml路径配置 bot_config_frame = ttk.Frame(frame) bot_config_frame.pack(fill=tk.X, pady=5) - + bot_config_label = ttk.Label(bot_config_frame, text="bot_config.toml路径:", font=("微软雅黑", 12)) bot_config_label.pack(side=tk.LEFT, padx=5) - + bot_config_var = tk.StringVar(value=self.config_path) bot_config_entry = ttk.Entry(bot_config_frame, textvariable=bot_config_var, font=("微软雅黑", 12)) bot_config_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) - + def apply_config(): new_bot_config_path = bot_config_var.get().strip() new_env_path = env_var.get().strip() - + if not new_bot_config_path or not new_env_path: messagebox.showerror("错误", "路径不能为空") return - + if not os.path.exists(new_bot_config_path): messagebox.showerror("错误", "bot_config.toml文件不存在") return - + # 更新配置 self.config_path = new_bot_config_path self.editor_config["config"]["bot_config_path"] = new_bot_config_path self.editor_config["config"]["env_file"] = new_env_path - + # 保存编辑器配置 config_path = os.path.join(os.path.dirname(__file__), "configexe.toml") with open(config_path, "wb") as f: tomli_w.dump(self.editor_config, f) - + # 重新加载配置 self.load_config() self.load_env_vars() - + # 刷新显示 self.refresh_config() - + messagebox.showinfo("成功", "路径配置已更新,程序将重新启动") dialog.destroy() - + # 重启程序 self.root.quit() - os.execv(sys.executable, ['python'] + sys.argv) - + os.execv(sys.executable, ["python"] + sys.argv) + def browse_bot_config(): file_path = filedialog.askopenfilename( - title="选择bot_config.toml文件", - filetypes=[("TOML文件", "*.toml"), ("所有文件", "*.*")] + title="选择bot_config.toml文件", filetypes=[("TOML文件", "*.toml"), ("所有文件", "*.*")] ) if file_path: bot_config_var.set(file_path) apply_config() - + browse_bot_config_btn = ttk.Button(bot_config_frame, text="浏览", command=browse_bot_config) browse_bot_config_btn.pack(side=tk.LEFT, padx=5) - + # .env路径配置 env_frame = ttk.Frame(frame) env_frame.pack(fill=tk.X, pady=5) - + env_label = ttk.Label(env_frame, text=".env路径:", font=("微软雅黑", 12)) env_label.pack(side=tk.LEFT, padx=5) - + env_path = self.editor_config["config"].get("env_file", ".env") if not os.path.isabs(env_path): env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), env_path) env_var = tk.StringVar(value=env_path) env_entry = ttk.Entry(env_frame, textvariable=env_var, font=("微软雅黑", 12)) env_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) - + def browse_env(): file_path = filedialog.askopenfilename( - title="选择.env文件", - filetypes=[("环境变量文件", "*.env"), ("所有文件", "*.*")] + title="选择.env文件", filetypes=[("环境变量文件", "*.env"), ("所有文件", "*.*")] ) if file_path: env_var.set(file_path) apply_config() - + browse_env_btn = ttk.Button(env_frame, text="浏览", command=browse_env) browse_env_btn.pack(side=tk.LEFT, padx=5) + def main(): root = tk.Tk() _app = ConfigEditor(root) diff --git a/src/chat/focus_chat/heartFC_Cycleinfo.py b/src/chat/focus_chat/heartFC_Cycleinfo.py index 5a9a52fd8..129a58af8 100644 --- a/src/chat/focus_chat/heartFC_Cycleinfo.py +++ b/src/chat/focus_chat/heartFC_Cycleinfo.py @@ -1,9 +1,10 @@ import time import os -from typing import List, Optional, Dict, Any +from typing import Optional, Dict, Any log_dir = "log/log_cycle_debug/" + class CycleDetail: """循环信息记录类""" @@ -23,35 +24,40 @@ class CycleDetail: def to_dict(self) -> Dict[str, Any]: """将循环信息转换为字典格式""" + def convert_to_serializable(obj, depth=0, seen=None): if seen is None: seen = set() - + # 防止递归过深 if depth > 5: # 降低递归深度限制 return str(obj) - + # 防止循环引用 obj_id = id(obj) if obj_id in seen: return str(obj) seen.add(obj_id) - + try: - if hasattr(obj, 'to_dict'): + if hasattr(obj, "to_dict"): # 对于有to_dict方法的对象,直接调用其to_dict方法 return obj.to_dict() elif isinstance(obj, dict): # 对于字典,只保留基本类型和可序列化的值 - return {k: convert_to_serializable(v, depth + 1, seen) - for k, v in obj.items() - if isinstance(k, (str, int, float, bool))} + return { + k: convert_to_serializable(v, depth + 1, seen) + for k, v in obj.items() + if isinstance(k, (str, int, float, bool)) + } elif isinstance(obj, (list, tuple)): # 对于列表和元组,只保留可序列化的元素 - return [convert_to_serializable(item, depth + 1, seen) - for item in obj - if not isinstance(item, (dict, list, tuple)) or - isinstance(item, (str, int, float, bool, type(None)))] + return [ + convert_to_serializable(item, depth + 1, seen) + for item in obj + if not isinstance(item, (dict, list, tuple)) + or isinstance(item, (str, int, float, bool, type(None))) + ] elif isinstance(obj, (str, int, float, bool, type(None))): return obj else: @@ -74,19 +80,19 @@ class CycleDetail: def complete_cycle(self): """完成循环,记录结束时间""" self.end_time = time.time() - + # 处理 prefix,只保留中英文字符 if not self.prefix: self.prefix = "group" else: # 只保留中文和英文字符 - self.prefix = ''.join(char for char in self.prefix if '\u4e00' <= char <= '\u9fff' or char.isascii()) + self.prefix = "".join(char for char in self.prefix if "\u4e00" <= char <= "\u9fff" or char.isascii()) if not self.prefix: self.prefix = "group" - + current_time_minute = time.strftime("%Y%m%d_%H%M", time.localtime()) self.log_cycle_to_file(log_dir + self.prefix + f"/{current_time_minute}_cycle_" + str(self.cycle_id) + ".json") - + def log_cycle_to_file(self, file_path: str): """将循环信息写入文件""" # 如果目录不存在,则创建目录 @@ -95,6 +101,7 @@ class CycleDetail: os.makedirs(dir_name, exist_ok=True) # 写入文件 import json + with open(file_path, "a", encoding="utf-8") as f: f.write(json.dumps(self.to_dict(), ensure_ascii=False) + "\n") diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index c69dea6b7..2fef2a448 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -418,7 +418,9 @@ class HeartFChatting: # 记录耗时 processor_time_costs[processor_name] = duration_since_parallel_start except asyncio.TimeoutError: - logger.info(f"{self.log_prefix} 处理器 {processor_name} 超时(>{global_config.focus_chat.processor_max_time}s),已跳过") + logger.info( + f"{self.log_prefix} 处理器 {processor_name} 超时(>{global_config.focus_chat.processor_max_time}s),已跳过" + ) processor_time_costs[processor_name] = global_config.focus_chat.processor_max_time except Exception as e: logger.error( @@ -462,7 +464,7 @@ class HeartFChatting: } self.all_observations = observations - + with Timer("调整动作", cycle_timers): # 处理特殊的观察 await self.action_modifier.modify_actions(observations=observations) @@ -476,26 +478,24 @@ class HeartFChatting: with Timer("并行回忆和处理", cycle_timers): memory_task = asyncio.create_task(self.memory_activator.activate_memory(observations)) processor_task = asyncio.create_task(self._process_processors(observations, [])) - + # 等待两个任务完成 - running_memorys, (all_plan_info, processor_time_costs) = await asyncio.gather(memory_task, processor_task) + running_memorys, (all_plan_info, processor_time_costs) = await asyncio.gather( + memory_task, processor_task + ) else: # 串行执行 with Timer("回忆", cycle_timers): running_memorys = await self.memory_activator.activate_memory(observations) with Timer("执行 信息处理器", cycle_timers): - all_plan_info, processor_time_costs = await self._process_processors( - observations, running_memorys - ) + all_plan_info, processor_time_costs = await self._process_processors(observations, running_memorys) loop_processor_info = { "all_plan_info": all_plan_info, "processor_time_costs": processor_time_costs, } - - with Timer("规划器", cycle_timers): plan_result = await self.action_planner.plan(all_plan_info, running_memorys) diff --git a/src/chat/focus_chat/planners/modify_actions.py b/src/chat/focus_chat/planners/modify_actions.py index 731fe5f90..6e7afa65f 100644 --- a/src/chat/focus_chat/planners/modify_actions.py +++ b/src/chat/focus_chat/planners/modify_actions.py @@ -30,7 +30,6 @@ class ActionModifier: observations: Optional[List[Observation]] = None, **kwargs: Any, ): - # 处理Observation对象 if observations: # action_info = ActionInfo() @@ -163,22 +162,34 @@ class ActionModifier: if len(last_max_reply_num) >= max_reply_num and all(last_max_reply_num): # 如果最近max_reply_num次都是reply,直接移除 result["remove"].append("reply") - logger.info(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,直接移除") + logger.info( + f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,直接移除" + ) elif len(last_max_reply_num) >= sec_thres_reply_num and all(last_max_reply_num[-sec_thres_reply_num:]): # 如果最近sec_thres_reply_num次都是reply,40%概率移除 if random.random() < 0.4 / global_config.focus_chat.consecutive_replies: result["remove"].append("reply") - logger.info(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.4 / global_config.focus_chat.consecutive_replies}概率移除,移除") + logger.info( + f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.4 / global_config.focus_chat.consecutive_replies}概率移除,移除" + ) else: - logger.debug(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.4 / global_config.focus_chat.consecutive_replies}概率移除,不移除") + logger.debug( + f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.4 / global_config.focus_chat.consecutive_replies}概率移除,不移除" + ) elif len(last_max_reply_num) >= one_thres_reply_num and all(last_max_reply_num[-one_thres_reply_num:]): # 如果最近one_thres_reply_num次都是reply,20%概率移除 if random.random() < 0.2 / global_config.focus_chat.consecutive_replies: result["remove"].append("reply") - logger.info(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.2 / global_config.focus_chat.consecutive_replies}概率移除,移除") + logger.info( + f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.2 / global_config.focus_chat.consecutive_replies}概率移除,移除" + ) else: - logger.debug(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.2 / global_config.focus_chat.consecutive_replies}概率移除,不移除") + logger.debug( + f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,{0.2 / global_config.focus_chat.consecutive_replies}概率移除,不移除" + ) else: - logger.debug(f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,无需移除") + logger.debug( + f"最近{len(last_max_reply_num)}次回复中,有{no_reply_count}次no_reply,{len(last_max_reply_num) - no_reply_count}次reply,无需移除" + ) return result diff --git a/src/chat/focus_chat/planners/planner.py b/src/chat/focus_chat/planners/planner.py index a0b0ccf9c..298da3115 100644 --- a/src/chat/focus_chat/planners/planner.py +++ b/src/chat/focus_chat/planners/planner.py @@ -29,14 +29,6 @@ def init_prompt(): {self_info_block} {extra_info_block} {memory_str} -你需要基于以下信息决定如何参与对话 -这些信息可能会有冲突,请你整合这些信息,并选择一个最合适的action: -{chat_content_block} - -{mind_info_block} -{cycle_info_block} - -请综合分析聊天内容和你看到的新消息,参考聊天规划,选择合适的action: 注意,除了下面动作选项之外,你在群聊里不能做其他任何事情,这是你能力的边界,现在请你选择合适的action: {action_options_text} @@ -46,6 +38,15 @@ def init_prompt(): {moderation_prompt} +你需要基于以下信息决定如何参与对话 +这些信息可能会有冲突,请你整合这些信息,并选择一个最合适的action: +{chat_content_block} + +{mind_info_block} +{cycle_info_block} + +请综合分析聊天内容和你看到的新消息,参考聊天规划,选择合适的action: + 请你以下面格式输出你选择的action: {{ "action": "action_name", @@ -270,7 +271,6 @@ class ActionPlanner: ) -> str: """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: - memory_str = "" if global_config.focus_chat.parallel_processing: memory_str = "" @@ -278,10 +278,7 @@ class ActionPlanner: memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" for running_memory in running_memorys: memory_str += f"{running_memory['topic']}: {running_memory['content']}\n" - - - - + chat_context_description = "你现在正在一个群聊中" chat_target_name = None # Only relevant for private if not is_group_chat and chat_target_info: diff --git a/src/chat/heart_flow/observation/actions_observation.py b/src/chat/heart_flow/observation/actions_observation.py index 6f0cd81c3..6550ddb72 100644 --- a/src/chat/heart_flow/observation/actions_observation.py +++ b/src/chat/heart_flow/observation/actions_observation.py @@ -42,5 +42,5 @@ class ActionObservation: "observe_id": self.observe_id, "last_observe_time": self.last_observe_time, "all_actions": self.all_actions, - "all_using_actions": self.all_using_actions + "all_using_actions": self.all_using_actions, } diff --git a/src/chat/heart_flow/observation/chatting_observation.py b/src/chat/heart_flow/observation/chatting_observation.py index 7e9e562d0..6fd180af5 100644 --- a/src/chat/heart_flow/observation/chatting_observation.py +++ b/src/chat/heart_flow/observation/chatting_observation.py @@ -81,7 +81,7 @@ class ChattingObservation(Observation): "person_list": self.person_list, "oldest_messages_str": self.oldest_messages_str, "compressor_prompt": self.compressor_prompt, - "last_observe_time": self.last_observe_time + "last_observe_time": self.last_observe_time, } async def initialize(self): @@ -89,6 +89,8 @@ class ChattingObservation(Observation): logger.debug(f"初始化observation: self.is_group_chat: {self.is_group_chat}") logger.debug(f"初始化observation: self.chat_target_info: {self.chat_target_info}") initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10) + self.last_observe_time = initial_messages[-1]["time"] if initial_messages else self.last_observe_time + # logger.error(f"初始化observation: initial_messages: {initial_messages}\n\n\n\n{self.last_observe_time}") self.talking_message = initial_messages self.talking_message_str = await build_readable_messages(self.talking_message) diff --git a/src/chat/heart_flow/observation/hfcloop_observation.py b/src/chat/heart_flow/observation/hfcloop_observation.py index 48bf33ede..1e1c7fe07 100644 --- a/src/chat/heart_flow/observation/hfcloop_observation.py +++ b/src/chat/heart_flow/observation/hfcloop_observation.py @@ -39,7 +39,7 @@ class HFCloopObservation: responses_for_prompt = [] cycle_last_reason = "" - + # 检查这最近的活动循环中有多少是连续的文本回复 (从最近的开始看) for cycle in recent_active_cycles: action_type = cycle.loop_plan_info["action_result"]["action_type"] @@ -57,29 +57,33 @@ class HFCloopObservation: action_reasoning_str = f"你选择这个action的原因是:{action_reasoning}" else: action_reasoning_str = "" - + if action_type == "reply": consecutive_text_replies += 1 response_text = cycle.loop_plan_info["action_result"]["action_data"].get("text", "[空回复]") responses_for_prompt.append(response_text) - + if is_taken: action_detailed_str += f"{action_taken_time_str}时,你选择回复(action:{action_type},内容是:'{response_text}')。{action_reasoning_str}\n" else: action_detailed_str += f"{action_taken_time_str}时,你选择回复(action:{action_type},内容是:'{response_text}'),但是动作失败了。{action_reasoning_str}\n" elif action_type == "no_reply": - action_detailed_str += f"{action_taken_time_str}时,你选择不回复(action:{action_type}),{action_reasoning_str}\n" + action_detailed_str += ( + f"{action_taken_time_str}时,你选择不回复(action:{action_type}),{action_reasoning_str}\n" + ) else: if is_taken: - action_detailed_str += f"{action_taken_time_str}时,你选择执行了(action:{action_type}),{action_reasoning_str}\n" + action_detailed_str += ( + f"{action_taken_time_str}时,你选择执行了(action:{action_type}),{action_reasoning_str}\n" + ) else: action_detailed_str += f"{action_taken_time_str}时,你选择执行了(action:{action_type}),但是动作失败了。{action_reasoning_str}\n" - + if action_detailed_str: cycle_info_block = f"\n你最近做的事:\n{action_detailed_str}\n" else: cycle_info_block = "\n" - + # 根据连续文本回复的数量构建提示信息 if consecutive_text_replies >= 3: # 如果最近的三个活动都是文本回复 cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意' @@ -116,5 +120,5 @@ class HFCloopObservation: "observe_id": self.observe_id, "last_observe_time": self.last_observe_time, # 不序列化history_loop,避免循环引用 - "history_loop_count": len(self.history_loop) + "history_loop_count": len(self.history_loop), } diff --git a/src/chat/heart_flow/observation/observation.py b/src/chat/heart_flow/observation/observation.py index 5c8b5fda4..6396cda06 100644 --- a/src/chat/heart_flow/observation/observation.py +++ b/src/chat/heart_flow/observation/observation.py @@ -18,7 +18,7 @@ class Observation: return { "observe_info": self.observe_info, "observe_id": self.observe_id, - "last_observe_time": self.last_observe_time + "last_observe_time": self.last_observe_time, } async def observe(self): diff --git a/src/chat/heart_flow/observation/structure_observation.py b/src/chat/heart_flow/observation/structure_observation.py index 6e670f5e9..cfe06e435 100644 --- a/src/chat/heart_flow/observation/structure_observation.py +++ b/src/chat/heart_flow/observation/structure_observation.py @@ -22,7 +22,7 @@ class StructureObservation: "observe_id": self.observe_id, "last_observe_time": self.last_observe_time, "history_loop": self.history_loop, - "structured_info": self.structured_info + "structured_info": self.structured_info, } def get_observe_info(self): diff --git a/src/chat/heart_flow/observation/working_observation.py b/src/chat/heart_flow/observation/working_observation.py index 3cab4a375..e94343b01 100644 --- a/src/chat/heart_flow/observation/working_observation.py +++ b/src/chat/heart_flow/observation/working_observation.py @@ -39,6 +39,10 @@ class WorkingMemoryObservation: "observe_info": self.observe_info, "observe_id": self.observe_id, "last_observe_time": self.last_observe_time, - "working_memory": self.working_memory.to_dict() if hasattr(self.working_memory, 'to_dict') else str(self.working_memory), - "retrieved_working_memory": [item.to_dict() if hasattr(item, 'to_dict') else str(item) for item in self.retrieved_working_memory] + "working_memory": self.working_memory.to_dict() + if hasattr(self.working_memory, "to_dict") + else str(self.working_memory), + "retrieved_working_memory": [ + item.to_dict() if hasattr(item, "to_dict") else str(item) for item in self.retrieved_working_memory + ], } diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 1a1469fe0..274ec99e6 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -146,10 +146,10 @@ class FocusChatConfig(ConfigBase): consecutive_replies: float = 1 """连续回复能力,值越高,麦麦连续回复的概率越高""" - + parallel_processing: bool = False """是否允许处理器阶段和回忆阶段并行执行""" - + processor_max_time: int = 25 """处理器最大时间,单位秒,如果超过这个时间,处理器会自动停止""" @@ -166,13 +166,11 @@ class FocusChatProcessorConfig(ConfigBase): working_memory_processor: bool = True """是否启用工作记忆处理器""" - + lite_chat_mind_processor: bool = False """是否启用轻量级聊天思维处理器,可以节省token消耗和时间""" - - @dataclass class ExpressionConfig(ConfigBase): """表达配置类""" diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 3df45460f..96212c725 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -753,7 +753,7 @@ class LLMRequest: response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt) # 原样返回响应,不做处理 - + if len(response) == 3: content, reasoning_content, tool_calls = response return content, (reasoning_content, self.model_name, tool_calls) diff --git a/src/plugins/vtb_action/__init__.py b/src/plugins/vtb_action/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/vtb_action/actions/__init__.py b/src/plugins/vtb_action/actions/__init__.py new file mode 100644 index 000000000..7a85b034b --- /dev/null +++ b/src/plugins/vtb_action/actions/__init__.py @@ -0,0 +1 @@ +from . import vtb_action # noqa diff --git a/src/plugins/vtb_action/actions/vtb_action.py b/src/plugins/vtb_action/actions/vtb_action.py new file mode 100644 index 000000000..79d6914fb --- /dev/null +++ b/src/plugins/vtb_action/actions/vtb_action.py @@ -0,0 +1,74 @@ +from src.common.logger_manager import get_logger +from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action +from typing import Tuple + +logger = get_logger("vtb_action") + + +@register_action +class VTBAction(PluginAction): + """VTB虚拟主播动作处理类""" + + action_name = "vtb_action" + action_description = "使用虚拟主播预设动作表达心情或感觉,适用于需要生动表达情感的场景" + action_parameters = { + "text": "描述想要表达的心情或感觉的文本内容,必填,应当是对情感状态的自然描述", + } + action_require = [ + "当需要表达特定情感或心情时使用", + "当用户明确要求使用虚拟主播动作时使用", + "当回应内容需要更生动的情感表达时使用", + "当想要通过预设动作增强互动体验时使用", + ] + default = True # 设为默认动作 + associated_types = ["vtb_text"] + + async def process(self) -> Tuple[bool, str]: + """处理VTB虚拟主播动作""" + logger.info(f"{self.log_prefix} 执行VTB动作: {self.reasoning}") + + # 获取要表达的心情或感觉文本 + text = self.action_data.get("text") + + if not text: + logger.error(f"{self.log_prefix} 执行VTB动作时未提供文本内容") + return False, "执行VTB动作失败:未提供文本内容" + + # 处理文本使其更适合VTB动作表达 + processed_text = self._process_text_for_vtb(text) + + try: + # 发送VTB动作消息 + await self.send_message(type="vtb_text", data=processed_text) + + logger.info(f"{self.log_prefix} VTB动作执行成功,文本内容: {processed_text}") + return True, "VTB动作执行成功" + + except Exception as e: + logger.error(f"{self.log_prefix} 执行VTB动作时出错: {e}") + return False, f"执行VTB动作时出错: {e}" + + def _process_text_for_vtb(self, text: str) -> str: + """ + 处理文本使其更适合VTB动作表达 + - 优化情感表达的准确性 + - 规范化心情描述格式 + - 确保文本适合虚拟主播动作系统理解 + """ + # 简单示例实现 + processed_text = text.strip() + + # 移除多余的空格和换行 + import re + + processed_text = re.sub(r"\s+", " ", processed_text) + + # 确保文本长度适中,避免过长的描述 + if len(processed_text) > 100: + processed_text = processed_text[:100] + "..." + + # 如果文本为空,提供默认的情感描述 + if not processed_text: + processed_text = "平静" + + return processed_text From d932b48444b67d7476a7484de08b85e4b77891cb Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Mon, 2 Jun 2025 21:06:00 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E4=B8=BAnormal=E5=A2=9E=E5=8A=A0ac?= =?UTF-8?q?tion=5Fplan=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../planners/actions/plugin_action.py | 11 +- src/chat/heart_flow/heartflow.py | 2 +- src/chat/heart_flow/sub_heartflow.py | 21 ++ src/chat/normal_chat/normal_chat.py | 175 +++++++++++- .../normal_chat_action_modifier.py | 102 +++++++ src/chat/normal_chat/normal_chat_expressor.py | 260 ++++++++++++++++++ src/chat/normal_chat/normal_chat_generator.py | 19 +- src/chat/normal_chat/normal_chat_planner.py | 258 +++++++++++++++++ src/chat/normal_chat/normal_prompt.py | 33 ++- src/config/official_configs.py | 3 + template/bot_config_template.toml | 4 +- 11 files changed, 867 insertions(+), 21 deletions(-) create mode 100644 src/chat/normal_chat/normal_chat_action_modifier.py create mode 100644 src/chat/normal_chat/normal_chat_expressor.py create mode 100644 src/chat/normal_chat/normal_chat_planner.py diff --git a/src/chat/focus_chat/planners/actions/plugin_action.py b/src/chat/focus_chat/planners/actions/plugin_action.py index e0f28efa2..d161b5ab3 100644 --- a/src/chat/focus_chat/planners/actions/plugin_action.py +++ b/src/chat/focus_chat/planners/actions/plugin_action.py @@ -135,11 +135,14 @@ class PluginAction(BaseAction): # 获取锚定消息(如果有) observations = self._services.get("observations", []) - chatting_observation: ChattingObservation = next( - obs for obs in observations if isinstance(obs, ChattingObservation) - ) + if len(observations) > 0: + chatting_observation: ChattingObservation = next( + obs for obs in observations if isinstance(obs, ChattingObservation) + ) - anchor_message = chatting_observation.search_message_by_text(target) + anchor_message = chatting_observation.search_message_by_text(target) + else: + anchor_message = None # 如果没有找到锚点消息,创建一个占位符 if not anchor_message: diff --git a/src/chat/heart_flow/heartflow.py b/src/chat/heart_flow/heartflow.py index d58c5cde2..48d433241 100644 --- a/src/chat/heart_flow/heartflow.py +++ b/src/chat/heart_flow/heartflow.py @@ -1,6 +1,6 @@ from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState from src.common.logger_manager import get_logger -from typing import Any, Optional, List +from typing import Any, Optional, List, Dict from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager from src.chat.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager diff --git a/src/chat/heart_flow/sub_heartflow.py b/src/chat/heart_flow/sub_heartflow.py index 984b36383..13c6c0df3 100644 --- a/src/chat/heart_flow/sub_heartflow.py +++ b/src/chat/heart_flow/sub_heartflow.py @@ -330,6 +330,27 @@ class SubHeartflow: oldest_key = next(iter(self.interest_dict)) self.interest_dict.pop(oldest_key) + def get_normal_chat_action_manager(self): + """获取NormalChat的ActionManager实例 + + Returns: + ActionManager: NormalChat的ActionManager实例,如果不存在则返回None + """ + if self.normal_chat_instance: + return self.normal_chat_instance.get_action_manager() + return None + + def set_normal_chat_planner_enabled(self, enabled: bool): + """设置NormalChat的planner是否启用 + + Args: + enabled: 是否启用planner + """ + if self.normal_chat_instance: + self.normal_chat_instance.set_planner_enabled(enabled) + else: + logger.warning(f"{self.log_prefix} NormalChat实例不存在,无法设置planner状态") + async def get_full_state(self) -> dict: """获取子心流的完整状态,包括兴趣、思维和聊天状态。""" return { diff --git a/src/chat/normal_chat/normal_chat.py b/src/chat/normal_chat/normal_chat.py index eecc81c2c..840672a1c 100644 --- a/src/chat/normal_chat/normal_chat.py +++ b/src/chat/normal_chat/normal_chat.py @@ -20,6 +20,10 @@ from src.chat.emoji_system.emoji_manager import emoji_manager from src.chat.normal_chat.willing.willing_manager import willing_manager from src.chat.normal_chat.normal_chat_utils import get_recent_message_stats from src.config.config import global_config +from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.normal_chat.normal_chat_planner import NormalChatPlanner +from src.chat.normal_chat.normal_chat_action_modifier import NormalChatActionModifier +from src.chat.normal_chat.normal_chat_expressor import NormalChatExpressor logger = get_logger("normal_chat") @@ -48,6 +52,12 @@ class NormalChat: self._chat_task: Optional[asyncio.Task] = None self._initialized = False # Track initialization status + # Planner相关初始化 + self.action_manager = ActionManager() + self.planner = NormalChatPlanner(self.stream_name, self.action_manager) + self.action_modifier = NormalChatActionModifier(self.action_manager, self.stream_id, self.stream_name) + self.enable_planner = global_config.normal_chat.enable_planner # 从配置中读取是否启用planner + # 记录最近的回复内容,每项包含: {time, user_message, response, is_mentioned, is_reference_reply} self.recent_replies = [] self.max_replies_history = 20 # 最多保存最近20条回复记录 @@ -64,6 +74,10 @@ class NormalChat: self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + + # 初始化Normal Chat专用表达器 + self.expressor = NormalChatExpressor(self.chat_stream, self.stream_name) + self._initialized = True logger.debug(f"[{self.stream_name}] NormalChat 初始化完成 (异步部分)。") @@ -281,19 +295,108 @@ class NormalChat: info_catcher = info_catcher_manager.get_info_catcher(thinking_id) info_catcher.catch_decide_to_response(message) - try: - with Timer("生成回复", timing_results): - response_set = await self.gpt.generate_response( + # 定义并行执行的任务 + async def generate_normal_response(): + """生成普通回复""" + try: + # 如果启用planner,获取可用actions + enable_planner = self.enable_planner + available_actions = None + + if enable_planner: + try: + await self.action_modifier.modify_actions_for_normal_chat( + self.chat_stream, self.recent_replies + ) + available_actions = self.action_manager.get_using_actions() + except Exception as e: + logger.warning(f"[{self.stream_name}] 获取available_actions失败: {e}") + available_actions = None + + return await self.gpt.generate_response( message=message, thinking_id=thinking_id, + enable_planner=enable_planner, + available_actions=available_actions, ) + except Exception as e: + logger.error(f"[{self.stream_name}] 回复生成出现错误:{str(e)} {traceback.format_exc()}") + return None - info_catcher.catch_after_generate_response(timing_results["生成回复"]) - except Exception as e: - logger.error(f"[{self.stream_name}] 回复生成出现错误:{str(e)} {traceback.format_exc()}") - response_set = None # 确保出错时 response_set 为 None + async def plan_and_execute_actions(): + """规划和执行额外动作""" + if not self.enable_planner: + logger.debug(f"[{self.stream_name}] Planner未启用,跳过动作规划") + return None - if not response_set: + try: + # 并行执行动作修改和规划准备 + async def modify_actions(): + """修改可用动作集合""" + return await self.action_modifier.modify_actions_for_normal_chat( + self.chat_stream, self.recent_replies + ) + + async def prepare_planning(): + """准备规划所需的信息""" + return self._get_sender_name(message) + + # 并行执行动作修改和准备工作 + _, sender_name = await asyncio.gather(modify_actions(), prepare_planning()) + + # 检查是否应该跳过规划 + if self.action_modifier.should_skip_planning(): + logger.debug(f"[{self.stream_name}] 没有可用动作,跳过规划") + return None + + # 执行规划 + plan_result = await self.planner.plan(message, sender_name) + action_type = plan_result["action_result"]["action_type"] + action_data = plan_result["action_result"]["action_data"] + reasoning = plan_result["action_result"]["reasoning"] + + logger.info(f"[{self.stream_name}] Planner决策: {action_type}, 理由: {reasoning}") + self.action_type = action_type # 更新实例属性 + + # 如果规划器决定不执行任何动作 + if action_type == "no_action": + logger.debug(f"[{self.stream_name}] Planner决定不执行任何额外动作") + return None + + # 执行额外的动作(不影响回复生成) + action_result = await self._execute_action(action_type, action_data, message, thinking_id) + if action_result is not None: + logger.info(f"[{self.stream_name}] 额外动作 {action_type} 执行完成") + else: + logger.warning(f"[{self.stream_name}] 额外动作 {action_type} 执行失败") + + return {"action_type": action_type, "action_data": action_data, "reasoning": reasoning} + + except Exception as e: + logger.error(f"[{self.stream_name}] Planner执行失败: {e}") + return None + + # 并行执行回复生成和动作规划 + self.action_type = None # 初始化动作类型 + with Timer("并行生成回复和规划", timing_results): + response_set, plan_result = await asyncio.gather( + generate_normal_response(), plan_and_execute_actions(), return_exceptions=True + ) + + # 处理生成回复的结果 + if isinstance(response_set, Exception): + logger.error(f"[{self.stream_name}] 回复生成异常: {response_set}") + response_set = None + elif response_set: + info_catcher.catch_after_generate_response(timing_results["并行生成回复和规划"]) + + # 处理规划结果(可选,不影响回复) + if isinstance(plan_result, Exception): + logger.error(f"[{self.stream_name}] 动作规划异常: {plan_result}") + elif plan_result: + logger.debug(f"[{self.stream_name}] 额外动作处理完成: {plan_result['action_type']}") + + if not response_set or (self.enable_planner and self.action_type != "no_action"): logger.info(f"[{self.stream_name}] 模型未生成回复内容") # 如果模型未生成回复,移除思考消息 container = await message_manager.get_container(self.stream_id) # 使用 self.stream_id @@ -523,3 +626,59 @@ class NormalChat: self.willing_amplifier = 5 elif self.willing_amplifier < 0.1: self.willing_amplifier = 0.1 + + def _get_sender_name(self, message: MessageRecv) -> str: + """获取发送者名称,用于planner""" + if message.chat_stream.user_info: + user_info = message.chat_stream.user_info + if user_info.user_cardname and user_info.user_nickname: + return f"[{user_info.user_nickname}][群昵称:{user_info.user_cardname}]" + elif user_info.user_nickname: + return f"[{user_info.user_nickname}]" + else: + return f"用户({user_info.user_id})" + return "某人" + + async def _execute_action( + self, action_type: str, action_data: dict, message: MessageRecv, thinking_id: str + ) -> Optional[bool]: + """执行具体的动作,只返回执行成功与否""" + try: + # 创建动作处理器实例 + action_handler = self.action_manager.create_action( + action_name=action_type, + action_data=action_data, + reasoning=action_data.get("reasoning", ""), + cycle_timers={}, # normal_chat使用空的cycle_timers + thinking_id=thinking_id, + observations=[], # normal_chat不使用observations + expressor=self.expressor, # 使用normal_chat专用的expressor + chat_stream=self.chat_stream, + log_prefix=self.stream_name, + shutting_down=self._disabled, + ) + + if action_handler: + # 执行动作 + result = await action_handler.handle_action() + if result and isinstance(result, tuple) and len(result) >= 2: + # handle_action返回 (success: bool, message: str) + success, _ = result[0], result[1] + return success + elif result: + # 如果返回了其他结果,假设成功 + return True + + except Exception as e: + logger.error(f"[{self.stream_name}] 执行动作 {action_type} 失败: {e}") + + return False + + def set_planner_enabled(self, enabled: bool): + """设置是否启用planner""" + self.enable_planner = enabled + logger.info(f"[{self.stream_name}] Planner {'启用' if enabled else '禁用'}") + + def get_action_manager(self) -> ActionManager: + """获取动作管理器实例""" + return self.action_manager diff --git a/src/chat/normal_chat/normal_chat_action_modifier.py b/src/chat/normal_chat/normal_chat_action_modifier.py new file mode 100644 index 000000000..e40aa7eea --- /dev/null +++ b/src/chat/normal_chat/normal_chat_action_modifier.py @@ -0,0 +1,102 @@ +from typing import List, Optional, Any, Dict +from src.common.logger_manager import get_logger +from src.chat.message_receive.chat_stream import chat_manager +from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.normal_chat.normal_chat_utils import get_recent_message_stats +from src.config.config import global_config +import time + +logger = get_logger("normal_chat_action_modifier") + + +class NormalChatActionModifier: + """Normal Chat动作修改器 + + 负责根据Normal Chat的上下文和状态动态调整可用的动作集合 + """ + + def __init__(self, action_manager: ActionManager, stream_id: str, stream_name: str): + """初始化动作修改器""" + self.action_manager = action_manager + self.stream_id = stream_id + self.stream_name = stream_name + self.log_prefix = f"[{stream_name}]动作修改器" + + # 缓存所有注册的动作 + self.all_actions = self.action_manager.get_registered_actions() + + async def modify_actions_for_normal_chat( + self, + chat_stream, + recent_replies: List[dict], + **kwargs: Any, + ): + """为Normal Chat修改可用动作集合 + + Args: + chat_stream: 聊天流对象 + recent_replies: 最近的回复记录 + **kwargs: 其他参数 + """ + + # 合并所有动作变更 + merged_action_changes = {"add": [], "remove": []} + reasons = [] + + # 1. 移除Normal Chat不适用的动作 + excluded_actions = ["exit_focus_chat_action", "no_reply", "reply"] + for action_name in excluded_actions: + if action_name in self.action_manager.get_using_actions(): + merged_action_changes["remove"].append(action_name) + reasons.append(f"移除{action_name}(Normal Chat不适用)") + + # 2. 检查动作的关联类型 + if chat_stream: + chat_context = chat_stream.context if hasattr(chat_stream, "context") else None + if chat_context: + type_mismatched_actions = [] + + current_using_actions = self.action_manager.get_using_actions() + for action_name in current_using_actions.keys(): + if action_name in self.all_actions: + data = self.all_actions[action_name] + if data.get("associated_types"): + if not chat_context.check_types(data["associated_types"]): + type_mismatched_actions.append(action_name) + logger.debug(f"{self.log_prefix} 动作 {action_name} 关联类型不匹配,移除该动作") + + if type_mismatched_actions: + merged_action_changes["remove"].extend(type_mismatched_actions) + reasons.append(f"移除{type_mismatched_actions}(关联类型不匹配)") + + # 应用动作变更 + for action_name in merged_action_changes["add"]: + if action_name in self.all_actions and action_name not in excluded_actions: + success = self.action_manager.add_action_to_using(action_name) + if success: + logger.debug(f"{self.log_prefix} 添加动作: {action_name}") + + for action_name in merged_action_changes["remove"]: + success = self.action_manager.remove_action_from_using(action_name) + if success: + logger.debug(f"{self.log_prefix} 移除动作: {action_name}") + + # 记录变更原因 + if merged_action_changes["add"] or merged_action_changes["remove"]: + logger.info(f"{self.log_prefix} 动作调整完成: {' | '.join(reasons)}") + logger.debug(f"{self.log_prefix} 当前可用动作: {list(self.action_manager.get_using_actions().keys())}") + + def get_available_actions_count(self) -> int: + """获取当前可用动作数量(排除默认的no_action)""" + current_actions = self.action_manager.get_using_actions() + # 排除no_action(如果存在) + filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"} + return len(filtered_actions) + + def should_skip_planning(self) -> bool: + """判断是否应该跳过规划过程""" + available_count = self.get_available_actions_count() + if available_count == 0: + logger.debug(f"{self.log_prefix} 没有可用动作,跳过规划") + return True + return False diff --git a/src/chat/normal_chat/normal_chat_expressor.py b/src/chat/normal_chat/normal_chat_expressor.py new file mode 100644 index 000000000..ba455e34c --- /dev/null +++ b/src/chat/normal_chat/normal_chat_expressor.py @@ -0,0 +1,260 @@ +""" +Normal Chat Expressor + +为Normal Chat专门设计的表达器,不需要经过LLM风格化处理, +直接发送消息,主要用于插件动作中需要发送消息的场景。 +""" + +import time +from typing import List, Optional, Tuple, Dict, Any +from src.chat.message_receive.message import MessageRecv, MessageSending, MessageThinking, MessageSet, Seg +from src.chat.message_receive.message import UserInfo +from src.chat.message_receive.chat_stream import ChatStream +from src.chat.message_receive.message_sender import message_manager +from src.config.config import global_config +from src.common.logger_manager import get_logger +from src.chat.emoji_system.emoji_manager import emoji_manager +from src.chat.utils.utils_image import image_path_to_base64 +import random + +logger = get_logger("normal_chat_expressor") + + +class NormalChatExpressor: + """Normal Chat专用表达器 + + 特点: + 1. 不经过LLM风格化,直接发送消息 + 2. 支持文本和表情包发送 + 3. 为插件动作提供简化的消息发送接口 + 4. 保持与focus_chat expressor相似的API,但去掉复杂的风格化流程 + """ + + def __init__(self, chat_stream: ChatStream, stream_name: str): + """初始化Normal Chat表达器 + + Args: + chat_stream: 聊天流对象 + stream_name: 流名称 + """ + self.chat_stream = chat_stream + self.stream_name = stream_name + self.log_prefix = f"[{stream_name}]Normal表达器" + logger.debug(f"{self.log_prefix} 初始化完成") + + async def create_thinking_message( + self, anchor_message: Optional[MessageRecv], thinking_id: str + ) -> Optional[MessageThinking]: + """创建思考消息 + + Args: + anchor_message: 锚点消息 + thinking_id: 思考ID + + Returns: + MessageThinking: 创建的思考消息,如果失败返回None + """ + if not anchor_message or not anchor_message.chat_stream: + logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流") + return None + + messageinfo = anchor_message.message_info + thinking_time_point = time.time() + + bot_user_info = UserInfo( + user_id=global_config.bot.qq_account, + user_nickname=global_config.bot.nickname, + platform=messageinfo.platform, + ) + + thinking_message = MessageThinking( + message_id=thinking_id, + chat_stream=self.chat_stream, + bot_user_info=bot_user_info, + reply=anchor_message, + thinking_start_time=thinking_time_point, + ) + + await message_manager.add_message(thinking_message) + logger.debug(f"{self.log_prefix} 创建思考消息: {thinking_id}") + return thinking_message + + async def send_response_messages( + self, + anchor_message: Optional[MessageRecv], + response_set: List[Tuple[str, str]], + thinking_id: str = "", + display_message: str = "", + ) -> Optional[MessageSending]: + """发送回复消息 + + Args: + anchor_message: 锚点消息 + response_set: 回复内容集合,格式为 [(type, content), ...] + thinking_id: 思考ID + display_message: 显示消息 + + Returns: + MessageSending: 发送的第一条消息,如果失败返回None + """ + try: + if not response_set: + logger.warning(f"{self.log_prefix} 回复内容为空") + return None + + # 如果没有thinking_id,生成一个 + if not thinking_id: + thinking_time_point = round(time.time(), 2) + thinking_id = "mt" + str(thinking_time_point) + + # 创建思考消息 + if anchor_message: + await self.create_thinking_message(anchor_message, thinking_id) + + # 创建消息集 + + first_bot_msg = None + mark_head = False + is_emoji = False + if len(response_set) == 0: + return None + message_id = f"{thinking_id}_{len(response_set)}" + response_type, content = response_set[0] + if len(response_set) > 1: + message_segment = Seg(type="seglist", data=[Seg(type=t, data=c) for t, c in response_set]) + else: + message_segment = Seg(type=response_type, data=content) + if response_type == "emoji": + is_emoji = True + + bot_msg = await self._build_sending_message( + message_id=message_id, + message_segment=message_segment, + thinking_id=thinking_id, + anchor_message=anchor_message, + thinking_start_time=time.time(), + reply_to=mark_head, + is_emoji=is_emoji, + ) + logger.debug(f"{self.log_prefix} 添加{response_type}类型消息: {content}") + + # 提交消息集 + if bot_msg: + await message_manager.add_message(bot_msg) + logger.info(f"{self.log_prefix} 成功发送 {response_type}类型消息: {content}") + container = await message_manager.get_container(self.chat_stream.stream_id) # 使用 self.stream_id + for msg in container.messages[:]: + if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: + container.messages.remove(msg) + logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}") + break + return first_bot_msg + else: + logger.warning(f"{self.log_prefix} 没有有效的消息被创建") + return None + + except Exception as e: + logger.error(f"{self.log_prefix} 发送消息失败: {e}") + import traceback + + traceback.print_exc() + return None + + async def _build_sending_message( + self, + message_id: str, + message_segment: Seg, + thinking_id: str, + anchor_message: Optional[MessageRecv], + thinking_start_time: float, + reply_to: bool = False, + is_emoji: bool = False, + ) -> MessageSending: + """构建发送消息 + + Args: + message_id: 消息ID + message_segment: 消息段 + thinking_id: 思考ID + anchor_message: 锚点消息 + thinking_start_time: 思考开始时间 + reply_to: 是否回复 + is_emoji: 是否为表情包 + + Returns: + MessageSending: 构建的发送消息 + """ + bot_user_info = UserInfo( + user_id=global_config.bot.qq_account, + user_nickname=global_config.bot.nickname, + platform=anchor_message.message_info.platform if anchor_message else "unknown", + ) + + message_sending = MessageSending( + message_id=message_id, + chat_stream=self.chat_stream, + bot_user_info=bot_user_info, + message_segment=message_segment, + sender_info=self.chat_stream.user_info, + reply=anchor_message if reply_to else None, + thinking_start_time=thinking_start_time, + is_emoji=is_emoji, + ) + + return message_sending + + async def deal_reply( + self, + cycle_timers: dict, + action_data: Dict[str, Any], + reasoning: str, + anchor_message: MessageRecv, + thinking_id: str, + ) -> Tuple[bool, Optional[str]]: + """处理回复动作 - 兼容focus_chat expressor API + + Args: + cycle_timers: 周期计时器(normal_chat中不使用) + action_data: 动作数据,包含text、target、emojis等 + reasoning: 推理说明 + anchor_message: 锚点消息 + thinking_id: 思考ID + + Returns: + Tuple[bool, Optional[str]]: (是否成功, 回复文本) + """ + try: + response_set = [] + + # 处理文本内容 + text_content = action_data.get("text", "") + if text_content: + response_set.append(("text", text_content)) + + # 处理表情包 + emoji_content = action_data.get("emojis", "") + if emoji_content: + response_set.append(("emoji", emoji_content)) + + if not response_set: + logger.warning(f"{self.log_prefix} deal_reply: 没有有效的回复内容") + return False, None + + # 发送消息 + result = await self.send_response_messages( + anchor_message=anchor_message, + response_set=response_set, + thinking_id=thinking_id, + ) + + if result: + return True, text_content if text_content else "发送成功" + else: + return False, None + + except Exception as e: + logger.error(f"{self.log_prefix} deal_reply执行失败: {e}") + import traceback + + traceback.print_exc() + return False, None diff --git a/src/chat/normal_chat/normal_chat_generator.py b/src/chat/normal_chat/normal_chat_generator.py index 5d17d22ab..f74904f60 100644 --- a/src/chat/normal_chat/normal_chat_generator.py +++ b/src/chat/normal_chat/normal_chat_generator.py @@ -36,7 +36,9 @@ class NormalChatGenerator: self.current_model_type = "r1" # 默认使用 R1 self.current_model_name = "unknown model" - async def generate_response(self, message: MessageThinking, thinking_id: str) -> Optional[Union[str, List[str]]]: + async def generate_response( + self, message: MessageThinking, thinking_id: str, enable_planner: bool = False, available_actions=None + ) -> Optional[Union[str, List[str]]]: """根据当前模型类型选择对应的生成函数""" # 从global_config中获取模型概率值并选择模型 if random.random() < global_config.normal_chat.normal_chat_first_probability: @@ -50,7 +52,9 @@ class NormalChatGenerator: f"{self.current_model_name}思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}" ) # noqa: E501 - model_response = await self._generate_response_with_model(message, current_model, thinking_id) + model_response = await self._generate_response_with_model( + message, current_model, thinking_id, enable_planner, available_actions + ) if model_response: logger.debug(f"{global_config.bot.nickname}的原始回复是:{model_response}") @@ -61,7 +65,14 @@ class NormalChatGenerator: logger.info(f"{self.current_model_name}思考,失败") return None - async def _generate_response_with_model(self, message: MessageThinking, model: LLMRequest, thinking_id: str): + async def _generate_response_with_model( + self, + message: MessageThinking, + model: LLMRequest, + thinking_id: str, + enable_planner: bool = False, + available_actions=None, + ): info_catcher = info_catcher_manager.get_info_catcher(thinking_id) person_id = person_info_manager.get_person_id( @@ -86,6 +97,8 @@ class NormalChatGenerator: message_txt=message.processed_plain_text, sender_name=sender_name, chat_stream=message.chat_stream, + enable_planner=enable_planner, + available_actions=available_actions, ) logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}") diff --git a/src/chat/normal_chat/normal_chat_planner.py b/src/chat/normal_chat/normal_chat_planner.py new file mode 100644 index 000000000..634bf5c77 --- /dev/null +++ b/src/chat/normal_chat/normal_chat_planner.py @@ -0,0 +1,258 @@ +import json +from typing import Dict, Any +from rich.traceback import install +from src.llm_models.utils_model import LLMRequest +from src.config.config import global_config +from src.common.logger_manager import get_logger +from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.individuality.individuality import individuality +from src.chat.focus_chat.planners.action_manager import ActionManager +from src.chat.normal_chat.normal_prompt import prompt_builder +from src.chat.message_receive.message import MessageThinking +from json_repair import repair_json + +logger = get_logger("normal_chat_planner") + +install(extra_lines=3) + + +def init_prompt(): + Prompt( + """ +你的自我认知是: +{self_info_block} + +注意,除了下面动作选项之外,你在聊天中不能做其他任何事情,这是你能力的边界,现在请你选择合适的action: + +{action_options_text} + +重要说明: +- "no_action" 表示只进行普通聊天回复,不执行任何额外动作 +- 其他action表示在普通回复的基础上,执行相应的额外动作 + +你必须从上面列出的可用action中选择一个,并说明原因。 +你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。 + +{moderation_prompt} + +当前聊天上下文: +{chat_context} + +基于以上聊天上下文和用户的最新消息,选择最合适的action。 + +请你以下面格式输出你选择的action: +{{ + "action": "action_name", + "reasoning": "说明你做出该action的原因", + "参数1": "参数1的值", + "参数2": "参数2的值", + "参数3": "参数3的值", + ... +}} + +请输出你的决策 JSON:""", + "normal_chat_planner_prompt", + ) + + Prompt( + """ +action_name: {action_name} + 描述:{action_description} + 参数: +{action_parameters} + 动作要求: +{action_require}""", + "normal_chat_action_prompt", + ) + + +class NormalChatPlanner: + def __init__(self, log_prefix: str, action_manager: ActionManager): + self.log_prefix = log_prefix + # LLM规划器配置 + self.planner_llm = LLMRequest( + model=global_config.model.normal_chat_2, + max_tokens=1000, + request_type="normal_chat.planner", # 用于normal_chat动作规划 + ) + + self.action_manager = action_manager + + async def plan(self, message: MessageThinking, sender_name: str = "某人") -> Dict[str, Any]: + """ + Normal Chat 规划器: 使用LLM根据上下文决定做出什么动作。 + + 参数: + message: 思考消息对象 + sender_name: 发送者名称 + """ + + action = "no_action" # 默认动作改为no_action + reasoning = "规划器初始化默认" + action_data = {} + + try: + # 设置默认值 + nickname_str = "" + for nicknames in global_config.bot.alias_names: + nickname_str += f"{nicknames}," + name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。" + + personality_block = individuality.get_personality_prompt(x_person=2, level=2) + identity_block = individuality.get_identity_prompt(x_person=2, level=2) + + self_info = name_block + personality_block + identity_block + + # 获取当前可用的动作 + current_available_actions = self.action_manager.get_using_actions() + + # 如果没有可用动作或只有no_action动作,直接返回no_action + if not current_available_actions or ( + len(current_available_actions) == 1 and "no_action" in current_available_actions + ): + logger.debug(f"{self.log_prefix}规划器: 没有可用动作或只有no_action动作,返回no_action") + return { + "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning}, + "chat_context": "", + "action_prompt": "", + } + + # 构建normal_chat的上下文 (使用与normal_chat相同的prompt构建方法) + chat_context = await prompt_builder.build_prompt( + message_txt=message.processed_plain_text, + sender_name=sender_name, + chat_stream=message.chat_stream, + ) + + # 构建planner的prompt + prompt = await self.build_planner_prompt( + self_info_block=self_info, + chat_context=chat_context, + current_available_actions=current_available_actions, + ) + + if not prompt: + logger.warning(f"{self.log_prefix}规划器: 构建提示词失败") + return { + "action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning}, + "chat_context": chat_context, + "action_prompt": "", + } + + # 使用LLM生成动作决策 + try: + content, reasoning_content, model_name = await self.planner_llm.generate_response(prompt) + logger.debug(f"{self.log_prefix}规划器原始响应: {content}") + + # 解析JSON响应 + try: + # 尝试修复JSON + fixed_json = repair_json(content) + action_result = json.loads(fixed_json) + + action = action_result.get("action", "no_action") + reasoning = action_result.get("reasoning", "未提供原因") + + # 提取其他参数作为action_data + action_data = {k: v for k, v in action_result.items() if k not in ["action", "reasoning"]} + + # 验证动作是否在可用动作列表中 + if action not in current_available_actions: + logger.warning(f"{self.log_prefix}规划器选择了不可用的动作: {action}, 回退到no_action") + action = "no_action" + reasoning = f"选择的动作{action}不在可用列表中,回退到no_action" + action_data = {} + + except json.JSONDecodeError as e: + logger.warning(f"{self.log_prefix}规划器JSON解析失败: {e}, 内容: {content}") + action = "no_action" + reasoning = "JSON解析失败,使用默认动作" + action_data = {} + + except Exception as e: + logger.error(f"{self.log_prefix}规划器LLM调用失败: {e}") + action = "no_action" + reasoning = "LLM调用失败,使用默认动作" + action_data = {} + + except Exception as outer_e: + logger.error(f"{self.log_prefix}规划器异常: {outer_e}") + chat_context = "无法获取聊天上下文" # 设置默认值 + prompt = "" # 设置默认值 + action = "no_action" + reasoning = "规划器出现异常,使用默认动作" + action_data = {} + + logger.debug(f"{self.log_prefix}规划器决策动作:{action}, 动作信息: '{action_data}', 理由: {reasoning}") + + # 恢复到默认动作集 + self.action_manager.restore_actions() + logger.debug( + f"{self.log_prefix}规划后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}" + ) + + action_result = {"action_type": action, "action_data": action_data, "reasoning": reasoning} + + plan_result = { + "action_result": action_result, + "chat_context": chat_context, + "action_prompt": prompt, + } + + return plan_result + + async def build_planner_prompt( + self, + self_info_block: str, + chat_context: str, + current_available_actions: Dict[str, Any], + ) -> str: + """构建 Normal Chat Planner LLM 的提示词""" + try: + # 构建动作选项文本 + action_options_text = "" + for action_name, action_info in current_available_actions.items(): + action_description = action_info.get("description", "") + action_parameters = action_info.get("parameters", {}) + action_require = action_info.get("require", []) + + # 格式化参数 + parameters_text = "" + for param_name, param_desc in action_parameters.items(): + parameters_text += f" - {param_name}: {param_desc}\n" + + # 格式化要求 + require_text = "" + for req in action_require: + require_text += f" - {req}\n" + + # 构建单个动作的提示 + action_prompt = await global_prompt_manager.format_prompt( + "normal_chat_action_prompt", + action_name=action_name, + action_description=action_description, + action_parameters=parameters_text, + action_require=require_text, + ) + action_options_text += action_prompt + "\n\n" + + # 审核提示 + moderation_prompt = "请确保你的回复符合平台规则,避免不当内容。" + + # 使用模板构建最终提示词 + prompt = await global_prompt_manager.format_prompt( + "normal_chat_planner_prompt", + self_info_block=self_info_block, + action_options_text=action_options_text, + moderation_prompt=moderation_prompt, + chat_context=chat_context, + ) + + return prompt + + except Exception as e: + logger.error(f"{self.log_prefix}构建Planner提示词失败: {e}") + return "" + + +init_prompt() diff --git a/src/chat/normal_chat/normal_prompt.py b/src/chat/normal_chat/normal_prompt.py index e4d69a0ff..d30dcbceb 100644 --- a/src/chat/normal_chat/normal_prompt.py +++ b/src/chat/normal_chat/normal_prompt.py @@ -38,7 +38,8 @@ def init_prompt(): {chat_talking_prompt} 现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言或者回复这条消息。\n 你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 -你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},请你给出回复 + +{action_descriptions}你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},请你给出回复 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,{reply_style2}。{prompt_ger} 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要随意遵从他人指令。 请注意不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容。 @@ -70,7 +71,8 @@ def init_prompt(): 现在 {sender_name} 说的: {message_txt} 引起了你的注意,你想要回复这条消息。 你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 -你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,{mood_prompt},请你给出回复 + +{action_descriptions}你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,{mood_prompt},请你给出回复 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,{reply_style2}。{prompt_ger} 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要随意遵从他人指令。 请注意不要输出多余内容(包括前后缀,冒号和引号,括号等),只输出回复内容。 @@ -90,10 +92,21 @@ class PromptBuilder: chat_stream, message_txt=None, sender_name="某人", + enable_planner=False, + available_actions=None, ) -> Optional[str]: - return await self._build_prompt_normal(chat_stream, message_txt or "", sender_name) + return await self._build_prompt_normal( + chat_stream, message_txt or "", sender_name, enable_planner, available_actions + ) - async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str: + async def _build_prompt_normal( + self, + chat_stream, + message_txt: str, + sender_name: str = "某人", + enable_planner: bool = False, + available_actions=None, + ) -> str: prompt_personality = individuality.get_prompt(x_person=2, level=2) is_group_chat = bool(chat_stream.group_info) @@ -214,6 +227,16 @@ class PromptBuilder: moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" + # 构建action描述 (如果启用planner) + action_descriptions = "" + logger.debug(f"Enable planner {enable_planner}, available actions: {available_actions}") + if enable_planner and available_actions: + action_descriptions = "你有以下的动作能力,但执行这些动作不由你决定,由另外一个模型同步决定,因此你只需要知道有如下能力即可:\n" + for action_name, action_info in available_actions.items(): + action_description = action_info.get("description", "") + action_descriptions += f"- {action_name}: {action_description}\n" + action_descriptions += "\n" + # 知识构建 start_time = time.time() prompt_info = await self.get_prompt_info(message_txt, threshold=0.38) @@ -256,6 +279,7 @@ class PromptBuilder: # moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), moderation_prompt=moderation_prompt_block, now_time=now_time, + action_descriptions=action_descriptions, ) else: template_name = "reasoning_prompt_private_main" @@ -281,6 +305,7 @@ class PromptBuilder: # moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), moderation_prompt=moderation_prompt_block, now_time=now_time, + action_descriptions=action_descriptions, ) # --- End choosing template --- diff --git a/src/config/official_configs.py b/src/config/official_configs.py index a71eeb087..6ef834f3c 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -127,6 +127,9 @@ class NormalChatConfig(ConfigBase): at_bot_inevitable_reply: bool = False """@bot 必然回复""" + enable_planner: bool = False + """是否启用动作规划器""" + @dataclass class FocusChatConfig(ConfigBase): diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d4cdbd15d..2f40d3d08 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "2.8.0" +version = "2.9.0" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -87,6 +87,8 @@ emoji_response_penalty = 0 # 表情包回复惩罚系数,设为0为不回复 mentioned_bot_inevitable_reply = true # 提及 bot 必然回复 at_bot_inevitable_reply = true # @bot 必然回复 +enable_planner = false # 是否启用动作规划器(实验性功能,与focus_chat共享actions) + down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法 talk_frequency_down_groups = [] #降低回复频率的群号码 From 20d68a78962ecbfdf33c70a7b5a4d705b94c2633 Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Mon, 2 Jun 2025 21:14:50 +0800 Subject: [PATCH 3/4] fix: pass ruff --- src/chat/focus_chat/planners/actions/emoji_action.py | 1 - src/chat/heart_flow/heartflow.py | 2 +- src/chat/normal_chat/normal_chat_action_modifier.py | 6 +----- src/chat/normal_chat/normal_chat_expressor.py | 5 +---- src/config/official_configs.py | 6 +++--- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/chat/focus_chat/planners/actions/emoji_action.py b/src/chat/focus_chat/planners/actions/emoji_action.py index b46481cc6..44af4ecca 100644 --- a/src/chat/focus_chat/planners/actions/emoji_action.py +++ b/src/chat/focus_chat/planners/actions/emoji_action.py @@ -6,7 +6,6 @@ from typing import Tuple, List from src.chat.heart_flow.observation.observation import Observation from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer from src.chat.message_receive.chat_stream import ChatStream -from src.chat.heart_flow.observation.chatting_observation import ChattingObservation from src.chat.focus_chat.hfc_utils import create_empty_anchor_message logger = get_logger("action_taken") diff --git a/src/chat/heart_flow/heartflow.py b/src/chat/heart_flow/heartflow.py index 48d433241..d58c5cde2 100644 --- a/src/chat/heart_flow/heartflow.py +++ b/src/chat/heart_flow/heartflow.py @@ -1,6 +1,6 @@ from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState from src.common.logger_manager import get_logger -from typing import Any, Optional, List, Dict +from typing import Any, Optional, List from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager from src.chat.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager diff --git a/src/chat/normal_chat/normal_chat_action_modifier.py b/src/chat/normal_chat/normal_chat_action_modifier.py index e40aa7eea..f4d0285c5 100644 --- a/src/chat/normal_chat/normal_chat_action_modifier.py +++ b/src/chat/normal_chat/normal_chat_action_modifier.py @@ -1,10 +1,6 @@ -from typing import List, Optional, Any, Dict +from typing import List, Any from src.common.logger_manager import get_logger -from src.chat.message_receive.chat_stream import chat_manager from src.chat.focus_chat.planners.action_manager import ActionManager -from src.chat.normal_chat.normal_chat_utils import get_recent_message_stats -from src.config.config import global_config -import time logger = get_logger("normal_chat_action_modifier") diff --git a/src/chat/normal_chat/normal_chat_expressor.py b/src/chat/normal_chat/normal_chat_expressor.py index ba455e34c..ac7b5cb75 100644 --- a/src/chat/normal_chat/normal_chat_expressor.py +++ b/src/chat/normal_chat/normal_chat_expressor.py @@ -7,15 +7,12 @@ Normal Chat Expressor import time from typing import List, Optional, Tuple, Dict, Any -from src.chat.message_receive.message import MessageRecv, MessageSending, MessageThinking, MessageSet, Seg +from src.chat.message_receive.message import MessageRecv, MessageSending, MessageThinking, Seg from src.chat.message_receive.message import UserInfo from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.message_sender import message_manager from src.config.config import global_config from src.common.logger_manager import get_logger -from src.chat.emoji_system.emoji_manager import emoji_manager -from src.chat.utils.utils_image import image_path_to_base64 -import random logger = get_logger("normal_chat_expressor") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 751bb0ff1..512b2f8d1 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -306,16 +306,16 @@ class KeywordRuleConfig(ConfigBase): """验证配置""" if not self.keywords and not self.regex: raise ValueError("关键词规则必须至少包含keywords或regex中的一个") - + if not self.reaction: raise ValueError("关键词规则必须包含reaction") - + # 验证正则表达式 for pattern in self.regex: try: re.compile(pattern) except re.error as e: - raise ValueError(f"无效的正则表达式 '{pattern}': {str(e)}") + raise ValueError(f"无效的正则表达式 '{pattern}': {str(e)}") from e @dataclass From 243d5184e651861ee1b6335105cffb975c8d3458 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Jun 2025 13:15:04 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../planners/actions/emoji_action.py | 8 ++----- .../focus_chat/planners/planner_simple.py | 8 ++----- .../focus_chat/replyer/default_replyer.py | 24 ++++++++----------- .../working_memory/memory_manager.py | 8 +++++-- src/chat/utils/utils.py | 2 +- src/chat/utils/utils_image.py | 4 +--- src/common/remote.py | 4 +++- src/config/config_base.py | 6 ++++- 8 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/chat/focus_chat/planners/actions/emoji_action.py b/src/chat/focus_chat/planners/actions/emoji_action.py index 44af4ecca..68da4b1d8 100644 --- a/src/chat/focus_chat/planners/actions/emoji_action.py +++ b/src/chat/focus_chat/planners/actions/emoji_action.py @@ -23,11 +23,7 @@ class EmojiAction(BaseAction): action_parameters: dict[str:str] = { "description": "文字描述你想要发送的表情", } - action_require: list[str] = [ - "你想要发送一个表情", - "表达情绪时可以选择使用", - "一般在你回复之后可以选择性使用" - ] + action_require: list[str] = ["你想要发送一个表情", "表达情绪时可以选择使用", "一般在你回复之后可以选择性使用"] associated_types: list[str] = ["emoji"] @@ -108,7 +104,7 @@ class EmojiAction(BaseAction): # ) # else: # anchor_message.update_chat_stream(self.chat_stream) - + logger.info(f"{self.log_prefix} 为了表情包创建占位符") anchor_message = await create_empty_anchor_message( self.chat_stream.platform, self.chat_stream.group_info, self.chat_stream diff --git a/src/chat/focus_chat/planners/planner_simple.py b/src/chat/focus_chat/planners/planner_simple.py index d8518ea23..83fff195a 100644 --- a/src/chat/focus_chat/planners/planner_simple.py +++ b/src/chat/focus_chat/planners/planner_simple.py @@ -58,7 +58,6 @@ def init_prompt(): """, "simple_planner_prompt", ) - Prompt( """ @@ -192,7 +191,6 @@ class ActionPlanner(BasePlanner): reasoning = f"LLM 请求失败,你的模型出现问题: {req_e}" action = "no_reply" - if llm_content: try: fixed_json_string = repair_json(llm_content) @@ -233,9 +231,7 @@ class ActionPlanner(BasePlanner): reasoning = extracted_reasoning except Exception as json_e: - logger.warning( - f"{self.log_prefix}解析LLM响应JSON失败 {json_e}. LLM原始输出: '{llm_content}'" - ) + logger.warning(f"{self.log_prefix}解析LLM响应JSON失败 {json_e}. LLM原始输出: '{llm_content}'") traceback.print_exc() reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'." action = "no_reply" @@ -353,7 +349,7 @@ class ActionPlanner(BasePlanner): # moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" moderation_prompt_block = "" - + # 获取当前时间 time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" diff --git a/src/chat/focus_chat/replyer/default_replyer.py b/src/chat/focus_chat/replyer/default_replyer.py index 650eb2788..644b5fe9d 100644 --- a/src/chat/focus_chat/replyer/default_replyer.py +++ b/src/chat/focus_chat/replyer/default_replyer.py @@ -75,7 +75,7 @@ def init_prompt(): 不要浮夸,不要夸张修辞,只输出一条回复就好。 现在,你说: """, - "default_replyer_private_prompt", + "default_replyer_private_prompt", ) @@ -181,7 +181,7 @@ class DefaultReplyer: return False, None # --- 回复器 (Replier) 的定义 --- # - + async def deal_emoji( self, anchor_message: MessageRecv, @@ -192,10 +192,9 @@ class DefaultReplyer: """ 表情动作处理类 """ - + await self._create_thinking_message(anchor_message, thinking_id) - - + try: has_sent_something = False sent_msg_list = [] @@ -208,8 +207,7 @@ class DefaultReplyer: reply.append(("emoji", emoji_base64)) else: logger.warning(f"{self.log_prefix} 没有找到合适表情") - - + if reply: with Timer("发送表情", cycle_timers): sent_msg_list = await self.send_response_messages( @@ -230,8 +228,6 @@ class DefaultReplyer: logger.error(f"回复失败: {e}") traceback.print_exc() return False, None - - async def reply( self, @@ -293,7 +289,7 @@ class DefaultReplyer: # TODO: API-Adapter修改标记 # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\nPrompt:\n{prompt}\n") content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt) - + logger.debug(f"prompt: {prompt}") logger.info(f"最终回复: {content}") @@ -384,8 +380,8 @@ class DefaultReplyer: style_habbits_str = "\n".join(style_habbits) grammar_habbits_str = "\n".join(grammar_habbits) - - # 关键词检测与反应 + + # 关键词检测与反应 keywords_reaction_prompt = "" try: # 处理关键词规则 @@ -393,7 +389,7 @@ class DefaultReplyer: if any(keyword in target_message for keyword in rule.keywords): logger.info(f"检测到关键词规则:{rule.keywords},触发反应:{rule.reaction}") keywords_reaction_prompt += f"{rule.reaction}," - + # 处理正则表达式规则 for rule in global_config.keyword_reaction.regex_rules: for pattern_str in rule.regex: @@ -411,7 +407,7 @@ class DefaultReplyer: continue except Exception as e: logger.error(f"关键词检测与反应时发生异常: {str(e)}", exc_info=True) - + time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" # logger.debug("开始构建 focus prompt") diff --git a/src/chat/focus_chat/working_memory/memory_manager.py b/src/chat/focus_chat/working_memory/memory_manager.py index 1015207aa..4d18eae81 100644 --- a/src/chat/focus_chat/working_memory/memory_manager.py +++ b/src/chat/focus_chat/working_memory/memory_manager.py @@ -510,11 +510,15 @@ class MemoryManager: # 如果有摘要信息,添加到提示中 if summary1: prompt += f"记忆1主题:{summary1['brief']}\n" - prompt += "记忆1关键要点:\n" + "\n".join([f"- {point}" for point in summary1.get("key_points", [])]) + "\n\n" + prompt += ( + "记忆1关键要点:\n" + "\n".join([f"- {point}" for point in summary1.get("key_points", [])]) + "\n\n" + ) if summary2: prompt += f"记忆2主题:{summary2['brief']}\n" - prompt += "记忆2关键要点:\n" + "\n".join([f"- {point}" for point in summary2.get("key_points", [])]) + "\n\n" + prompt += ( + "记忆2关键要点:\n" + "\n".join([f"- {point}" for point in summary2.get("key_points", [])]) + "\n\n" + ) # 添加记忆原始内容 prompt += f""" diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 4ed9174c6..3ceebc9e2 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -636,4 +636,4 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n" else: # mode = "lite" or unknown # 只返回时分秒格式,喵~ - return time.strftime("%H:%M:%S", time.localtime(timestamp)) \ No newline at end of file + return time.strftime("%H:%M:%S", time.localtime(timestamp)) diff --git a/src/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index 9dcd644a9..087584535 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -184,9 +184,7 @@ class ImageManager: return f"[图片:{cached_description}]" # 调用AI获取描述 - prompt = ( - "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来,请留意其主题,直观感受,以及是否有擦边色情内容。最多100个字。" - ) + prompt = "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来,请留意其主题,直观感受,以及是否有擦边色情内容。最多100个字。" description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) if description is None: diff --git a/src/common/remote.py b/src/common/remote.py index 23cb5cf68..064a07cb0 100644 --- a/src/common/remote.py +++ b/src/common/remote.py @@ -90,7 +90,9 @@ class TelemetryHeartBeatTask(AsyncTask): else: logger.error("无效的服务端响应") else: - logger.error(f"请求UUID失败,不过你还是可以正常使用麦麦,状态码: {response.status_code}, 响应内容: {response.text}") + logger.error( + f"请求UUID失败,不过你还是可以正常使用麦麦,状态码: {response.status_code}, 响应内容: {response.text}" + ) # 请求失败,重试次数+1 try_count += 1 diff --git a/src/config/config_base.py b/src/config/config_base.py index ba6519f7d..6c414f0b2 100644 --- a/src/config/config_base.py +++ b/src/config/config_base.py @@ -79,7 +79,11 @@ class ConfigBase: if field_origin_type is list: # 如果列表元素类型是ConfigBase的子类,则对每个元素调用from_dict - if field_type_args and isinstance(field_type_args[0], type) and issubclass(field_type_args[0], ConfigBase): + if ( + field_type_args + and isinstance(field_type_args[0], type) + and issubclass(field_type_args[0], ConfigBase) + ): return [field_type_args[0].from_dict(item) for item in value] return [cls._convert_field(item, field_type_args[0]) for item in value] elif field_origin_type is set: