Files
gemini_boy/memory_manager.py
2025-06-06 16:42:11 +08:00

371 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# memory_manager.py
import json
import logging
import uuid # 用于生成短期事件的唯一ID
from config import get_config
import file_manager # 导入文件管理模块来加载/保存features和memory
logger = logging.getLogger(__name__)
def build_system_prompt(current_role_id, current_memory_id, current_global_turn_count):
"""
构建并返回当前会话的系统提示词。
这个提示词会包含AI的静态特征和动态记忆。
:param current_role_id: 当前激活的角色的ID。
:param current_memory_id: 当前激活的记忆集的ID。
:param current_global_turn_count: 当前对话的总轮次用于短期记忆的“last_active_turn”参考。
:return: 组合好的系统提示词字符串。
"""
try:
# 1. 加载当前角色的特征数据
features_data = file_manager.load_active_features(current_role_id)
config_data = get_config()["Session"]
default_role_name = config_data.get("DEFAULT_ROLE_NAME", "通用AI助手")
default_memory_name = config_data.get("DEFAULT_MEMORY_NAME", "通用记忆集")
if not features_data:
logger.warning(f"无法加载角色 '{current_role_id}' 的特征,将使用默认特征。")
features_data = file_manager.DEFAULT_FEATURE_CONTENT.copy()
features_data["角色名称"] = default_role_name
# 2. 加载当前记忆集的记忆数据
memory_data = file_manager.load_active_memory(current_memory_id)
if not memory_data:
logger.warning(
f"无法加载记忆集 '{current_memory_id}' 的记忆,将使用默认空记忆。"
)
memory_data = file_manager.DEFAULT_MEMORY_CONTENT.copy()
memory_data["name"] = default_memory_name
if memory_data["long_term_facts"]:
memory_data["long_term_facts"][
0
] = f"此记忆集名为 '{default_memory_name}'目前未包含特定用户或AI的长期事实。"
else:
memory_data["long_term_facts"].append(
f"此记忆集名为 '{default_memory_name}'目前未包含特定用户或AI的长期事实。"
)
# 将 Python 对象转换为美化后的 JSON 字符串,确保中文不被转义
features_json_str = json.dumps(features_data, ensure_ascii=False, indent=2)
memory_json_str = json.dumps(memory_data, ensure_ascii=False, indent=2)
# 组合最终的系统提示词
# 增加一些说明性文字帮助模型更好理解传入的JSON结构
system_prompt = (
f"以下是你的语言和思维特征,请你严格遵循,并基于以下记忆内容回答问题。\n\n"
f"--- 语言和思维特征 (Features) ---\n"
f"{features_json_str}\n\n"
f"--- 当前记忆 (Memory) ---\n"
f"以下是关于你的长期记忆和短期事件的最新整理,请你充分利用这些信息来理解用户意图、提供个性化回复和维持对话连贯性。\n"
f"当前总对话轮次: {current_global_turn_count}\n" # 传递当前轮次给模型作为last_active_turn的参考
f"{memory_json_str}\n\n"
f"请记,你的所有回答都应体现并遵循上述特征和记忆。你是一个名为“{features_data.get('角色名称', default_role_name)}”的智能体。始终以专业、清晰、友好的态度进行交流,并优先利用现有记忆来提供更个性化和连贯的回答。"
)
logger.debug("系统提示词构建成功。")
return system_prompt
except Exception as e:
logger.error(f"构建系统提示词时发生错误: {e}", exc_info=True)
# 降级为通用提示词并从config中获取默认角色名称
config_data = get_config()["Session"]
default_role_name = config_data.get("DEFAULT_ROLE_NAME", "通用AI助手")
return f"你是一个名为“{default_role_name}”的AI助手旨在提供清晰、准确、友好的回答。"
def build_memory_update_prompt(
recent_chat_history_list, current_memory_data, current_global_turn_count
):
"""
构建用于记忆更新的Prompt (V3)。
:param recent_chat_history_list: 最近N轮对话的列表 (Python list of dicts)。
:param current_memory_data: 当前memory.json内容的字典 (Python dict)。
:param current_global_turn_count: 当前全局对话的总轮次。
:return: 记忆更新指令字符串。
"""
config = get_config()["Application"]
memory_retention_turns = config["MEMORY_RETENTION_TURNS"]
# 将Python对象转换为JSON字符串用于嵌入Prompt
recent_chat_history_json_str = json.dumps(
recent_chat_history_list, ensure_ascii=False, indent=2
)
current_memory_json_str = json.dumps(
current_memory_data, ensure_ascii=False, indent=2
)
prompt = (
"你是一个高级记忆整合与管理系统。你的任务是根据提供的现有记忆和最近的对话,"
"分析其中的关键信息、用户偏好、重要事件、对话中的新概念以及话题状态。\n"
"请你严格按照以下JSON格式和更新规则输出更新后的长短期记忆。\n\n"
"**当前全局对话轮次:** {current_global_turn_count}\n"
"**短期记忆保留阈值:** 超过 {memory_retention_turns} 轮未活跃的短期事件应被考虑移除或晋升。\n\n"
"**更新规则:**\n"
"1. **长期事实 (long_term_facts):**\n"
" - 包含所有对用户或AI角色长期重要、不变、或已确认的关键信息和事实例如用户名字、长期兴趣、职业、已完成的重要事项等\n"
" - **晋升机制:** 从最近对话和短期事件中识别出新的、应晋升为长期记忆的关键事实。判断标准包括:信息被重复提及、用户明确确认、对用户身份或偏好有根本性影响、或事件已圆满结束且其核心结果是永久性的。请将其添加到此列表中。\n"
" - 如果现有长期事实有新的细节补充或需要修正,请更新其内容。\n"
" - 请确保每条事实简洁明了,通常为一句话或一个关键事实片段。避免冗余。\n"
"2. **短期事件 (short_term_events):**\n"
" - 这是一个列表,每个元素代表一个正在进行、最近发生、或仍在讨论中的短期事件或话题。\n"
" - **字段要求:** 每个短期事件对象必须包含以下字段:\n"
" - `id`: (字符串) 唯一标识符。如果是一个新事件请生成一个简短且具有描述性的ID (例如 'AI_ethics_discussion', 'phone_recommendation')要求英文小写下划线分隔最大长度30字符。如果模型没有生成ID或ID不符合格式后端会为其生成一个UUID。\n"
" - `topic`: (字符串) 事件或话题的核心内容简洁概括最多20字。\n"
" - `summary`: (字符串) 对事件的开始、发展和当前状态结果的清晰、简洁描述。请涵盖关键的细节和进展。总结应不超过100字。\n"
" - `status`: (字符串) 事件的当前状态,请选择以下之一:'进行中', '待跟进', '已解决', '已结束'\n"
" - `last_active_turn`: (整数) 最近一次该事件或话题被讨论或有实质进展的全局对话轮次编号(基于`current_global_turn_count`更新)。\n"
" - `mentions_count`: (整数) 该事件或话题被明确提及或有实质进展的累积次数。新事件从1开始。\n"
" - **更新逻辑:**\n"
" - **新增:** 从最近对话中识别出新的、重要的短期事件,并添加到列表中。为其生成一个临时的、描述性的`id``last_active_turn`设为当前全局轮次,`mentions_count`设为1。\n"
" - **更新:** 对于已存在的短期事件,根据最近对话更新其`summary`, `status`。如果它在最近对话中被提及,请更新其`last_active_turn`为当前全局轮次,并增加`mentions_count`。\n"
" - **遗忘/删除:** 如果短期事件中的信息已完全整合进长期事实,或该事件明确已解决(`status`为'已解决''已结束')且无后续,或者其`last_active_turn`距离`current_global_turn_count`过远(超过`memory_retention_turns`),请将其从`short_term_events`列表中移除。\n"
" - **避免重复:** 确保`long_term_facts`和`short_term_events`中的信息不冗余。短期事件应侧重于动态和未完成部分,长期事实则侧重于稳定和核心信息。\n"
" - **保持精简:** `short_term_events`列表的长度应控制在合理范围,优先保留最重要的事件。可以删除重要性较低的已结束或不活跃事件。\n\n"
"--- 现有记忆 (Current Memory JSON) ---\n"
f"{current_memory_json_str}\n\n"
"--- 最近对话 (Recent Chat History JSON - 格式: [{'role': '...', 'parts': [{'text': '...'}]}]) ---\n"
f"{recent_chat_history_json_str}\n\n"
"请现在根据上述信息和规则,输出一个仅包含 'long_term_facts''short_term_events' 两个键的JSON对象。请严格遵循JSON格式不要包含其他任何文本或解释。"
"```json\n"
"{\n"
' "long_term_facts": [\n'
' "用户名是张三。",\n'
' "张三对科幻电影感兴趣,喜欢《银翼杀手》。"\n'
" ],\n"
' "short_term_events": [\n'
" {\n"
' "id": "phone_recommendation",\n'
' "topic": "新手机推荐需求收集",\n'
' "summary": "用户对安卓手机有偏好预算4000-6000元看重拍照和续航。",\n'
' "status": "待跟进",\n'
' "last_active_turn": 10,\n'
' "mentions_count": 3\n'
" },\n"
" {\n"
' "id": "weekend_plan_discussion",\n'
' "topic": "周末活动计划",\n'
' "summary": "用户提到周末想去公园散步,但天气预报有雨,正在考虑备选方案。",\n'
' "status": "进行中",\n'
' "last_active_turn": 12,\n'
' "mentions_count": 1\n'
" }\n"
" ]\n"
"}\n"
"```\n"
)
logger.debug("记忆更新提示词构建成功。")
return prompt
def process_memory_update_response(
model_response_text, current_memory_data, current_global_turn_count
):
"""
处理Gemini模型返回的记忆更新JSON字符串并进行后端逻辑修正和合并。
:param model_response_text: Gemini模型返回的JSON字符串。
:param current_memory_data: 当前内存中的记忆数据 (Python dict)。
:param current_global_turn_count: 当前全局对话轮次。
:return: 更新后的记忆数据 (Python dict)。
"""
config = get_config()["Application"]
memory_retention_turns = config["MEMORY_RETENTION_TURNS"]
max_short_term_events = config["MAX_SHORT_TERM_EVENTS"]
updated_memory = current_memory_data.copy() # 复制当前记忆作为基准
try:
# 尝试解析模型返回的JSON
# 有时模型会返回Markdown代码块需要先提取
if model_response_text.strip().startswith("```json"):
model_response_text = model_response_text.strip()[
7:-3
].strip() # 移除 ```json 和 ```
parsed_response = json.loads(model_response_text)
logger.debug(f"模型返回的记忆更新JSON已解析{parsed_response}")
# 1. 更新长期事实 (long_term_facts)
if "long_term_facts" in parsed_response and isinstance(
parsed_response["long_term_facts"], list
):
updated_memory["long_term_facts"] = list(
set(parsed_response["long_term_facts"])
) # 去重
logger.info(
f"长期事实已更新,共 {len(updated_memory['long_term_facts'])} 条。"
)
else:
logger.warning("模型返回的长期事实格式不正确或缺失,保留原有长期事实。")
# 2. 合并和处理短期事件 (short_term_events)
model_short_term_events = parsed_response.get("short_term_events", [])
if not isinstance(model_short_term_events, list):
logger.warning("模型返回的短期事件格式不正确,将忽略。")
model_short_term_events = []
existing_events_map = {
event["id"]: event
for event in current_memory_data.get("short_term_events", [])
if "id" in event
}
new_short_term_events_list = []
# 遍历模型返回的短期事件
for event in model_short_term_events:
# 确保事件包含必要字段
if not all(
k in event
for k in [
"topic",
"summary",
"status",
"last_active_turn",
"mentions_count",
]
):
logger.warning(f"模型返回的短期事件缺少必要字段,跳过: {event}")
continue
event_id = event.get("id")
is_new_event = False
final_event_id = None
# 尝试使用模型提供的ID
if event_id and isinstance(event_id, str):
# 验证ID格式英文小写下划线分隔最大长度30字符
if (
len(event_id) <= 30
and all(c.isalnum() or c == "_" for c in event_id)
and event_id.islower()
):
if event_id not in existing_events_map:
# 模型提供了有效且不冲突的ID
final_event_id = event_id
is_new_event = True
logger.debug(f"使用模型建议的有效新ID: {event_id}")
else:
# ID冲突这是现有事件的更新
final_event_id = event_id
logger.debug(
f"更新现有短期事件: {event.get('topic', '未知')} (ID: {event_id})"
)
else:
logger.warning(
f"模型建议的短期事件ID '{event_id}' 格式不正确将尝试生成新ID。"
)
# 如果模型ID无效或冲突尝试基于topic生成
if final_event_id is None and event.get("topic"):
temp_id = "".join(
c if c.isalnum() else "_" for c in event["topic"]
).lower()
temp_id = temp_id.strip("_")
if len(temp_id) > 30:
temp_id = temp_id[:30]
# 确保生成的ID是唯一的
if temp_id and temp_id not in existing_events_map:
final_event_id = temp_id
is_new_event = True
logger.debug(f"基于topic生成描述性新ID: {final_event_id}")
else:
logger.warning(
f"基于topic '{event['topic']}' 生成的ID '{temp_id}' 已存在或无效将生成UUID。"
)
# 如果所有尝试都失败生成UUID
if final_event_id is None:
final_event_id = str(uuid.uuid4())
is_new_event = True
logger.debug(f"无法生成描述性ID生成新UUID: {final_event_id}")
event["id"] = final_event_id
if is_new_event:
# 确保新事件的活跃轮次和提及次数正确初始化
event["last_active_turn"] = current_global_turn_count
event["mentions_count"] = 1
logger.info(f"发现新短期事件: {event['topic']} (ID: {event['id']})")
else:
# 现有事件:更新其活跃轮次和提及次数
event["last_active_turn"] = current_global_turn_count
# 从 existing_events_map 中获取旧的 mentions_count
old_event = existing_events_map.get(event["id"])
if old_event and "mentions_count" in old_event:
event["mentions_count"] = old_event["mentions_count"] + 1
else:
event["mentions_count"] = (
1 # 如果旧事件没有 mentions_count则初始化为1
)
logger.debug(f"更新现有短期事件: {event['topic']} (ID: {event['id']})")
new_short_term_events_list.append(event)
# 从现有事件映射中移除,以便后续处理未被模型提及的旧事件
if final_event_id in existing_events_map:
del existing_events_map[final_event_id]
# 将模型未提及但仍然活跃的旧事件添加回来 (后端辅助的遗忘机制)
for old_event_id, old_event in existing_events_map.items():
if not all(
k in old_event
for k in [
"topic",
"summary",
"status",
"last_active_turn",
"mentions_count",
]
):
logger.warning(f"跳过损坏的旧短期事件: {old_event}")
continue
# 判断是否需要遗忘
if current_global_turn_count - old_event[
"last_active_turn"
] > memory_retention_turns and old_event["status"] in ["已解决", "已结束"]:
logger.info(
f"遗忘旧短期事件 (不活跃且已解决/结束): {old_event['topic']} (ID: {old_event['id']})"
)
continue # 不添加到新列表中,实现遗忘
new_short_term_events_list.append(old_event)
logger.debug(
f"保留旧短期事件 (仍活跃或未解决): {old_event['topic']} (ID: {old_event['id']})"
)
# 强制截断短期事件列表,确保不超过最大数量
if len(new_short_term_events_list) > max_short_term_events:
# 优先保留最新的和提及次数多的
new_short_term_events_list.sort(
key=lambda x: (
x.get("last_active_turn", 0),
x.get("mentions_count", 0),
),
reverse=True,
)
removed_count = len(new_short_term_events_list) - max_short_term_events
logger.warning(
f"短期事件列表超出 {max_short_term_events} 个,移除 {removed_count} 个最不活跃的事件。"
)
new_short_term_events_list = new_short_term_events_list[
:max_short_term_events
]
updated_memory["short_term_events"] = new_short_term_events_list
logger.info(
f"短期事件已处理,共 {len(updated_memory['short_term_events'])} 条。"
)
except json.JSONDecodeError as e:
logger.error(
f"模型返回的记忆更新响应不是有效JSON无法解析: {model_response_text[:200]}... - 错误: {e}"
)
# 返回原始记忆,不进行更新
return current_memory_data
except Exception as e:
logger.error(f"处理记忆更新响应时发生未知错误: {e}", exc_info=True)
# 返回原始记忆,不进行更新
return current_memory_data
return updated_memory