From a445c222505cd7ff724ffdea93a0ba2ac6eb2d5b Mon Sep 17 00:00:00 2001 From: KawaiiYusora Date: Thu, 6 Mar 2025 01:08:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=B9=8B=E6=88=91=E7=9A=84LLM?= =?UTF-8?q?=E4=B8=BA=E4=BB=80=E4=B9=88=E5=8F=AA=E6=9C=89=E4=B8=80=E5=8D=8A?= =?UTF-8?q?TAG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 3ba873d74..c7dbc6ffd 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -36,6 +36,7 @@ class LLM_request: data = { "model": self.model_name, "messages": [{"role": "user", "content": prompt}], + "max_tokens": 8000, **self.params } @@ -65,10 +66,10 @@ class LLM_request: think_match = None reasoning_content = message.get("reasoning_content", "") if not reasoning_content: - think_match = re.search(r'(.*?)', content, re.DOTALL) + think_match = re.search(r'(?:)?(.*?)', content, re.DOTALL) if think_match: reasoning_content = think_match.group(1).strip() - content = re.sub(r'.*?', '', content, flags=re.DOTALL).strip() + content = re.sub(r'(?:)?.*?', '', content, flags=re.DOTALL, count=1).strip() return content, reasoning_content return "没有返回结果", "" @@ -112,9 +113,10 @@ class LLM_request: ] } ], + "max_tokens": 8000, **self.params } - + # 发送请求到完整的chat/completions端点 api_url = f"{self.base_url.rstrip('/')}/chat/completions" @@ -122,9 +124,9 @@ class LLM_request: max_retries = 3 base_wait_time = 15 - + current_image_base64 = image_base64 - + for retry in range(max_retries): try: @@ -141,7 +143,7 @@ class LLM_request: logger.warning("图片太大(413),尝试压缩...") current_image_base64 = compress_base64_image_by_scale(current_image_base64) continue - + response.raise_for_status() # 检查其他响应状态 result = await response.json() @@ -151,10 +153,10 @@ class LLM_request: think_match = None reasoning_content = message.get("reasoning_content", "") if not reasoning_content: - think_match = re.search(r'(.*?)', content, re.DOTALL) + think_match = re.search(r'(?:)?(.*?)', content, re.DOTALL) if think_match: reasoning_content = think_match.group(1).strip() - content = re.sub(r'.*?', '', content, flags=re.DOTALL).strip() + content = re.sub(r'(?:)?.*?', '', content, flags=re.DOTALL, count=1).strip() return content, reasoning_content return "没有返回结果", "" @@ -197,6 +199,7 @@ class LLM_request: ] } ], + "max_tokens": 8000, **self.params } @@ -226,10 +229,10 @@ class LLM_request: think_match = None reasoning_content = message.get("reasoning_content", "") if not reasoning_content: - think_match = re.search(r'(.*?)', content, re.DOTALL) + think_match = re.search(r'(?:)?(.*?)', content, re.DOTALL) if think_match: reasoning_content = think_match.group(1).strip() - content = re.sub(r'.*?', '', content, flags=re.DOTALL).strip() + content = re.sub(r'(?:)?.*?', '', content, flags=re.DOTALL, count=1).strip() return content, reasoning_content return "没有返回结果", "" From eaa711ada78d5a113caf4ecded75d19edeeb362e Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 6 Mar 2025 14:27:22 +0800 Subject: [PATCH 2/3] v0.5.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记忆系统接入关键词,重新启动自主发言功能 --- src/plugins/chat/bot.py | 32 ++--- src/plugins/chat/config.py | 4 + src/plugins/chat/llm_generator.py | 5 +- src/plugins/chat/message_sender.py | 10 +- src/plugins/chat/prompt_builder.py | 115 ++++++---------- src/plugins/chat/topic_identifier.py | 4 +- src/plugins/chat/utils.py | 63 ++++++++- src/plugins/chat/willing_manager.py | 6 +- src/plugins/memory_system/memory.py | 197 ++++++++++++++++++++++++++- src/plugins/models/utils_model.py | 196 ++++++++++++++++++++++++++ 10 files changed, 520 insertions(+), 112 deletions(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 398cb37e3..dc82cf236 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -15,7 +15,7 @@ from .message import Message_Thinking # 导入 Message_Thinking 类 from .relationship_manager import relationship_manager from .willing_manager import willing_manager # 导入意愿管理器 from .utils import is_mentioned_bot_in_txt, calculate_typing_time -from ..memory_system.memory import memory_graph +from ..memory_system.memory import memory_graph,hippocampus from loguru import logger class ChatBot: @@ -70,24 +70,12 @@ class ChatBot: - topic=await topic_identifier.identify_topic_llm(message.processed_plain_text) - - - # topic1 = topic_identifier.identify_topic_jieba(message.processed_plain_text) - # topic2 = await topic_identifier.identify_topic_llm(message.processed_plain_text) - # topic3 = topic_identifier.identify_topic_snownlp(message.processed_plain_text) - logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") - - all_num = 0 - interested_num = 0 - if topic: - for current_topic in topic: - all_num += 1 - first_layer_items, second_layer_items = memory_graph.get_related_item(current_topic, depth=2) - if first_layer_items: - interested_num += 1 - print(f"\033[1;32m[前额叶]\033[0m 对|{current_topic}|有印象") - interested_rate = interested_num / all_num if all_num > 0 else 0 + # topic=await topic_identifier.identify_topic_llm(message.processed_plain_text) + topic = '' + interested_rate = 0 + interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text)/100 + print(f"\033[1;32m[记忆激活]\033[0m 对{message.processed_plain_text}的激活度:---------------------------------------{interested_rate}\n") + # logger.info(f"\033[1;32m[主题识别]\033[0m 使用{global_config.topic_extract}主题: {topic}") await self.storage.store_message(message, topic[0] if topic else None) @@ -134,7 +122,7 @@ class ChatBot: if isinstance(msg, Message_Thinking) and msg.message_id == think_id: thinking_message = msg container.messages.remove(msg) - print(f"\033[1;32m[思考消息删除]\033[0m 已找到思考消息对象,开始删除") + # print(f"\033[1;32m[思考消息删除]\033[0m 已找到思考消息对象,开始删除") break #记录开始思考的时间,避免从思考到回复的时间太久 @@ -167,7 +155,7 @@ class ChatBot: message_set.add_message(bot_message) #message_set 可以直接加入 message_manager - print(f"\033[1;32m[回复]\033[0m 将回复载入发送容器") + # print(f"\033[1;32m[回复]\033[0m 将回复载入发送容器") message_manager.add_message(message_set) bot_response_time = tinking_time_point @@ -205,7 +193,7 @@ class ChatBot: ) message_manager.add_message(bot_message) - willing_manager.change_reply_willing_after_sent(event.group_id) + # willing_manager.change_reply_willing_after_sent(event.group_id) # 创建全局ChatBot实例 chat_bot = ChatBot() \ No newline at end of file diff --git a/src/plugins/chat/config.py b/src/plugins/chat/config.py index be599f48a..d5ee364ce 100644 --- a/src/plugins/chat/config.py +++ b/src/plugins/chat/config.py @@ -40,6 +40,7 @@ class BotConfig: llm_normal_minor: Dict[str, str] = field(default_factory=lambda: {}) embedding: Dict[str, str] = field(default_factory=lambda: {}) vlm: Dict[str, str] = field(default_factory=lambda: {}) + rerank: Dict[str, str] = field(default_factory=lambda: {}) # 主题提取配置 topic_extract: str = 'snownlp' # 只支持jieba,snownlp,llm @@ -136,6 +137,9 @@ class BotConfig: if "embedding" in model_config: config.embedding = model_config["embedding"] + if "rerank" in model_config: + config.rerank = model_config["rerank"] + if 'topic' in toml_dict: topic_config=toml_dict['topic'] if 'topic_extract' in topic_config: diff --git a/src/plugins/chat/llm_generator.py b/src/plugins/chat/llm_generator.py index ab0f4e12c..004cd0450 100644 --- a/src/plugins/chat/llm_generator.py +++ b/src/plugins/chat/llm_generator.py @@ -63,10 +63,11 @@ class ResponseGenerator: # 获取关系值 relationship_value = relationship_manager.get_relationship(message.user_id).relationship_value if relationship_manager.get_relationship(message.user_id) else 0.0 if relationship_value != 0.0: - print(f"\033[1;32m[关系管理]\033[0m 回复中_当前关系值: {relationship_value}") + # print(f"\033[1;32m[关系管理]\033[0m 回复中_当前关系值: {relationship_value}") + pass # 构建prompt - prompt, prompt_check = prompt_builder._build_prompt( + prompt, prompt_check = await prompt_builder._build_prompt( message_txt=message.processed_plain_text, sender_name=sender_name, relationship_value=relationship_value, diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index 970fd3682..c81dec1bb 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -103,7 +103,7 @@ class MessageContainer: def add_message(self, message: Union[Message_Thinking, Message_Sending]) -> None: """添加消息到队列""" - print(f"\033[1;32m[添加消息]\033[0m 添加消息到对应群") + # print(f"\033[1;32m[添加消息]\033[0m 添加消息到对应群") if isinstance(message, MessageSet): for single_message in message.messages: self.messages.append(single_message) @@ -156,17 +156,13 @@ class MessageManager: #最早的对象,可能是思考消息,也可能是发送消息 message_earliest = container.get_earliest_message() #一个message_thinking or message_sending - #一个月后删了 - if not message_earliest: - print(f"\033[1;34m[BUG,如果出现这个,说明有BUG,3月4日留]\033[0m ") - return - #如果是思考消息 if isinstance(message_earliest, Message_Thinking): #优先等待这条消息 message_earliest.update_thinking_time() thinking_time = message_earliest.thinking_time - print(f"\033[1;34m[调试]\033[0m 消息正在思考中,已思考{int(thinking_time)}秒") + if thinking_time % 10 == 0: + print(f"\033[1;34m[调试]\033[0m 消息正在思考中,已思考{int(thinking_time)}秒") else:# 如果不是message_thinking就只能是message_sending print(f"\033[1;34m[调试]\033[0m 消息'{message_earliest.processed_plain_text}'正在发送中") #直接发,等什么呢 diff --git a/src/plugins/chat/prompt_builder.py b/src/plugins/chat/prompt_builder.py index ba22a403d..1c510e251 100644 --- a/src/plugins/chat/prompt_builder.py +++ b/src/plugins/chat/prompt_builder.py @@ -2,13 +2,15 @@ import time import random from ..schedule.schedule_generator import bot_schedule import os -from .utils import get_embedding, combine_messages, get_recent_group_detailed_plain_text +from .utils import get_embedding, combine_messages, get_recent_group_detailed_plain_text,find_similar_topics from ...common.database import Database from .config import global_config from .topic_identifier import topic_identifier -from ..memory_system.memory import memory_graph +from ..memory_system.memory import memory_graph,hippocampus from random import choice - +import numpy as np +import jieba +from collections import Counter class PromptBuilder: def __init__(self): @@ -16,7 +18,9 @@ class PromptBuilder: self.activate_messages = '' self.db = Database.get_instance() - def _build_prompt(self, + + + async def _build_prompt(self, message_txt: str, sender_name: str = "某人", relationship_value: float = 0.0, @@ -31,60 +35,7 @@ class PromptBuilder: Returns: str: 构建好的prompt - """ - - - memory_prompt = '' - start_time = time.time() # 记录开始时间 - # topic = await topic_identifier.identify_topic_llm(message_txt) - topic = topic_identifier.identify_topic_snownlp(message_txt) - - # print(f"\033[1;32m[pb主题识别]\033[0m 主题: {topic}") - - all_first_layer_items = [] # 存储所有第一层记忆 - all_second_layer_items = {} # 用字典存储每个topic的第二层记忆 - overlapping_second_layer = set() # 存储重叠的第二层记忆 - - if topic: - # 遍历所有topic - for current_topic in topic: - first_layer_items, second_layer_items = memory_graph.get_related_item(current_topic, depth=2) - # if first_layer_items: - # print(f"\033[1;32m[前额叶]\033[0m 主题 '{current_topic}' 的第一层记忆: {first_layer_items}") - - # 记录第一层数据 - all_first_layer_items.extend(first_layer_items) - - # 记录第二层数据 - all_second_layer_items[current_topic] = second_layer_items - - # 检查是否有重叠的第二层数据 - for other_topic, other_second_layer in all_second_layer_items.items(): - if other_topic != current_topic: - # 找到重叠的记忆 - overlap = set(second_layer_items) & set(other_second_layer) - if overlap: - # print(f"\033[1;32m[前额叶]\033[0m 发现主题 '{current_topic}' 和 '{other_topic}' 有共同的第二层记忆: {overlap}") - overlapping_second_layer.update(overlap) - - selected_first_layer = random.sample(all_first_layer_items, min(2, len(all_first_layer_items))) if all_first_layer_items else [] - selected_second_layer = random.sample(list(overlapping_second_layer), min(2, len(overlapping_second_layer))) if overlapping_second_layer else [] - - # 合并并去重 - all_memories = list(set(selected_first_layer + selected_second_layer)) - if all_memories: - print(f"\033[1;32m[前额叶]\033[0m 合并所有需要的记忆: {all_memories}") - random_item = " ".join(all_memories) - memory_prompt = f"看到这些聊天,你想起来{random_item}\n" - else: - memory_prompt = "" # 如果没有记忆,则返回空字符串 - - end_time = time.time() # 记录结束时间 - print(f"\033[1;32m[回忆耗时]\033[0m 耗时: {(end_time - start_time):.3f}秒") # 输出耗时 - - - - + """ #先禁用关系 if 0 > 30: relation_prompt = "关系特别特别好,你很喜欢喜欢他" @@ -112,22 +63,48 @@ class PromptBuilder: prompt_info = self.get_prompt_info(message_txt,threshold=0.5) if prompt_info: prompt_info = f'''\n----------------------------------------------------\n你有以下这些[知识]:\n{prompt_info}\n请你记住上面的[知识],之后可能会用到\n----------------------------------------------------\n''' - # promt_info_prompt = '你有一些[知识],在上面可以参考。' end_time = time.time() print(f"\033[1;32m[知识检索]\033[0m 耗时: {(end_time - start_time):.3f}秒") - # print(f"\033[1;34m[调试]\033[0m 获取知识库内容结果: {prompt_info}") - - # print(f"\033[1;34m[调试信息]\033[0m 正在构建聊天上下文") - + # 获取聊天上下文 chat_talking_prompt = '' if group_id: chat_talking_prompt = get_recent_group_detailed_plain_text(self.db, group_id, limit=global_config.MAX_CONTEXT_SIZE,combine = True) chat_talking_prompt = f"以下是群里正在聊天的内容:\n{chat_talking_prompt}" - # print(f"\033[1;34m[调试]\033[0m 已从数据库获取群 {group_id} 的消息记录:{chat_talking_prompt}") + + + # 使用新的记忆获取方法 + memory_prompt = '' + start_time = time.time() + + # 调用 hippocampus 的 get_relevant_memories 方法 + relevant_memories = await hippocampus.get_relevant_memories( + text=message_txt, + max_topics=5, + similarity_threshold=0.4 + ) + + if relevant_memories: + # 格式化记忆内容 + memory_items = [] + for memory in relevant_memories: + memory_items.append(f"关于「{memory['topic']}」的记忆:{memory['content']}") + + memory_prompt = f"看到这些聊天,你想起来:\n" + "\n".join(memory_items) + "\n" + + # 打印调试信息 + print("\n\033[1;32m[记忆检索]\033[0m 找到以下相关记忆:") + for memory in relevant_memories: + print(f"- 主题「{memory['topic']}」[相似度: {memory['similarity']:.2f}]: {memory['content']}") + + end_time = time.time() + print(f"\033[1;32m[回忆耗时]\033[0m 耗时: {(end_time - start_time):.3f}秒") + + + #激活prompt构建 activate_prompt = '' activate_prompt = f"以上是群里正在进行的聊天,{memory_prompt} 现在昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},你想要{relation_prompt_2}。" @@ -162,29 +139,19 @@ class PromptBuilder: if random.random() < 0.01: prompt_ger += '你喜欢用文言文' - #额外信息要求 extra_info = '''但是记得回复平淡一些,简短一些,尤其注意在没明确提到时不要过多提及自身的背景, 记住不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只需要输出回复内容就好,不要输出其他任何内容''' - - #合并prompt prompt = "" prompt += f"{prompt_info}\n" prompt += f"{prompt_date}\n" prompt += f"{chat_talking_prompt}\n" - - # prompt += f"{memory_prompt}\n" - - # prompt += f"{activate_prompt}\n" prompt += f"{prompt_personality}\n" prompt += f"{prompt_ger}\n" prompt += f"{extra_info}\n" - - '''读空气prompt处理''' - activate_prompt_check=f"以上是群里正在进行的聊天,昵称为 '{sender_name}' 的用户说的:{message_txt}。引起了你的注意,你和他{relation_prompt},你想要{relation_prompt_2},但是这不一定是合适的时机,请你决定是否要回应这条消息。" prompt_personality_check = '' extra_check_info=f"请注意把握群里的聊天内容的基础上,综合群内的氛围,例如,和{global_config.BOT_NICKNAME}相关的话题要积极回复,如果是at自己的消息一定要回复,如果自己正在和别人聊天一定要回复,其他话题如果合适搭话也可以回复,如果认为应该回复请输出yes,否则输出no,请注意是决定是否需要回复,而不是编写回复内容,除了yes和no不要输出任何回复内容。" diff --git a/src/plugins/chat/topic_identifier.py b/src/plugins/chat/topic_identifier.py index 812d4e321..60c5b0051 100644 --- a/src/plugins/chat/topic_identifier.py +++ b/src/plugins/chat/topic_identifier.py @@ -42,7 +42,7 @@ class TopicIdentifier: print(f"\033[1;32m[主题识别]\033[0m 主题: {topic_list}") return topic_list if topic_list else None - def identify_topic_snownlp(self, text: str) -> Optional[List[str]]: + def identify_topic_snownlp(self, text: str,num:int=5) -> Optional[List[str]]: """使用 SnowNLP 进行主题识别 Args: @@ -57,7 +57,7 @@ class TopicIdentifier: try: s = SnowNLP(text) # 提取前3个关键词作为主题 - keywords = s.keywords(5) + keywords = s.keywords(num) return keywords if keywords else None except Exception as e: print(f"\033[1;31m[错误]\033[0m SnowNLP 处理失败: {str(e)}") diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index aa16268ef..ddc698bc7 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -11,6 +11,8 @@ from collections import Counter import math from nonebot import get_driver from ..models.utils_model import LLM_request +import aiohttp +import jieba driver = get_driver() config = driver.config @@ -117,7 +119,7 @@ def get_cloest_chat_from_db(db, length: int, timestamp: str): chat_text += record["detailed_plain_text"] return chat_text - print(f"消息已读取3次,跳过") + # print(f"消息已读取3次,跳过") return '' def get_recent_group_messages(db, group_id: int, limit: int = 12) -> list: @@ -421,3 +423,62 @@ def calculate_typing_time(input_string: str, chinese_time: float = 0.2, english_ return total_time +def find_similar_topics(message_txt: str, all_memory_topic: list, top_k: int = 5) -> list: + """使用重排序API找出与输入文本最相似的话题 + + Args: + message_txt: 输入文本 + all_memory_topic: 所有记忆主题列表 + top_k: 返回最相似的话题数量 + + Returns: + list: 最相似话题列表及其相似度分数 + """ + + if not all_memory_topic: + return [] + + try: + llm = LLM_request(model=global_config.rerank) + return llm.rerank_sync(message_txt, all_memory_topic, top_k) + except Exception as e: + print(f"重排序API调用出错: {str(e)}") + return [] + +def cosine_similarity(v1, v2): + """计算余弦相似度""" + dot_product = np.dot(v1, v2) + norm1 = np.linalg.norm(v1) + norm2 = np.linalg.norm(v2) + if norm1 == 0 or norm2 == 0: + return 0 + return dot_product / (norm1 * norm2) + +def text_to_vector(text): + """将文本转换为词频向量""" + # 分词 + words = jieba.lcut(text) + # 统计词频 + word_freq = Counter(words) + return word_freq + +def find_similar_topics_simple(text: str, topics: list, top_k: int = 5) -> list: + """使用简单的余弦相似度计算文本相似度""" + # 将输入文本转换为词频向量 + text_vector = text_to_vector(text) + + # 计算每个主题的相似度 + similarities = [] + for topic in topics: + topic_vector = text_to_vector(topic) + # 获取所有唯一词 + all_words = set(text_vector.keys()) | set(topic_vector.keys()) + # 构建向量 + v1 = [text_vector.get(word, 0) for word in all_words] + v2 = [topic_vector.get(word, 0) for word in all_words] + # 计算相似度 + similarity = cosine_similarity(v1, v2) + similarities.append((topic, similarity)) + + # 按相似度降序排序并返回前k个 + return sorted(similarities, key=lambda x: x[1], reverse=True)[:top_k] \ No newline at end of file diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py index 7559406f9..acc8543da 100644 --- a/src/plugins/chat/willing_manager.py +++ b/src/plugins/chat/willing_manager.py @@ -37,13 +37,13 @@ class WillingManager: current_willing *= 0.15 print(f"表情包, 当前意愿: {current_willing}") - if interested_rate > 0.65: + if interested_rate > 0.4: print(f"兴趣度: {interested_rate}, 当前意愿: {current_willing}") - current_willing += interested_rate-0.6 + current_willing += interested_rate-0.1 self.group_reply_willing[group_id] = min(current_willing, 3.0) - reply_probability = max((current_willing - 0.55) * 1.9, 0) + reply_probability = max((current_willing - 0.45) * 2, 0) if group_id not in config.talk_allowed_groups: current_willing = 0 reply_probability = 0 diff --git a/src/plugins/memory_system/memory.py b/src/plugins/memory_system/memory.py index 4d20d05a9..cdb6e6e1b 100644 --- a/src/plugins/memory_system/memory.py +++ b/src/plugins/memory_system/memory.py @@ -11,7 +11,7 @@ from ..chat.config import global_config from ...common.database import Database # 使用正确的导入语法 from ..models.utils_model import LLM_request import math -from ..chat.utils import calculate_information_content, get_cloest_chat_from_db +from ..chat.utils import calculate_information_content, get_cloest_chat_from_db ,find_similar_topics,text_to_vector,cosine_similarity @@ -135,6 +135,14 @@ class Hippocampus: self.llm_model_get_topic = LLM_request(model = global_config.llm_normal_minor,temperature=0.5) self.llm_model_summary = LLM_request(model = global_config.llm_normal,temperature=0.5) + def get_all_node_names(self) -> list: + """获取记忆图中所有节点的名字列表 + + Returns: + list: 包含所有节点名字的列表 + """ + return list(self.memory_graph.G.nodes()) + def calculate_node_hash(self, concept, memory_items): """计算节点的特征值""" if not isinstance(memory_items, list): @@ -483,6 +491,193 @@ class Hippocampus: prompt = f'这是一段文字:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,可以包含时间和人物,以及具体的观点。只输出这句话就好' return prompt + async def _identify_topics(self, text: str) -> list: + """从文本中识别可能的主题 + + Args: + text: 输入文本 + + Returns: + list: 识别出的主题列表 + """ + topics_response = await self.llm_model_get_topic.generate_response(self.find_topic_llm(text, 5)) + print(f"话题: {topics_response[0]}") + topics = [topic.strip() for topic in topics_response[0].replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if topic.strip()] + print(f"话题: {topics}") + + return topics + + def _find_similar_topics(self, topics: list, similarity_threshold: float = 0.4, debug_info: str = "") -> list: + """查找与给定主题相似的记忆主题 + + Args: + topics: 主题列表 + similarity_threshold: 相似度阈值 + debug_info: 调试信息前缀 + + Returns: + list: (主题, 相似度) 元组列表 + """ + all_memory_topics = self.get_all_node_names() + all_similar_topics = [] + + # 计算每个识别出的主题与记忆主题的相似度 + for topic in topics: + if debug_info: + print(f"\033[1;32m[{debug_info}]\033[0m 正在思考有没有见过: {topic}") + + topic_vector = text_to_vector(topic) + has_similar_topic = False + + for memory_topic in all_memory_topics: + memory_vector = text_to_vector(memory_topic) + # 获取所有唯一词 + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + # 构建向量 + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + # 计算相似度 + similarity = cosine_similarity(v1, v2) + + if similarity >= similarity_threshold: + has_similar_topic = True + if debug_info: + print(f"\033[1;32m[{debug_info}]\033[0m 找到相似主题: {topic} -> {memory_topic} (相似度: {similarity:.2f})") + all_similar_topics.append((memory_topic, similarity)) + + if not has_similar_topic and debug_info: + print(f"\033[1;31m[{debug_info}]\033[0m 没有见过: {topic} ,呃呃") + + return all_similar_topics + + def _get_top_topics(self, similar_topics: list, max_topics: int = 5) -> list: + """获取相似度最高的主题 + + Args: + similar_topics: (主题, 相似度) 元组列表 + max_topics: 最大主题数量 + + Returns: + list: (主题, 相似度) 元组列表 + """ + seen_topics = set() + top_topics = [] + + for topic, score in sorted(similar_topics, key=lambda x: x[1], reverse=True): + if topic not in seen_topics and len(top_topics) < max_topics: + seen_topics.add(topic) + top_topics.append((topic, score)) + + return top_topics + + async def memory_activate_value(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.3) -> int: + """计算输入文本对记忆的激活程度""" + print(f"\033[1;32m[记忆激活]\033[0m 开始计算文本的记忆激活度: {text}") + + # 识别主题 + identified_topics = await self._identify_topics(text) + print(f"\033[1;32m[记忆激活]\033[0m 识别出的主题: {identified_topics}") + + if not identified_topics: + print(f"\033[1;32m[记忆激活]\033[0m 未识别出主题,返回0") + return 0 + + # 查找相似主题 + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆激活" + ) + + if not all_similar_topics: + print(f"\033[1;32m[记忆激活]\033[0m 未找到相似主题,返回0") + return 0 + + # 获取最相关的主题 + top_topics = self._get_top_topics(all_similar_topics, max_topics) + + # 如果只找到一个主题,进行惩罚 + if len(top_topics) == 1: + topic, score = top_topics[0] + activation = int(score * 50) # 单主题情况下,直接用相似度*50作为激活值 + print(f"\033[1;32m[记忆激活]\033[0m 只找到一个主题,进行惩罚:") + print(f"\033[1;32m[记忆激活]\033[0m - 主题: {topic}") + print(f"\033[1;32m[记忆激活]\033[0m - 相似度: {score:.3f}") + print(f"\033[1;32m[记忆激活]\033[0m - 最终激活值: {activation}") + return activation + + # 计算关键词匹配率 + matched_topics = set() + topic_similarities = {} + + print(f"\033[1;32m[记忆激活]\033[0m 计算关键词匹配情况:") + for memory_topic, similarity in top_topics: + # 对每个记忆主题,检查它与哪些输入主题相似 + for input_topic in identified_topics: + topic_vector = text_to_vector(input_topic) + memory_vector = text_to_vector(memory_topic) + all_words = set(topic_vector.keys()) | set(memory_vector.keys()) + v1 = [topic_vector.get(word, 0) for word in all_words] + v2 = [memory_vector.get(word, 0) for word in all_words] + sim = cosine_similarity(v1, v2) + if sim >= similarity_threshold: + matched_topics.add(input_topic) + topic_similarities[input_topic] = max(topic_similarities.get(input_topic, 0), sim) + print(f"\033[1;32m[记忆激活]\033[0m - 输入主题「{input_topic}」匹配到记忆「{memory_topic}」, 相似度: {sim:.3f}") + + # 计算主题匹配率 + topic_match = len(matched_topics) / len(identified_topics) + print(f"\033[1;32m[记忆激活]\033[0m 主题匹配率:") + print(f"\033[1;32m[记忆激活]\033[0m - 匹配主题数: {len(matched_topics)}") + print(f"\033[1;32m[记忆激活]\033[0m - 总主题数: {len(identified_topics)}") + print(f"\033[1;32m[记忆激活]\033[0m - 匹配率: {topic_match:.3f}") + + # 计算匹配主题的平均相似度 + average_similarities = sum(topic_similarities.values()) / len(topic_similarities) if topic_similarities else 0 + print(f"\033[1;32m[记忆激活]\033[0m 平均相似度:") + print(f"\033[1;32m[记忆激活]\033[0m - 各主题相似度: {[f'{k}:{v:.3f}' for k,v in topic_similarities.items()]}") + print(f"\033[1;32m[记忆激活]\033[0m - 平均相似度: {average_similarities:.3f}") + + # 计算最终激活值 + activation = (topic_match + average_similarities) / 2 * 100 + print(f"\033[1;32m[记忆激活]\033[0m 最终激活值: {int(activation)}") + + return int(activation) + + async def get_relevant_memories(self, text: str, max_topics: int = 5, similarity_threshold: float = 0.4) -> list: + """根据输入文本获取相关的记忆内容""" + # 识别主题 + identified_topics = await self._identify_topics(text) + + # 查找相似主题 + all_similar_topics = self._find_similar_topics( + identified_topics, + similarity_threshold=similarity_threshold, + debug_info="记忆检索" + ) + + # 获取最相关的主题 + relevant_topics = self._get_top_topics(all_similar_topics, max_topics) + + # 获取相关记忆内容 + relevant_memories = [] + for topic, score in relevant_topics: + # 获取该主题的记忆内容 + first_layer, _ = self.memory_graph.get_related_item(topic, depth=1) + if first_layer: + # 为每条记忆添加来源主题和相似度信息 + for memory in first_layer: + relevant_memories.append({ + 'topic': topic, + 'similarity': score, + 'content': memory + }) + + # 按相似度排序 + relevant_memories.sort(key=lambda x: x['similarity'], reverse=True) + + return relevant_memories + def segment_text(text): seg_text = list(jieba.cut(text)) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 57a0acb55..cad23ab09 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -424,3 +424,199 @@ class LLM_request: logger.error("达到最大重试次数,embedding请求仍然失败") return None + + def rerank_sync(self, query: str, documents: list, top_k: int = 5) -> list: + """同步方法:使用重排序API对文档进行排序 + + Args: + query: 查询文本 + documents: 待排序的文档列表 + top_k: 返回前k个结果 + + Returns: + list: [(document, score), ...] 格式的结果列表 + """ + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + data = { + "model": self.model_name, + "query": query, + "documents": documents, + "top_n": top_k, + "return_documents": True, + } + + api_url = f"{self.base_url.rstrip('/')}/rerank" + logger.info(f"发送请求到URL: {api_url}") + + max_retries = 2 + base_wait_time = 6 + + for retry in range(max_retries): + try: + response = requests.post(api_url, headers=headers, json=data, timeout=30) + + if response.status_code == 429: + wait_time = base_wait_time * (2 ** retry) + logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") + time.sleep(wait_time) + continue + + if response.status_code in [500, 503]: + wait_time = base_wait_time * (2 ** retry) + logger.error(f"服务器错误({response.status_code}),等待{wait_time}秒后重试...") + if retry < max_retries - 1: + time.sleep(wait_time) + continue + else: + # 如果是最后一次重试,尝试使用chat/completions作为备选方案 + return self._fallback_rerank_with_chat(query, documents, top_k) + + response.raise_for_status() + + result = response.json() + if 'results' in result: + return [(item["document"], item["score"]) for item in result["results"]] + return [] + + except Exception as e: + if retry < max_retries - 1: + wait_time = base_wait_time * (2 ** retry) + logger.error(f"[rerank_sync]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}", exc_info=True) + time.sleep(wait_time) + else: + logger.critical(f"重排序请求失败: {str(e)}", exc_info=True) + + logger.error("达到最大重试次数,重排序请求仍然失败") + return [] + + async def rerank(self, query: str, documents: list, top_k: int = 5) -> list: + """异步方法:使用重排序API对文档进行排序 + + Args: + query: 查询文本 + documents: 待排序的文档列表 + top_k: 返回前k个结果 + + Returns: + list: [(document, score), ...] 格式的结果列表 + """ + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + data = { + "model": self.model_name, + "query": query, + "documents": documents, + "top_n": top_k, + "return_documents": True, + } + + api_url = f"{self.base_url.rstrip('/')}/v1/rerank" + logger.info(f"发送请求到URL: {api_url}") + + max_retries = 3 + base_wait_time = 15 + + for retry in range(max_retries): + try: + async with aiohttp.ClientSession() as session: + async with session.post(api_url, headers=headers, json=data) as response: + if response.status == 429: + wait_time = base_wait_time * (2 ** retry) + logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...") + await asyncio.sleep(wait_time) + continue + + if response.status in [500, 503]: + wait_time = base_wait_time * (2 ** retry) + logger.error(f"服务器错误({response.status}),等待{wait_time}秒后重试...") + if retry < max_retries - 1: + await asyncio.sleep(wait_time) + continue + else: + # 如果是最后一次重试,尝试使用chat/completions作为备选方案 + return await self._fallback_rerank_with_chat_async(query, documents, top_k) + + response.raise_for_status() + + result = await response.json() + if 'results' in result: + return [(item["document"], item["score"]) for item in result["results"]] + return [] + + except Exception as e: + if retry < max_retries - 1: + wait_time = base_wait_time * (2 ** retry) + logger.error(f"[rerank]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}", exc_info=True) + await asyncio.sleep(wait_time) + else: + logger.critical(f"重排序请求失败: {str(e)}", exc_info=True) + # 作为最后的备选方案,尝试使用chat/completions + return await self._fallback_rerank_with_chat_async(query, documents, top_k) + + logger.error("达到最大重试次数,重排序请求仍然失败") + return [] + + async def _fallback_rerank_with_chat_async(self, query: str, documents: list, top_k: int = 5) -> list: + """当rerank API失败时的备选方案,使用chat/completions异步实现重排序 + + Args: + query: 查询文本 + documents: 待排序的文档列表 + top_k: 返回前k个结果 + + Returns: + list: [(document, score), ...] 格式的结果列表 + """ + try: + logger.info("使用chat/completions作为重排序的备选方案") + + # 构建提示词 + prompt = f"""请对以下文档列表进行重排序,按照与查询的相关性从高到低排序。 +查询: {query} + +文档列表: +{documents} + +请以JSON格式返回排序结果,格式为: +[{{"document": "文档内容", "score": 相关性分数}}, ...] +只返回JSON,不要其他任何文字。""" + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + data = { + "model": self.model_name, + "messages": [{"role": "user", "content": prompt}], + **self.params + } + + api_url = f"{self.base_url.rstrip('/')}/v1/chat/completions" + + async with aiohttp.ClientSession() as session: + async with session.post(api_url, headers=headers, json=data) as response: + response.raise_for_status() + result = await response.json() + + if "choices" in result and len(result["choices"]) > 0: + message = result["choices"][0]["message"] + content = message.get("content", "") + try: + import json + parsed_content = json.loads(content) + if isinstance(parsed_content, list): + return [(item["document"], item["score"]) for item in parsed_content] + except: + pass + return [] + except Exception as e: + logger.error(f"备选方案也失败了: {str(e)}") + return [] From 2f1579e5b751cc7202cba4fa08eba580dd38a87d Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Thu, 6 Mar 2025 14:43:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E4=B8=BA=E6=89=80=E6=9C=89=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=AF=B7=E6=B1=82=E6=B7=BB=E5=8A=A0=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=A4=B4=E5=92=8C=E8=AF=B7=E6=B1=82=E4=BD=93=E7=9A=84=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 88fd831b8..a3dfdfa9b 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -83,6 +83,7 @@ class LLM_request: await asyncio.sleep(wait_time) else: logger.critical(f"请求失败: {str(e)}", exc_info=True) + logger.critical(f"请求头: {headers} 请求体: {data}") raise RuntimeError(f"API请求失败: {str(e)}") logger.error("达到最大重试次数,请求仍然失败") @@ -170,6 +171,7 @@ class LLM_request: await asyncio.sleep(wait_time) else: logger.critical(f"请求失败: {str(e)}", exc_info=True) + logger.critical(f"请求头: {headers} 请求体: {data}") raise RuntimeError(f"API请求失败: {str(e)}") logger.error("达到最大重试次数,请求仍然失败") @@ -223,6 +225,7 @@ class LLM_request: await asyncio.sleep(wait_time) else: logger.error(f"请求失败: {str(e)}") + logger.critical(f"请求头: {headers} 请求体: {data}") return f"请求失败: {str(e)}", "" logger.error("达到最大重试次数,请求仍然失败") @@ -302,6 +305,7 @@ class LLM_request: time.sleep(wait_time) else: logger.critical(f"请求失败: {str(e)}", exc_info=True) + logger.critical(f"请求头: {headers} 请求体: {data}") raise RuntimeError(f"API请求失败: {str(e)}") logger.error("达到最大重试次数,请求仍然失败") @@ -358,6 +362,7 @@ class LLM_request: time.sleep(wait_time) else: logger.critical(f"embedding请求失败: {str(e)}", exc_info=True) + logger.critical(f"请求头: {headers} 请求体: {data}") return None logger.error("达到最大重试次数,embedding请求仍然失败") @@ -414,6 +419,7 @@ class LLM_request: await asyncio.sleep(wait_time) else: logger.critical(f"embedding请求失败: {str(e)}", exc_info=True) + logger.critical(f"请求头: {headers} 请求体: {data}") return None logger.error("达到最大重试次数,embedding请求仍然失败")