上传文件至 /

This commit is contained in:
2025-06-06 16:42:11 +08:00
parent ffdeafa791
commit 8b3e1835fd
4 changed files with 571 additions and 0 deletions

131
README.md Normal file
View File

@ -0,0 +1,131 @@
# AI 助手项目
这是一个基于 Google Gemini API 构建的 AI 助手项目,旨在提供一个交互式的聊天界面,支持多轮对话、记忆管理和可配置的系统指令。
## 项目结构
```
.
├── app.py # 主应用程序入口处理后端逻辑和API路由
├── config.ini # 配置文件用于存储API密钥、模型设置、文件路径等
├── config.py # 配置加载和管理模块
├── file_manager.py # 文件操作工具用于读写JSON、管理目录等
├── gemini_client.py # Gemini API 客户端处理与Google Gemini API的交互
├── memory_manager.py # 记忆管理模块,处理短期和长期记忆的存储与检索
├── README.md # 项目说明文件
├── requirements.txt # Python依赖列表
├── utils.py # 常用工具函数,如日志配置、时间处理等
├── data/
│ ├── chat_logs/ # 存储聊天记录
│ ├── features/ # 存储角色特性例如default_role/features.json
│ └── memories/ # 存储记忆数据例如default_memory/memory.json
└── static/
├── index.html # 前端主页面
├── css/
│ ├── modern-style.css # 现代风格CSS
│ └── style.css # 主要CSS样式
├── js/
│ ├── api.js # 前端API调用逻辑
│ ├── app.js # 前端主应用逻辑
│ ├── ui_manager.js # 前端UI管理逻辑
│ └── lib/ # 前端第三方库
│ ├── marked.min.js # Markdown解析库
│ └── purify.min.js # HTML净化库
```
## 功能特性
* 多轮对话: 支持与AI进行连续的多轮对话保持上下文。
* 记忆管理:
* 短期记忆: 存储最近的对话轮次,用于维持当前对话的连贯性。
* 长期记忆: 通过摘要和嵌入技术,将关键信息存储为长期记忆,以便在后续对话中检索和利用。
* 可配置性:
* 通过 `config.ini` 文件轻松配置 Gemini API 密钥、默认模型、上下文窗口大小、记忆保留轮次等。
* 支持从环境变量加载 `GEMINI_API_KEY`
* 流式响应: 支持 Gemini API 的流式响应,提供更流畅的用户体验。
* 前端界面: 提供一个简洁的Web界面方便用户与AI助手交互。
* 日志记录: 详细的日志记录,便于调试和问题追踪。
## 安装与运行
### 1. 克隆仓库
```bash
git clone https://github.com/your-username/your-repo-name.git
cd your-repo-name
```
(请将 `your-username/your-repo-name.git` 替换为实际的仓库地址)
### 2. 创建并激活虚拟环境 (推荐)
```bash
python -m venv venv
# Windows
.\venv\Scripts\activate
# macOS/Linux
source venv/bin/activate
```
### 3. 安装依赖
```bash
pip install -r requirements.txt
```
### 4. 配置 API 密钥
在项目根目录下找到 `config.ini` 文件。
`[API]` 部分的 `GEMINI_API_KEY` 替换为你的 Google Gemini API 密钥。
```ini
[API]
GEMINI_API_KEY = YOUR_GEMINI_API_KEY_HERE
```
或者,你可以设置环境变量 `GEMINI_API_KEY`。环境变量的优先级高于 `config.ini` 文件中的配置。
### 5. 运行应用程序
```bash
python app.py
```
应用程序将在本地启动一个Web服务。通常你可以在浏览器中访问 `http://127.0.0.1:5000` 来使用AI助手。
## 配置说明
项目的主要配置通过 `config.ini` 文件管理。以下是各个配置项的详细说明:
### [API] 部分
* `gemini_api_base_url`: Gemini API 的基础 URL。如果使用代理或自定义端点请修改此项。
* `gemini_api_key`: 你的 Google Gemini API 密钥。建议通过环境变量 `GEMINI_API_KEY` 设置,优先级更高。
* `default_gemini_model`: 默认使用的 Gemini 模型,例如 `models/gemini-2.0-flash`
* `memory_update_model`: 用于记忆更新(摘要和嵌入)的 Gemini 模型。
### [Application] 部分
* `context_window_size`: 短期记忆中保留的对话轮次数量,用于维持当前对话的上下文。
* `memory_retention_turns`: 长期记忆更新的频率,每隔多少轮对话进行一次记忆摘要和存储。
* `max_short_term_events`: 短期记忆中最大事件(消息)数量。
* `features_dir`: 存储角色特性配置文件的目录路径。
* `memories_dir`: 存储长期记忆数据文件的目录路径。
* `chat_logs_dir`: 存储聊天记录文件的目录路径。
* `app_log_file_path`: 应用程序日志文件的路径。
### [Session] 部分
* `current_role_id`: 当前会话使用的角色ID。
* `current_memory_id`: 当前会话使用的记忆ID。
* `current_chat_log_id`: 当前会话的聊天记录ID。
## 使用说明
在Web界面中输入你的问题或指令AI助手将根据你的输入和历史对话进行回复。
## 贡献
欢迎贡献如果你有任何改进建议或发现bug请提交Issue或Pull Request。
## 许可证
本项目采用 MIT 许可证。详见 `LICENSE` 文件 (如果存在)。

370
memory_manager.py Normal file
View File

@ -0,0 +1,370 @@
# 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

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask
requests

68
utils.py Normal file
View File

@ -0,0 +1,68 @@
# utils.py
import logging
import uuid
import os
import datetime
def setup_logging(log_file_path):
"""
配置应用程序的日志系统。
日志将输出到控制台和指定的日志文件。
:param log_file_path: 日志文件的完整路径。
"""
# 确保日志目录存在
log_dir = os.path.dirname(log_file_path)
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 创建一个根日志器
logger = logging.getLogger()
logger.setLevel(logging.INFO) # 设置最低日志级别为INFO
# 移除已有的处理器,防止重复日志
if logger.handlers:
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# 创建一个文件处理器,用于将日志写入文件
file_handler = logging.FileHandler(log_file_path, encoding="utf-8")
file_handler.setLevel(logging.INFO) # 文件处理器级别为INFO
# 创建一个控制台处理器,用于将日志输出到控制台
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # 控制台处理器级别为INFO
# 定义日志格式
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 将处理器添加到日志器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logging.info(f"日志系统已配置,日志将写入 {log_file_path}")
def generate_uuid():
"""
生成一个基于时间戳和随机数的唯一UUID字符串。
:return: 32位小写UUID字符串。
"""
# UUID4是随机生成适合作为唯一ID
return str(uuid.uuid4())
def get_current_timestamp():
"""
获取当前时间的标准格式字符串。
:return: 例如 "2023-10-27 10:00:00"
"""
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 可以定义一个通用的错误处理装饰器或其他实用函数