Merge branch 'dev' of https://github.com/MaiM-with-u/MaiBot into refactor

This commit is contained in:
tcmofashi
2025-04-09 17:01:28 +08:00
80 changed files with 5897 additions and 2522 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@ mongodb/
NapCat.Framework.Windows.Once/
log/
logs/
run_ad.bat
MaiBot-Napcat-Adapter-main
MaiBot-Napcat-Adapter
/test
/src/test
nonebot-maibot-adapter/

20
CLAUDE.md Normal file
View File

@@ -0,0 +1,20 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
- **Run Bot**: `python bot.py`
- **Lint**: `ruff check --fix .` or `ruff format .`
- **Run Tests**: `python -m unittest discover -v`
- **Run Single Test**: `python -m unittest src/plugins/message/test.py`
## Code Style
- **Formatting**: Line length 120 chars, use double quotes for strings
- **Imports**: Group standard library, external packages, then internal imports
- **Naming**: snake_case for functions/variables, PascalCase for classes
- **Error Handling**: Use try/except blocks with specific exceptions
- **Types**: Use type hints where possible
- **Docstrings**: Document classes and complex functions
- **Linting**: Follow ruff rules (E, F, B) with ignores E711, E501
When making changes, run `ruff check --fix .` to ensure code follows style guidelines. The codebase uses Ruff for linting and formatting.

160
README.md
View File

@@ -1,24 +1,66 @@
# 麦麦MaiCore-MaiMBot (编辑中)
<br />
<div align="center">
![Python Version](https://img.shields.io/badge/Python-3.9+-blue)
![License](https://img.shields.io/github/license/SengokuCola/MaiMBot?label=协议)
![Status](https://img.shields.io/badge/状态-开发中-yellow)
![Contributors](https://img.shields.io/github/contributors/MaiM-with-u/MaiBot.svg?style=flat&label=贡献者)
![forks](https://img.shields.io/github/forks/MaiM-with-u/MaiBot.svg?style=flat&label=分支数)
![stars](https://img.shields.io/github/stars/MaiM-with-u/MaiBot?style=flat&label=星标数)
![issues](https://img.shields.io/github/issues/MaiM-with-u/MaiBot)
</div>
<p align="center">
<a href="https://github.com/MaiM-with-u/MaiBot/">
<img src="depends-data/maimai.png" alt="Logo" width="200">
</a>
<br />
<a href="https://space.bilibili.com/1344099355">
画师略nd
</a>
<h3 align="center">MaiBot(麦麦)</h3>
<p align="center">
一款专注于<strong> 群组聊天 </strong>的赛博网友
<br />
<a href="https://docs.mai-mai.org"><strong>探索本项目的文档 »</strong></a>
<br />
<br />
<!-- <a href="https://github.com/shaojintian/Best_README_template">查看Demo</a>
· -->
<a href="https://github.com/MaiM-with-u/MaiBot/issues">报告Bug</a>
·
<a href="https://github.com/MaiM-with-u/MaiBot/issues">提出新特性</a>
</p>
</p>
## 新版0.6.0部署前先阅读https://docs.mai-mai.org/manual/usage/mmc_q_a
<div align="center">
![Python Version](https://img.shields.io/badge/Python-3.9+-blue)
![License](https://img.shields.io/github/license/SengokuCola/MaiMBot)
![Status](https://img.shields.io/badge/状态-开发中-yellow)
</div>
## 📝 项目简介
**🍔MaiCore是一个基于大语言模型的可交互智能体**
- LLM 提供对话能力
- 动态Prompt构建器
- 实时思维系统
- MongoDB 提供数据持久化支持
- 可扩展,可支持多种平台和多种功能
- 💭 **智能对话系统**基于LLM的自然语言交互
- 🤔 **实时思维系统**:模拟人类思考过程
- 💝 **情感表达系统**:丰富的表情包和情绪表达
- 🧠 **持久记忆系统**基于MongoDB的长期记忆存储
- 🔄 **动态人格系统**:自适应的性格特征
<div align="center">
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
<img src="depends-data/video.png" width="200" alt="麦麦演示视频">
<br>
👆 点击观看麦麦演示视频 👆
</a>
</div>
### 📢 版本信息
**最新版本: v0.6.0** ([查看更新日志](changelogs/changelog.md))
> [!WARNING]
@@ -28,19 +70,12 @@
> 次版本MaiBot将基于MaiCore运行不再依赖于nonebot相关组件运行。
> MaiBot将通过nonebot的插件与nonebot建立联系然后nonebot与QQ建立联系实现MaiBot与QQ的交互
**分支介绍:**
- main 稳定版本
- dev 开发版(不知道什么意思就别下)
- classical 0.6.0前的版本
**分支说明:**
- `main`: 稳定发布版本
- `dev`: 开发测试版本(不知道什么意思就别下)
- `classical`: 0.6.0前的版本
<div align="center">
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
<img src="docs/pic/video.png" width="300" alt="麦麦演示视频">
<br>
👆 点击观看麦麦演示视频 👆
</a>
</div>
> [!WARNING]
> - 项目处于活跃开发阶段,代码可能随时更改
@@ -49,6 +84,12 @@
> - 由于持续迭代可能存在一些已知或未知的bug
> - 由于开发中可能消耗较多token
### ⚠️ 重要提示
- 升级到v0.6.0版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a)
- 本版本基于MaiCore重构通过nonebot插件与QQ平台交互
- 项目处于活跃开发阶段功能和API可能随时调整
### 💬交流群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码
- [五群](https://qm.qq.com/q/JxvHZnxyec) 1022489779
- [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 【已满】
@@ -72,55 +113,35 @@
## 🎯 功能介绍
### 💬 聊天功能
- 提供思维流(心流)聊天和推理聊天两种对话逻辑
- 支持关键词检索主动发言对消息的话题topic进行识别如果检测到麦麦存储过的话题就会主动进行发言
- 支持bot名字呼唤发言检测到"麦麦"会主动发言,可配置
- 支持多模型,多厂商自定义配置
- 动态的prompt构建器更拟人
- 支持图片,转发消息,回复消息的识别
- 支持私聊功能可使用PFC模式的有目的多轮对话实验性
| 模块 | 主要功能 | 特点 |
|------|---------|------|
| 💬 聊天系统 | • 思维流/推理聊天<br>关键词主动发言<br>• 多模型支持<br>• 动态prompt构建<br>• 私聊功能(PFC) | 拟人化交互 |
| 🧠 思维流系统 | • 实时思考生成<br>• 自动启停机制<br>• 日程系统联动 | 智能化决策 |
| 🧠 记忆系统 2.0 | • 优化记忆抽取<br>• 海马体记忆机制<br>• 聊天记录概括 | 持久化记忆 |
| 😊 表情包系统 | • 情绪匹配发送<br>• GIF支持<br>• 自动收集与审查 | 丰富表达 |
| 📅 日程系统 | • 动态日程生成<br>• 自定义想象力<br>• 思维流联动 | 智能规划 |
| 👥 关系系统 2.0 | • 关系管理优化<br>• 丰富接口支持<br>• 个性化交互 | 深度社交 |
| 📊 统计系统 | • 使用数据统计<br>• LLM调用记录<br>• 实时控制台显示 | 数据可视 |
| 🔧 系统功能 | • 优雅关闭机制<br>• 自动数据保存<br>• 异常处理完善 | 稳定可靠 |
### 🧠 思维流系统
- 思维流能够在回复前后进行思考,生成实时想法
- 思维流自动启停机制,提升资源利用效率
- 思维流与日程系统联动,实现动态日程生成
## 📐 项目架构
### 🧠 记忆系统 2.0
- 优化记忆抽取策略和prompt结构
- 改进海马体记忆提取机制,提升自然度
- 对聊天记录进行概括存储,在需要时调用
```mermaid
graph TD
A[MaiCore] --> B[对话系统]
A --> C[思维流系统]
A --> D[记忆系统]
A --> E[情感系统]
B --> F[多模型支持]
B --> G[动态Prompt]
C --> H[实时思考]
C --> I[日程联动]
D --> J[记忆存储]
D --> K[记忆检索]
E --> L[表情管理]
E --> M[情绪识别]
```
### 😊 表情包系统
- 支持根据发言内容发送对应情绪的表情包
- 支持识别和处理gif表情包
- 会自动偷群友的表情包
- 表情包审查功能
- 表情包文件完整性自动检查
- 自动清理缓存图片
### 📅 日程系统
- 动态更新的日程生成
- 可自定义想象力程度
- 与聊天情况交互(思维流模式下)
### 👥 关系系统 2.0
- 优化关系管理系统,适用于新版本
- 提供更丰富的关系接口
- 针对每个用户创建"关系",实现个性化回复
### 📊 统计系统
- 详细的使用数据统计
- LLM调用统计
- 在控制台显示统计信息
### 🔧 系统功能
- 支持优雅的shutdown机制
- 自动保存功能,定期保存聊天记录和关系数据
- 完善的异常处理机制
- 可自定义时区设置
- 优化的日志输出格式
- 配置自动更新功能
## 开发计划TODOLIST
@@ -157,7 +178,6 @@ MaiCore是一个开源项目我们非常欢迎你的参与。你的贡献
## 致谢
- [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架
- [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现
### 贡献者

4
bot.py
View File

@@ -8,6 +8,7 @@ import time
import platform
from dotenv import load_dotenv
from src.common.logger import get_module_logger
from src.common.crash_logger import install_crash_handler
from src.main import MainSystem
logger = get_module_logger("main_bot")
@@ -193,6 +194,9 @@ def raw_main():
if platform.system().lower() != "windows":
time.tzset()
# 安装崩溃日志处理器
install_crash_handler()
check_eula()
print("检查EULA和隐私条款完成")
easter_egg()

BIN
depends-data/maimai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

BIN
depends-data/video.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -0,0 +1,72 @@
import sys
import traceback
import logging
from pathlib import Path
from logging.handlers import RotatingFileHandler
def setup_crash_logger():
"""设置崩溃日志记录器"""
# 创建logs/crash目录如果不存在
crash_log_dir = Path("logs/crash")
crash_log_dir.mkdir(parents=True, exist_ok=True)
# 创建日志记录器
crash_logger = logging.getLogger('crash_logger')
crash_logger.setLevel(logging.ERROR)
# 设置日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s\n'
'异常类型: %(exc_info)s\n'
'详细信息:\n%(message)s\n'
'-------------------\n'
)
# 创建按大小轮转的文件处理器最大10MB保留5个备份
log_file = crash_log_dir / "crash.log"
file_handler = RotatingFileHandler(
log_file,
maxBytes=10*1024*1024, # 10MB
backupCount=5,
encoding='utf-8'
)
file_handler.setFormatter(formatter)
crash_logger.addHandler(file_handler)
return crash_logger
def log_crash(exc_type, exc_value, exc_traceback):
"""记录崩溃信息到日志文件"""
if exc_type is None:
return
# 获取崩溃日志记录器
crash_logger = logging.getLogger('crash_logger')
# 获取完整的异常堆栈信息
stack_trace = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
# 记录崩溃信息
crash_logger.error(
stack_trace,
exc_info=(exc_type, exc_value, exc_traceback)
)
def install_crash_handler():
"""安装全局异常处理器"""
# 设置崩溃日志记录器
setup_crash_logger()
# 保存原始的异常处理器
original_hook = sys.excepthook
def exception_handler(exc_type, exc_value, exc_traceback):
"""全局异常处理器"""
# 记录崩溃信息
log_crash(exc_type, exc_value, exc_traceback)
# 调用原始的异常处理器
original_hook(exc_type, exc_value, exc_traceback)
# 设置全局异常处理器
sys.excepthook = exception_handler

View File

@@ -1,347 +1,378 @@
import customtkinter as ctk
import subprocess
import threading
import queue
import re
import os
import signal
from collections import deque
# import customtkinter as ctk
# import subprocess
# import threading
# import queue
# import re
# import os
# import signal
# from collections import deque
# import sys
# 设置应用的外观模式和默认颜色主题
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# # 设置应用的外观模式和默认颜色主题
# ctk.set_appearance_mode("dark")
# ctk.set_default_color_theme("blue")
class LogViewerApp(ctk.CTk):
"""日志查看器应用的主类继承自customtkinter的CTk类"""
# class LogViewerApp(ctk.CTk):
# """日志查看器应用的主类继承自customtkinter的CTk类"""
def __init__(self):
"""初始化日志查看器应用的界面和状态"""
super().__init__()
self.title("日志查看器")
self.geometry("1200x800")
# def __init__(self):
# """初始化日志查看器应用的界面和状态"""
# super().__init__()
# self.title("日志查看器")
# self.geometry("1200x800")
# 初始化进程、日志队列、日志数据等变量
self.process = None
self.log_queue = queue.Queue()
self.log_data = deque(maxlen=10000) # 使用固定长度队列
self.available_levels = set()
self.available_modules = set()
self.sorted_modules = []
self.module_checkboxes = {} # 存储模块复选框的字典
# # 标记GUI是否运行中
# self.is_running = True
# 日志颜色配置
self.color_config = {
"time": "#888888",
"DEBUG": "#2196F3",
"INFO": "#4CAF50",
"WARNING": "#FF9800",
"ERROR": "#F44336",
"module": "#D4D0AB",
"default": "#FFFFFF",
}
# # 程序关闭时的清理操作
# self.protocol("WM_DELETE_WINDOW", self._on_closing)
# 列可见性配置
self.column_visibility = {"show_time": True, "show_level": True, "show_module": True}
# # 初始化进程、日志队列、日志数据等变量
# self.process = None
# self.log_queue = queue.Queue()
# self.log_data = deque(maxlen=10000) # 使用固定长度队列
# self.available_levels = set()
# self.available_modules = set()
# self.sorted_modules = []
# self.module_checkboxes = {} # 存储模块复选框的字典
# 选中的日志等级和模块
self.selected_levels = set()
self.selected_modules = set()
# # 日志颜色配置
# self.color_config = {
# "time": "#888888",
# "DEBUG": "#2196F3",
# "INFO": "#4CAF50",
# "WARNING": "#FF9800",
# "ERROR": "#F44336",
# "module": "#D4D0AB",
# "default": "#FFFFFF",
# }
# 创建界面组件并启动日志队列处理
self.create_widgets()
self.after(100, self.process_log_queue)
# # 列可见性配置
# self.column_visibility = {"show_time": True, "show_level": True, "show_module": True}
def create_widgets(self):
"""创建应用界面的各个组件"""
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
# # 选中的日志等级和模块
# self.selected_levels = set()
# self.selected_modules = set()
# 控制面板
control_frame = ctk.CTkFrame(self)
control_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
# # 创建界面组件并启动日志队列处理
# self.create_widgets()
# self.after(100, self.process_log_queue)
self.start_btn = ctk.CTkButton(control_frame, text="启动", command=self.start_process)
self.start_btn.pack(side="left", padx=5)
# def create_widgets(self):
# """创建应用界面的各个组件"""
# self.grid_columnconfigure(0, weight=1)
# self.grid_rowconfigure(1, weight=1)
self.stop_btn = ctk.CTkButton(control_frame, text="停止", command=self.stop_process, state="disabled")
self.stop_btn.pack(side="left", padx=5)
# # 控制面板
# control_frame = ctk.CTkFrame(self)
# control_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
self.clear_btn = ctk.CTkButton(control_frame, text="清屏", command=self.clear_logs)
self.clear_btn.pack(side="left", padx=5)
# self.start_btn = ctk.CTkButton(control_frame, text="启动", command=self.start_process)
# self.start_btn.pack(side="left", padx=5)
column_filter_frame = ctk.CTkFrame(control_frame)
column_filter_frame.pack(side="left", padx=20)
# self.stop_btn = ctk.CTkButton(control_frame, text="停止", command=self.stop_process, state="disabled")
# self.stop_btn.pack(side="left", padx=5)
self.time_check = ctk.CTkCheckBox(column_filter_frame, text="显示时间", command=self.refresh_logs)
self.time_check.pack(side="left", padx=5)
self.time_check.select()
# self.clear_btn = ctk.CTkButton(control_frame, text="清屏", command=self.clear_logs)
# self.clear_btn.pack(side="left", padx=5)
self.level_check = ctk.CTkCheckBox(column_filter_frame, text="显示等级", command=self.refresh_logs)
self.level_check.pack(side="left", padx=5)
self.level_check.select()
# column_filter_frame = ctk.CTkFrame(control_frame)
# column_filter_frame.pack(side="left", padx=20)
self.module_check = ctk.CTkCheckBox(column_filter_frame, text="显示模块", command=self.refresh_logs)
self.module_check.pack(side="left", padx=5)
self.module_check.select()
# self.time_check = ctk.CTkCheckBox(column_filter_frame, text="显示时间", command=self.refresh_logs)
# self.time_check.pack(side="left", padx=5)
# self.time_check.select()
# 筛选面板
filter_frame = ctk.CTkFrame(self)
filter_frame.grid(row=0, column=1, rowspan=2, sticky="ns", padx=5)
# self.level_check = ctk.CTkCheckBox(column_filter_frame, text="显示等级", command=self.refresh_logs)
# self.level_check.pack(side="left", padx=5)
# self.level_check.select()
ctk.CTkLabel(filter_frame, text="日志等级筛选").pack(pady=5)
self.level_scroll = ctk.CTkScrollableFrame(filter_frame, width=150, height=200)
self.level_scroll.pack(fill="both", expand=True, padx=5)
# self.module_check = ctk.CTkCheckBox(column_filter_frame, text="显示模块", command=self.refresh_logs)
# self.module_check.pack(side="left", padx=5)
# self.module_check.select()
ctk.CTkLabel(filter_frame, text="模块筛选").pack(pady=5)
self.module_filter_entry = ctk.CTkEntry(filter_frame, placeholder_text="输入模块过滤词")
self.module_filter_entry.pack(pady=5)
self.module_filter_entry.bind("<KeyRelease>", self.update_module_filter)
# # 筛选面板
# filter_frame = ctk.CTkFrame(self)
# filter_frame.grid(row=0, column=1, rowspan=2, sticky="ns", padx=5)
self.module_scroll = ctk.CTkScrollableFrame(filter_frame, width=300, height=200)
self.module_scroll.pack(fill="both", expand=True, padx=5)
# ctk.CTkLabel(filter_frame, text="日志等级筛选").pack(pady=5)
# self.level_scroll = ctk.CTkScrollableFrame(filter_frame, width=150, height=200)
# self.level_scroll.pack(fill="both", expand=True, padx=5)
self.log_text = ctk.CTkTextbox(self, wrap="word")
self.log_text.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
# ctk.CTkLabel(filter_frame, text="模块筛选").pack(pady=5)
# self.module_filter_entry = ctk.CTkEntry(filter_frame, placeholder_text="输入模块过滤词")
# self.module_filter_entry.pack(pady=5)
# self.module_filter_entry.bind("<KeyRelease>", self.update_module_filter)
self.init_text_tags()
# self.module_scroll = ctk.CTkScrollableFrame(filter_frame, width=300, height=200)
# self.module_scroll.pack(fill="both", expand=True, padx=5)
def update_module_filter(self, event):
"""根据模块过滤词更新模块复选框的显示"""
filter_text = self.module_filter_entry.get().strip().lower()
for module, checkbox in self.module_checkboxes.items():
if filter_text in module.lower():
checkbox.pack(anchor="w", padx=5, pady=2)
else:
checkbox.pack_forget()
# self.log_text = ctk.CTkTextbox(self, wrap="word")
# self.log_text.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
def update_filters(self, level, module):
"""更新日志等级和模块的筛选器"""
if level not in self.available_levels:
self.available_levels.add(level)
self.add_checkbox(self.level_scroll, level, "level")
# self.init_text_tags()
module_key = self.get_module_key(module)
if module_key not in self.available_modules:
self.available_modules.add(module_key)
self.sorted_modules = sorted(self.available_modules, key=lambda x: x.lower())
self.rebuild_module_checkboxes()
# def update_module_filter(self, event):
# """根据模块过滤词更新模块复选框的显示"""
# filter_text = self.module_filter_entry.get().strip().lower()
# for module, checkbox in self.module_checkboxes.items():
# if filter_text in module.lower():
# checkbox.pack(anchor="w", padx=5, pady=2)
# else:
# checkbox.pack_forget()
def rebuild_module_checkboxes(self):
"""重新构建模块复选框"""
# 清空现有复选框
for widget in self.module_scroll.winfo_children():
widget.destroy()
self.module_checkboxes.clear()
# def update_filters(self, level, module):
# """更新日志等级和模块的筛选器"""
# if level not in self.available_levels:
# self.available_levels.add(level)
# self.add_checkbox(self.level_scroll, level, "level")
# 重建排序后的复选框
for module in self.sorted_modules:
self.add_checkbox(self.module_scroll, module, "module")
# module_key = self.get_module_key(module)
# if module_key not in self.available_modules:
# self.available_modules.add(module_key)
# self.sorted_modules = sorted(self.available_modules, key=lambda x: x.lower())
# self.rebuild_module_checkboxes()
def add_checkbox(self, parent, text, type_):
"""在指定父组件中添加复选框"""
# def rebuild_module_checkboxes(self):
# """重新构建模块复选框"""
# # 清空现有复选框
# for widget in self.module_scroll.winfo_children():
# widget.destroy()
# self.module_checkboxes.clear()
def update_filter():
current = cb.get()
if type_ == "level":
(self.selected_levels.add if current else self.selected_levels.discard)(text)
else:
(self.selected_modules.add if current else self.selected_modules.discard)(text)
self.refresh_logs()
# # 重建排序后的复选框
# for module in self.sorted_modules:
# self.add_checkbox(self.module_scroll, module, "module")
cb = ctk.CTkCheckBox(parent, text=text, command=update_filter)
cb.select() # 初始选中
# def add_checkbox(self, parent, text, type_):
# """在指定父组件中添加复选框"""
# 手动同步初始状态到集合(关键修复)
if type_ == "level":
self.selected_levels.add(text)
else:
self.selected_modules.add(text)
# def update_filter():
# current = cb.get()
# if type_ == "level":
# (self.selected_levels.add if current else self.selected_levels.discard)(text)
# else:
# (self.selected_modules.add if current else self.selected_modules.discard)(text)
# self.refresh_logs()
if type_ == "module":
self.module_checkboxes[text] = cb
cb.pack(anchor="w", padx=5, pady=2)
return cb
# cb = ctk.CTkCheckBox(parent, text=text, command=update_filter)
# cb.select() # 初始选中
def check_filter(self, entry):
"""检查日志条目是否符合当前筛选条件"""
level_ok = not self.selected_levels or entry["level"] in self.selected_levels
module_key = self.get_module_key(entry["module"])
module_ok = not self.selected_modules or module_key in self.selected_modules
return level_ok and module_ok
# # 手动同步初始状态到集合(关键修复)
# if type_ == "level":
# self.selected_levels.add(text)
# else:
# self.selected_modules.add(text)
def init_text_tags(self):
"""初始化日志文本的颜色标签"""
for tag, color in self.color_config.items():
self.log_text.tag_config(tag, foreground=color)
self.log_text.tag_config("default", foreground=self.color_config["default"])
# if type_ == "module":
# self.module_checkboxes[text] = cb
# cb.pack(anchor="w", padx=5, pady=2)
# return cb
def start_process(self):
"""启动日志进程并开始读取输出"""
self.process = subprocess.Popen(
["nb", "run"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
encoding="utf-8",
errors="ignore",
)
self.start_btn.configure(state="disabled")
self.stop_btn.configure(state="normal")
threading.Thread(target=self.read_output, daemon=True).start()
# def check_filter(self, entry):
# """检查日志条目是否符合当前筛选条件"""
# level_ok = not self.selected_levels or entry["level"] in self.selected_levels
# module_key = self.get_module_key(entry["module"])
# module_ok = not self.selected_modules or module_key in self.selected_modules
# return level_ok and module_ok
def stop_process(self):
"""停止日志进程并清理相关资源"""
if self.process:
try:
if hasattr(self.process, "pid"):
if os.name == "nt":
subprocess.run(
["taskkill", "/F", "/T", "/PID", str(self.process.pid)], check=True, capture_output=True
)
else:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except (subprocess.CalledProcessError, ProcessLookupError, OSError) as e:
print(f"终止进程失败: {e}")
finally:
self.process = None
self.log_queue.queue.clear()
self.start_btn.configure(state="normal")
self.stop_btn.configure(state="disabled")
self.refresh_logs()
# def init_text_tags(self):
# """初始化日志文本的颜色标签"""
# for tag, color in self.color_config.items():
# self.log_text.tag_config(tag, foreground=color)
# self.log_text.tag_config("default", foreground=self.color_config["default"])
def read_output(self):
"""读取日志进程的输出并放入队列"""
try:
while self.process and self.process.poll() is None:
line = self.process.stdout.readline()
if line:
self.log_queue.put(line)
else:
break # 避免空循环
self.process.stdout.close() # 确保关闭文件描述符
except ValueError: # 处理可能的I/O操作异常
pass
# def start_process(self):
# """启动日志进程并开始读取输出"""
# self.process = subprocess.Popen(
# ["nb", "run"],
# stdout=subprocess.PIPE,
# stderr=subprocess.STDOUT,
# text=True,
# bufsize=1,
# encoding="utf-8",
# errors="ignore",
# )
# self.start_btn.configure(state="disabled")
# self.stop_btn.configure(state="normal")
# threading.Thread(target=self.read_output, daemon=True).start()
def process_log_queue(self):
"""处理日志队列中的日志条目"""
while not self.log_queue.empty():
line = self.log_queue.get()
self.process_log_line(line)
self.after(100, self.process_log_queue)
# def stop_process(self):
# """停止日志进程并清理相关资源"""
# if self.process:
# try:
# if hasattr(self.process, "pid"):
# if os.name == "nt":
# subprocess.run(
# ["taskkill", "/F", "/T", "/PID", str(self.process.pid)], check=True, capture_output=True
# )
# else:
# os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
# except (subprocess.CalledProcessError, ProcessLookupError, OSError) as e:
# print(f"终止进程失败: {e}")
# finally:
# self.process = None
# self.log_queue.queue.clear()
# self.start_btn.configure(state="normal")
# self.stop_btn.configure(state="disabled")
# self.refresh_logs()
def process_log_line(self, line):
"""解析单行日志并更新日志数据和筛选器"""
match = re.match(
r"""^
(?:(?P<time>\d{2}:\d{2}(?::\d{2})?)\s*\|\s*)?
(?P<level>\w+)\s*\|\s*
(?P<module>.*?)
\s*[-|]\s*
(?P<message>.*)
$""",
line.strip(),
re.VERBOSE,
)
# def read_output(self):
# """读取日志进程的输出并放入队列"""
# try:
# while self.process and self.process.poll() is None and self.is_running:
# line = self.process.stdout.readline()
# if line:
# self.log_queue.put(line)
# else:
# break # 避免空循环
# self.process.stdout.close() # 确保关闭文件描述符
# except ValueError: # 处理可能的I/O操作异常
# pass
if match:
groups = match.groupdict()
time = groups.get("time", "")
level = groups.get("level", "OTHER")
module = groups.get("module", "UNKNOWN").strip()
message = groups.get("message", "").strip()
raw_line = line
else:
time, level, module, message = "", "OTHER", "UNKNOWN", line
raw_line = line
# def process_log_queue(self):
# """处理日志队列中的日志条目"""
# while not self.log_queue.empty():
# line = self.log_queue.get()
# self.process_log_line(line)
self.update_filters(level, module)
log_entry = {"raw": raw_line, "time": time, "level": level, "module": module, "message": message}
self.log_data.append(log_entry)
# # 仅在GUI仍在运行时继续处理队列
# if self.is_running:
# self.after(100, self.process_log_queue)
if self.check_filter(log_entry):
self.display_log(log_entry)
# def process_log_line(self, line):
# """解析单行日志并更新日志数据和筛选器"""
# match = re.match(
# r"""^
# (?:(?P<time>\d{2}:\d{2}(?::\d{2})?)\s*\|\s*)?
# (?P<level>\w+)\s*\|\s*
# (?P<module>.*?)
# \s*[-|]\s*
# (?P<message>.*)
# $""",
# line.strip(),
# re.VERBOSE,
# )
def get_module_key(self, module_name):
"""获取模块名称的标准化键"""
cleaned = module_name.strip()
return re.sub(r":\d+$", "", cleaned)
# if match:
# groups = match.groupdict()
# time = groups.get("time", "")
# level = groups.get("level", "OTHER")
# module = groups.get("module", "UNKNOWN").strip()
# message = groups.get("message", "").strip()
# raw_line = line
# else:
# time, level, module, message = "", "OTHER", "UNKNOWN", line
# raw_line = line
def display_log(self, entry):
"""在日志文本框中显示日志条目"""
parts = []
tags = []
# self.update_filters(level, module)
# log_entry = {"raw": raw_line, "time": time, "level": level, "module": module, "message": message}
# self.log_data.append(log_entry)
if self.column_visibility["show_time"] and entry["time"]:
parts.append(f"{entry['time']} ")
tags.append("time")
# if self.check_filter(log_entry):
# self.display_log(log_entry)
if self.column_visibility["show_level"]:
level_tag = entry["level"] if entry["level"] in self.color_config else "default"
parts.append(f"{entry['level']:<8} ")
tags.append(level_tag)
# def get_module_key(self, module_name):
# """获取模块名称的标准化键"""
# cleaned = module_name.strip()
# return re.sub(r":\d+$", "", cleaned)
if self.column_visibility["show_module"]:
parts.append(f"{entry['module']} ")
tags.append("module")
# def display_log(self, entry):
# """在日志文本框中显示日志条目"""
# parts = []
# tags = []
parts.append(f"- {entry['message']}\n")
tags.append("default")
# if self.column_visibility["show_time"] and entry["time"]:
# parts.append(f"{entry['time']} ")
# tags.append("time")
self.log_text.configure(state="normal")
for part, tag in zip(parts, tags):
self.log_text.insert("end", part, tag)
self.log_text.see("end")
self.log_text.configure(state="disabled")
# if self.column_visibility["show_level"]:
# level_tag = entry["level"] if entry["level"] in self.color_config else "default"
# parts.append(f"{entry['level']:<8} ")
# tags.append(level_tag)
def refresh_logs(self):
"""刷新日志显示,根据筛选条件重新显示日志"""
self.column_visibility = {
"show_time": self.time_check.get(),
"show_level": self.level_check.get(),
"show_module": self.module_check.get(),
}
# if self.column_visibility["show_module"]:
# parts.append(f"{entry['module']} ")
# tags.append("module")
self.log_text.configure(state="normal")
self.log_text.delete("1.0", "end")
# parts.append(f"- {entry['message']}\n")
# tags.append("default")
filtered_logs = [entry for entry in self.log_data if self.check_filter(entry)]
# self.log_text.configure(state="normal")
# for part, tag in zip(parts, tags):
# self.log_text.insert("end", part, tag)
# self.log_text.see("end")
# self.log_text.configure(state="disabled")
for entry in filtered_logs:
parts = []
tags = []
# def refresh_logs(self):
# """刷新日志显示,根据筛选条件重新显示日志"""
# self.column_visibility = {
# "show_time": self.time_check.get(),
# "show_level": self.level_check.get(),
# "show_module": self.module_check.get(),
# }
if self.column_visibility["show_time"] and entry["time"]:
parts.append(f"{entry['time']} ")
tags.append("time")
# self.log_text.configure(state="normal")
# self.log_text.delete("1.0", "end")
if self.column_visibility["show_level"]:
level_tag = entry["level"] if entry["level"] in self.color_config else "default"
parts.append(f"{entry['level']:<8} ")
tags.append(level_tag)
# filtered_logs = [entry for entry in self.log_data if self.check_filter(entry)]
if self.column_visibility["show_module"]:
parts.append(f"{entry['module']} ")
tags.append("module")
# for entry in filtered_logs:
# parts = []
# tags = []
parts.append(f"- {entry['message']}\n")
tags.append("default")
# if self.column_visibility["show_time"] and entry["time"]:
# parts.append(f"{entry['time']} ")
# tags.append("time")
for part, tag in zip(parts, tags):
self.log_text.insert("end", part, tag)
# if self.column_visibility["show_level"]:
# level_tag = entry["level"] if entry["level"] in self.color_config else "default"
# parts.append(f"{entry['level']:<8} ")
# tags.append(level_tag)
self.log_text.see("end")
self.log_text.configure(state="disabled")
# if self.column_visibility["show_module"]:
# parts.append(f"{entry['module']} ")
# tags.append("module")
def clear_logs(self):
"""清空日志文本框中的内容"""
self.log_text.configure(state="normal")
self.log_text.delete("1.0", "end")
self.log_text.configure(state="disabled")
# parts.append(f"- {entry['message']}\n")
# tags.append("default")
# for part, tag in zip(parts, tags):
# self.log_text.insert("end", part, tag)
# self.log_text.see("end")
# self.log_text.configure(state="disabled")
# def clear_logs(self):
# """清空日志文本框中的内容"""
# self.log_text.configure(state="normal")
# self.log_text.delete("1.0", "end")
# self.log_text.configure(state="disabled")
# def _on_closing(self):
# """处理窗口关闭事件,安全清理资源"""
# # 标记GUI已关闭
# self.is_running = False
# # 停止日志进程
# self.stop_process()
# # 安全清理tkinter变量
# for attr_name in list(self.__dict__.keys()):
# if isinstance(getattr(self, attr_name), (ctk.Variable, ctk.StringVar, ctk.IntVar, ctk.DoubleVar, ctk.BooleanVar)):
# try:
# var = getattr(self, attr_name)
# var.set(None)
# except Exception:
# pass
# setattr(self, attr_name, None)
# self.quit()
# sys.exit(0)
if __name__ == "__main__":
# 启动日志查看器应用
app = LogViewerApp()
app.mainloop()
# if __name__ == "__main__":
# # 启动日志查看器应用
# app = LogViewerApp()
# app.mainloop()

View File

@@ -1,320 +1,342 @@
import os
import queue
import sys
import threading
import time
from datetime import datetime
from typing import Dict, List
from typing import Optional
# import os
# import queue
# import sys
# import threading
# import time
# from datetime import datetime
# from typing import Dict, List
# from typing import Optional
sys.path.insert(0, sys.path[0] + "/../")
sys.path.insert(0, sys.path[0] + "/../")
from src.common.logger import get_module_logger
# sys.path.insert(0, sys.path[0] + "/../")
# sys.path.insert(0, sys.path[0] + "/../")
# from src.common.logger import get_module_logger
import customtkinter as ctk
from dotenv import load_dotenv
# import customtkinter as ctk
# from dotenv import load_dotenv
logger = get_module_logger("gui")
# logger = get_module_logger("gui")
# 获取当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取项目根目录
root_dir = os.path.abspath(os.path.join(current_dir, "..", ".."))
sys.path.insert(0, root_dir)
from src.common.database import db # noqa: E402
# # 获取当前文件的目录
# current_dir = os.path.dirname(os.path.abspath(__file__))
# # 获取项目根目录
# root_dir = os.path.abspath(os.path.join(current_dir, "..", ".."))
# sys.path.insert(0, root_dir)
# from src.common.database import db # noqa: E402
# 加载环境变量
if os.path.exists(os.path.join(root_dir, ".env.dev")):
load_dotenv(os.path.join(root_dir, ".env.dev"))
logger.info("成功加载开发环境配置")
elif os.path.exists(os.path.join(root_dir, ".env")):
load_dotenv(os.path.join(root_dir, ".env"))
logger.info("成功加载生产环境配置")
else:
logger.error("未找到环境配置文件")
sys.exit(1)
# # 加载环境变量
# if os.path.exists(os.path.join(root_dir, ".env.dev")):
# load_dotenv(os.path.join(root_dir, ".env.dev"))
# logger.info("成功加载开发环境配置")
# elif os.path.exists(os.path.join(root_dir, ".env")):
# load_dotenv(os.path.join(root_dir, ".env"))
# logger.info("成功加载生产环境配置")
# else:
# logger.error("未找到环境配置文件")
# sys.exit(1)
class ReasoningGUI:
def __init__(self):
# 记录启动时间戳转换为Unix时间戳
self.start_timestamp = datetime.now().timestamp()
logger.info(f"程序启动时间戳: {self.start_timestamp}")
# class ReasoningGUI:
# def __init__(self):
# # 记录启动时间戳转换为Unix时间戳
# self.start_timestamp = datetime.now().timestamp()
# logger.info(f"程序启动时间戳: {self.start_timestamp}")
# 设置主题
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# # 设置主题
# ctk.set_appearance_mode("dark")
# ctk.set_default_color_theme("blue")
# 创建主窗口
self.root = ctk.CTk()
self.root.title("麦麦推理")
self.root.geometry("800x600")
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
# # 创建主窗口
# self.root = ctk.CTk()
# self.root.title("麦麦推理")
# self.root.geometry("800x600")
# self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
# 存储群组数据
self.group_data: Dict[str, List[dict]] = {}
# # 存储群组数据
# self.group_data: Dict[str, List[dict]] = {}
# 创建更新队列
self.update_queue = queue.Queue()
# # 创建更新队列
# self.update_queue = queue.Queue()
# 创建主框架
self.frame = ctk.CTkFrame(self.root)
self.frame.pack(pady=20, padx=20, fill="both", expand=True)
# # 创建主框架
# self.frame = ctk.CTkFrame(self.root)
# self.frame.pack(pady=20, padx=20, fill="both", expand=True)
# 添加标题
self.title = ctk.CTkLabel(self.frame, text="麦麦的脑内所想", font=("Arial", 24))
self.title.pack(pady=10, padx=10)
# # 添加标题
# self.title = ctk.CTkLabel(self.frame, text="麦麦的脑内所想", font=("Arial", 24))
# self.title.pack(pady=10, padx=10)
# 创建左右分栏
self.paned = ctk.CTkFrame(self.frame)
self.paned.pack(fill="both", expand=True, padx=10, pady=10)
# # 创建左右分栏
# self.paned = ctk.CTkFrame(self.frame)
# self.paned.pack(fill="both", expand=True, padx=10, pady=10)
# 左侧群组列表
self.left_frame = ctk.CTkFrame(self.paned, width=200)
self.left_frame.pack(side="left", fill="y", padx=5, pady=5)
# # 左侧群组列表
# self.left_frame = ctk.CTkFrame(self.paned, width=200)
# self.left_frame.pack(side="left", fill="y", padx=5, pady=5)
self.group_label = ctk.CTkLabel(self.left_frame, text="群组列表", font=("Arial", 16))
self.group_label.pack(pady=5)
# self.group_label = ctk.CTkLabel(self.left_frame, text="群组列表", font=("Arial", 16))
# self.group_label.pack(pady=5)
# 创建可滚动框架来容纳群组按钮
self.group_scroll_frame = ctk.CTkScrollableFrame(self.left_frame, width=180, height=400)
self.group_scroll_frame.pack(pady=5, padx=5, fill="both", expand=True)
# # 创建可滚动框架来容纳群组按钮
# self.group_scroll_frame = ctk.CTkScrollableFrame(self.left_frame, width=180, height=400)
# self.group_scroll_frame.pack(pady=5, padx=5, fill="both", expand=True)
# 存储群组按钮的字典
self.group_buttons: Dict[str, ctk.CTkButton] = {}
# 当前选中的群组ID
self.selected_group_id: Optional[str] = None
# # 存储群组按钮的字典
# self.group_buttons: Dict[str, ctk.CTkButton] = {}
# # 当前选中的群组ID
# self.selected_group_id: Optional[str] = None
# 右侧内容显示
self.right_frame = ctk.CTkFrame(self.paned)
self.right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
# # 右侧内容显示
# self.right_frame = ctk.CTkFrame(self.paned)
# self.right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
self.content_label = ctk.CTkLabel(self.right_frame, text="推理内容", font=("Arial", 16))
self.content_label.pack(pady=5)
# self.content_label = ctk.CTkLabel(self.right_frame, text="推理内容", font=("Arial", 16))
# self.content_label.pack(pady=5)
# 创建富文本显示框
self.content_text = ctk.CTkTextbox(self.right_frame, width=500, height=400)
self.content_text.pack(pady=5, padx=5, fill="both", expand=True)
# # 创建富文本显示框
# self.content_text = ctk.CTkTextbox(self.right_frame, width=500, height=400)
# self.content_text.pack(pady=5, padx=5, fill="both", expand=True)
# 配置文本标签 - 只使用颜色
self.content_text.tag_config("timestamp", foreground="#888888") # 时间戳使用灰色
self.content_text.tag_config("user", foreground="#4CAF50") # 用户名使用绿色
self.content_text.tag_config("message", foreground="#2196F3") # 消息使用蓝色
self.content_text.tag_config("model", foreground="#9C27B0") # 模型名称使用紫色
self.content_text.tag_config("prompt", foreground="#FF9800") # prompt内容使用橙色
self.content_text.tag_config("reasoning", foreground="#FF9800") # 推理过程使用橙色
self.content_text.tag_config("response", foreground="#E91E63") # 回复使用粉色
self.content_text.tag_config("separator", foreground="#666666") # 分隔符使用深灰色
# # 配置文本标签 - 只使用颜色
# self.content_text.tag_config("timestamp", foreground="#888888") # 时间戳使用灰色
# self.content_text.tag_config("user", foreground="#4CAF50") # 用户名使用绿色
# self.content_text.tag_config("message", foreground="#2196F3") # 消息使用蓝色
# self.content_text.tag_config("model", foreground="#9C27B0") # 模型名称使用紫色
# self.content_text.tag_config("prompt", foreground="#FF9800") # prompt内容使用橙色
# self.content_text.tag_config("reasoning", foreground="#FF9800") # 推理过程使用橙色
# self.content_text.tag_config("response", foreground="#E91E63") # 回复使用粉色
# self.content_text.tag_config("separator", foreground="#666666") # 分隔符使用深灰色
# 底部控制栏
self.control_frame = ctk.CTkFrame(self.frame)
self.control_frame.pack(fill="x", padx=10, pady=5)
# # 底部控制栏
# self.control_frame = ctk.CTkFrame(self.frame)
# self.control_frame.pack(fill="x", padx=10, pady=5)
self.clear_button = ctk.CTkButton(self.control_frame, text="清除显示", command=self.clear_display, width=120)
self.clear_button.pack(side="left", padx=5)
# self.clear_button = ctk.CTkButton(self.control_frame, text="清除显示", command=self.clear_display, width=120)
# self.clear_button.pack(side="left", padx=5)
# 启动自动更新线程
self.update_thread = threading.Thread(target=self._auto_update, daemon=True)
self.update_thread.start()
# # 添加标志标记GUI是否已关闭
# self.is_running = True
# 启动GUI更新检查
self.root.after(100, self._process_queue)
# # 启动自动更新线程
# self.update_thread = threading.Thread(target=self._auto_update, daemon=True)
# self.update_thread.start()
def _on_closing(self):
"""处理窗口关闭事件"""
self.root.quit()
sys.exit(0)
# # 启动GUI更新检查
# self.root.after(100, self._process_queue)
def _process_queue(self):
"""处理更新队列中的任务"""
try:
while True:
task = self.update_queue.get_nowait()
if task["type"] == "update_group_list":
self._update_group_list_gui()
elif task["type"] == "update_display":
self._update_display_gui(task["group_id"])
except queue.Empty:
pass
finally:
# 继续检查队列
self.root.after(100, self._process_queue)
# def _on_closing(self):
# """处理窗口关闭事件"""
# # 标记GUI已关闭防止后台线程继续访问tkinter对象
# self.is_running = False
def _update_group_list_gui(self):
"""在主线程中更新群组列表"""
# 清除现有按钮
for button in self.group_buttons.values():
button.destroy()
self.group_buttons.clear()
# # 安全清理所有可能的tkinter变量
# for attr_name in list(self.__dict__.keys()):
# if isinstance(getattr(self, attr_name), (ctk.Variable, ctk.StringVar, ctk.IntVar, ctk.DoubleVar, ctk.BooleanVar)):
# # 删除变量前安全地将其设置为None
# try:
# var = getattr(self, attr_name)
# var.set(None)
# except Exception:
# pass
# setattr(self, attr_name, None)
# 创建新的群组按钮
for group_id in self.group_data.keys():
button = ctk.CTkButton(
self.group_scroll_frame,
text=f"群号: {group_id}",
width=160,
height=30,
corner_radius=8,
command=lambda gid=group_id: self._on_group_select(gid),
)
button.pack(pady=2, padx=5)
self.group_buttons[group_id] = button
# # 退出
# self.root.quit()
# sys.exit(0)
# 如果有选中的群组,保持其高亮状态
if self.selected_group_id and self.selected_group_id in self.group_buttons:
self._highlight_selected_group(self.selected_group_id)
# def _process_queue(self):
# """处理更新队列中的任务"""
# try:
# while True:
# task = self.update_queue.get_nowait()
# if task["type"] == "update_group_list":
# self._update_group_list_gui()
# elif task["type"] == "update_display":
# self._update_display_gui(task["group_id"])
# except queue.Empty:
# pass
# finally:
# # 继续检查队列但仅在GUI仍在运行时
# if self.is_running:
# self.root.after(100, self._process_queue)
def _on_group_select(self, group_id: str):
"""处理群组选择事件"""
self._highlight_selected_group(group_id)
self._update_display_gui(group_id)
# def _update_group_list_gui(self):
# """在主线程中更新群组列表"""
# # 清除现有按钮
# for button in self.group_buttons.values():
# button.destroy()
# self.group_buttons.clear()
def _highlight_selected_group(self, group_id: str):
"""高亮显示选中的群组按钮"""
# 重置所有按钮的颜色
for gid, button in self.group_buttons.items():
if gid == group_id:
# 设置选中按钮的颜色
button.configure(fg_color="#1E88E5", hover_color="#1976D2")
else:
# 恢复其他按钮的默认颜色
button.configure(fg_color="#2B2B2B", hover_color="#404040")
# # 创建新的群组按钮
# for group_id in self.group_data.keys():
# button = ctk.CTkButton(
# self.group_scroll_frame,
# text=f"群号: {group_id}",
# width=160,
# height=30,
# corner_radius=8,
# command=lambda gid=group_id: self._on_group_select(gid),
# )
# button.pack(pady=2, padx=5)
# self.group_buttons[group_id] = button
self.selected_group_id = group_id
# # 如果有选中的群组,保持其高亮状态
# if self.selected_group_id and self.selected_group_id in self.group_buttons:
# self._highlight_selected_group(self.selected_group_id)
def _update_display_gui(self, group_id: str):
"""在主线程中更新显示内容"""
if group_id in self.group_data:
self.content_text.delete("1.0", "end")
for item in self.group_data[group_id]:
# 时间戳
time_str = item["time"].strftime("%Y-%m-%d %H:%M:%S")
self.content_text.insert("end", f"[{time_str}]\n", "timestamp")
# def _on_group_select(self, group_id: str):
# """处理群组选择事件"""
# self._highlight_selected_group(group_id)
# self._update_display_gui(group_id)
# 用户信息
self.content_text.insert("end", "用户: ", "timestamp")
self.content_text.insert("end", f"{item.get('user', '未知')}\n", "user")
# def _highlight_selected_group(self, group_id: str):
# """高亮显示选中的群组按钮"""
# # 重置所有按钮的颜色
# for gid, button in self.group_buttons.items():
# if gid == group_id:
# # 设置选中按钮的颜色
# button.configure(fg_color="#1E88E5", hover_color="#1976D2")
# else:
# # 恢复其他按钮的默认颜色
# button.configure(fg_color="#2B2B2B", hover_color="#404040")
# 消息内容
self.content_text.insert("end", "消息: ", "timestamp")
self.content_text.insert("end", f"{item.get('message', '')}\n", "message")
# self.selected_group_id = group_id
# 模型信息
self.content_text.insert("end", "模型: ", "timestamp")
self.content_text.insert("end", f"{item.get('model', '')}\n", "model")
# def _update_display_gui(self, group_id: str):
# """在主线程中更新显示内容"""
# if group_id in self.group_data:
# self.content_text.delete("1.0", "end")
# for item in self.group_data[group_id]:
# # 时间戳
# time_str = item["time"].strftime("%Y-%m-%d %H:%M:%S")
# self.content_text.insert("end", f"[{time_str}]\n", "timestamp")
# Prompt内容
self.content_text.insert("end", "Prompt内容:\n", "timestamp")
prompt_text = item.get("prompt", "")
if prompt_text and prompt_text.lower() != "none":
lines = prompt_text.split("\n")
for line in lines:
if line.strip():
self.content_text.insert("end", " " + line + "\n", "prompt")
else:
self.content_text.insert("end", " 无Prompt内容\n", "prompt")
# # 用户信息
# self.content_text.insert("end", "用户: ", "timestamp")
# self.content_text.insert("end", f"{item.get('user', '未知')}\n", "user")
# 推理过程
self.content_text.insert("end", "推理过程:\n", "timestamp")
reasoning_text = item.get("reasoning", "")
if reasoning_text and reasoning_text.lower() != "none":
lines = reasoning_text.split("\n")
for line in lines:
if line.strip():
self.content_text.insert("end", " " + line + "\n", "reasoning")
else:
self.content_text.insert("end", " 无推理过程\n", "reasoning")
# # 消息内容
# self.content_text.insert("end", "消息: ", "timestamp")
# self.content_text.insert("end", f"{item.get('message', '')}\n", "message")
# 回复内容
self.content_text.insert("end", "回复: ", "timestamp")
self.content_text.insert("end", f"{item.get('response', '')}\n", "response")
# # 模型信息
# self.content_text.insert("end", "模型: ", "timestamp")
# self.content_text.insert("end", f"{item.get('model', '')}\n", "model")
# 分隔符
self.content_text.insert("end", f"\n{'=' * 50}\n\n", "separator")
# # Prompt内容
# self.content_text.insert("end", "Prompt内容:\n", "timestamp")
# prompt_text = item.get("prompt", "")
# if prompt_text and prompt_text.lower() != "none":
# lines = prompt_text.split("\n")
# for line in lines:
# if line.strip():
# self.content_text.insert("end", " " + line + "\n", "prompt")
# else:
# self.content_text.insert("end", " 无Prompt内容\n", "prompt")
# 滚动到顶部
self.content_text.see("1.0")
# # 推理过程
# self.content_text.insert("end", "推理过程:\n", "timestamp")
# reasoning_text = item.get("reasoning", "")
# if reasoning_text and reasoning_text.lower() != "none":
# lines = reasoning_text.split("\n")
# for line in lines:
# if line.strip():
# self.content_text.insert("end", " " + line + "\n", "reasoning")
# else:
# self.content_text.insert("end", " 无推理过程\n", "reasoning")
def _auto_update(self):
"""自动更新函数"""
while True:
try:
# 从数据库获取最新数据,只获取启动时间之后的记录
query = {"time": {"$gt": self.start_timestamp}}
logger.debug(f"查询条件: {query}")
# # 回复内容
# self.content_text.insert("end", "回复: ", "timestamp")
# self.content_text.insert("end", f"{item.get('response', '')}\n", "response")
# 先获取一条记录检查时间格式
sample = db.reasoning_logs.find_one()
if sample:
logger.debug(f"样本记录时间格式: {type(sample['time'])} 值: {sample['time']}")
# # 分隔符
# self.content_text.insert("end", f"\n{'=' * 50}\n\n", "separator")
cursor = db.reasoning_logs.find(query).sort("time", -1)
new_data = {}
total_count = 0
# # 滚动到顶部
# self.content_text.see("1.0")
for item in cursor:
# 调试输出
if total_count == 0:
logger.debug(f"记录时间: {item['time']}, 类型: {type(item['time'])}")
# def _auto_update(self):
# """自动更新函数"""
# while True:
# if not self.is_running:
# break # 如果GUI已关闭停止线程
total_count += 1
group_id = str(item.get("group_id", "unknown"))
if group_id not in new_data:
new_data[group_id] = []
# try:
# # 从数据库获取最新数据,只获取启动时间之后的记录
# query = {"time": {"$gt": self.start_timestamp}}
# logger.debug(f"查询条件: {query}")
# 转换时间戳为datetime对象
if isinstance(item["time"], (int, float)):
time_obj = datetime.fromtimestamp(item["time"])
elif isinstance(item["time"], datetime):
time_obj = item["time"]
else:
logger.warning(f"未知的时间格式: {type(item['time'])}")
time_obj = datetime.now() # 使用当前时间作为后备
# # 先获取一条记录检查时间格式
# sample = db.reasoning_logs.find_one()
# if sample:
# logger.debug(f"样本记录时间格式: {type(sample['time'])} 值: {sample['time']}")
new_data[group_id].append(
{
"time": time_obj,
"user": item.get("user", "未知"),
"message": item.get("message", ""),
"model": item.get("model", "未知"),
"reasoning": item.get("reasoning", ""),
"response": item.get("response", ""),
"prompt": item.get("prompt", ""), # 添加prompt字段
}
)
# cursor = db.reasoning_logs.find(query).sort("time", -1)
# new_data = {}
# total_count = 0
logger.info(f"从数据库加载了 {total_count} 条记录,分布在 {len(new_data)} 个群组中")
# for item in cursor:
# # 调试输出
# if total_count == 0:
# logger.debug(f"记录时间: {item['time']}, 类型: {type(item['time'])}")
# 更新数据
if new_data != self.group_data:
self.group_data = new_data
logger.info("数据已更新,正在刷新显示...")
# 将更新任务添加到队列
self.update_queue.put({"type": "update_group_list"})
if self.group_data:
# 如果没有选中的群组,选择最新的群组
if not self.selected_group_id or self.selected_group_id not in self.group_data:
self.selected_group_id = next(iter(self.group_data))
self.update_queue.put({"type": "update_display", "group_id": self.selected_group_id})
except Exception:
logger.exception("自动更新出错")
# total_count += 1
# group_id = str(item.get("group_id", "unknown"))
# if group_id not in new_data:
# new_data[group_id] = []
# 每5秒更新一次
time.sleep(5)
# # 转换时间戳为datetime对象
# if isinstance(item["time"], (int, float)):
# time_obj = datetime.fromtimestamp(item["time"])
# elif isinstance(item["time"], datetime):
# time_obj = item["time"]
# else:
# logger.warning(f"未知的时间格式: {type(item['time'])}")
# time_obj = datetime.now() # 使用当前时间作为后备
def clear_display(self):
"""清除显示内容"""
self.content_text.delete("1.0", "end")
# new_data[group_id].append(
# {
# "time": time_obj,
# "user": item.get("user", "未知"),
# "message": item.get("message", ""),
# "model": item.get("model", "未知"),
# "reasoning": item.get("reasoning", ""),
# "response": item.get("response", ""),
# "prompt": item.get("prompt", ""), # 添加prompt字段
# }
# )
def run(self):
"""运行GUI"""
self.root.mainloop()
# logger.info(f"从数据库加载了 {total_count} 条记录,分布在 {len(new_data)} 个群组中")
# # 更新数据
# if new_data != self.group_data:
# self.group_data = new_data
# logger.info("数据已更新,正在刷新显示...")
# # 将更新任务添加到队列
# self.update_queue.put({"type": "update_group_list"})
# if self.group_data:
# # 如果没有选中的群组,选择最新的群组
# if not self.selected_group_id or self.selected_group_id not in self.group_data:
# self.selected_group_id = next(iter(self.group_data))
# self.update_queue.put({"type": "update_display", "group_id": self.selected_group_id})
# except Exception:
# logger.exception("自动更新出错")
# # 每5秒更新一次
# time.sleep(5)
# def clear_display(self):
# """清除显示内容"""
# self.content_text.delete("1.0", "end")
# def run(self):
# """运行GUI"""
# self.root.mainloop()
def main():
app = ReasoningGUI()
app.run()
# def main():
# app = ReasoningGUI()
# app.run()
if __name__ == "__main__":
main()
# if __name__ == "__main__":
# main()

View File

@@ -6,7 +6,9 @@ from src.plugins.config.config import global_config
from src.plugins.schedule.schedule_generator import bot_schedule
import asyncio
from src.common.logger import get_module_logger, LogConfig, HEARTFLOW_STYLE_CONFIG # noqa: E402
from src.individuality.individuality import Individuality
import time
import random
heartflow_config = LogConfig(
# 使用海马体专用样式
@@ -40,8 +42,6 @@ class Heartflow:
self._subheartflows = {}
self.active_subheartflows_nums = 0
self.personality_info = " ".join(global_config.PROMPT_PERSONALITY)
async def _cleanup_inactive_subheartflows(self):
"""定期清理不活跃的子心流"""
while True:
@@ -70,7 +70,7 @@ class Heartflow:
while True:
# 检查是否存在子心流
if not self._subheartflows:
logger.info("当前没有子心流,等待新的子心流创建...")
# logger.info("当前没有子心流,等待新的子心流创建...")
await asyncio.sleep(30) # 每分钟检查一次是否有新的子心流
continue
@@ -81,7 +81,24 @@ class Heartflow:
logger.debug("麦麦大脑袋转起来了")
self.current_state.update_current_state_info()
personality_info = self.personality_info
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
personality_info = prompt_personality
current_thinking_info = self.current_mind
mood_info = self.current_state.mood
related_memory_info = "memory"
@@ -123,7 +140,23 @@ class Heartflow:
return await self.minds_summary(sub_minds)
async def minds_summary(self, minds_str):
personality_info = self.personality_info
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
personality_info = prompt_personality
mood_info = self.current_state.mood
prompt = ""
@@ -144,7 +177,7 @@ class Heartflow:
添加一个SubHeartflow实例到self._subheartflows字典中
并根据subheartflow_id为子心流创建一个观察对象
"""
try:
if subheartflow_id not in self._subheartflows:
logger.debug(f"创建 subheartflow: {subheartflow_id}")

View File

@@ -4,6 +4,8 @@ from datetime import datetime
from src.plugins.models.utils_model import LLM_request
from src.plugins.config.config import global_config
from src.common.database import db
from src.individuality.individuality import Individuality
import random
# 所有观察的基类
@@ -23,8 +25,7 @@ class ChattingObservation(Observation):
self.talking_message = []
self.talking_message_str = ""
self.personality_info = " ".join(global_config.PROMPT_PERSONALITY)
self.name = global_config.BOT_NICKNAME
self.nick_name = global_config.BOT_ALIAS_NAMES
@@ -57,7 +58,7 @@ class ChattingObservation(Observation):
for msg in new_messages:
if "detailed_plain_text" in msg:
new_messages_str += f"{msg['detailed_plain_text']}"
# print(f"new_messages_str{new_messages_str}")
# 将新消息添加到talking_message同时保持列表长度不超过20条
@@ -115,8 +116,26 @@ class ChattingObservation(Observation):
async def update_talking_summary(self, new_messages_str):
# 基于已经有的talking_summary和新的talking_message生成一个summary
# print(f"更新聊天总结:{self.talking_summary}")
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
personality_info = prompt_personality
prompt = ""
prompt += f"{self.personality_info},请注意识别你自己的聊天发言"
prompt += f"{personality_info},请注意识别你自己的聊天发言"
prompt += f"你的名字叫:{self.name},你的昵称是:{self.nick_name}\n"
prompt += f"你正在参与一个qq群聊的讨论你记得这个群之前在聊的内容是{self.observe_info}\n"
prompt += f"现在群里的群友们产生了新的讨论,有了新的发言,具体内容如下:{new_messages_str}\n"
@@ -126,7 +145,6 @@ class ChattingObservation(Observation):
self.observe_info, reasoning_content = await self.llm_summary.generate_response_async(prompt)
print(f"prompt{prompt}")
print(f"self.observe_info{self.observe_info}")
def translate_message_list_to_str(self):
self.talking_message_str = ""

View File

@@ -11,6 +11,8 @@ from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_
from src.plugins.chat.utils import get_embedding
from src.common.database import db
from typing import Union
from src.individuality.individuality import Individuality
import random
subheartflow_config = LogConfig(
# 使用海马体专用样式
@@ -51,12 +53,10 @@ class SubHeartflow:
if not self.current_mind:
self.current_mind = "你什么也没想"
self.personality_info = " ".join(global_config.PROMPT_PERSONALITY)
self.is_active = False
self.observations: list[Observation] = []
self.running_knowledges = []
def add_observation(self, observation: Observation):
@@ -85,7 +85,9 @@ class SubHeartflow:
async def subheartflow_start_working(self):
while True:
current_time = time.time()
if current_time - self.last_reply_time > global_config.sub_heart_flow_freeze_time: # 120秒无回复/不在场,冻结
if (
current_time - self.last_reply_time > global_config.sub_heart_flow_freeze_time
): # 120秒无回复/不在场,冻结
self.is_active = False
await asyncio.sleep(global_config.sub_heart_flow_update_interval) # 每60秒检查一次
else:
@@ -99,7 +101,9 @@ class SubHeartflow:
await asyncio.sleep(global_config.sub_heart_flow_update_interval)
# 检查是否超过10分钟没有激活
if current_time - self.last_active_time > global_config.sub_heart_flow_stop_time: # 5分钟无回复/不在场,销毁
if (
current_time - self.last_active_time > global_config.sub_heart_flow_stop_time
): # 5分钟无回复/不在场,销毁
logger.info(f"子心流 {self.subheartflow_id} 已经5分钟没有激活正在销毁...")
break # 退出循环,销毁自己
@@ -146,11 +150,11 @@ class SubHeartflow:
# self.current_mind = reponse
# logger.debug(f"prompt:\n{prompt}\n")
# logger.info(f"麦麦的脑内状态:{self.current_mind}")
async def do_observe(self):
observation = self.observations[0]
await observation.observe()
async def do_thinking_before_reply(self, message_txt):
current_thinking_info = self.current_mind
mood_info = self.current_state.mood
@@ -159,6 +163,22 @@ class SubHeartflow:
chat_observe_info = observation.observe_info
# print(f"chat_observe_info{chat_observe_info}")
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
# 调取记忆
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
text=chat_observe_info, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False
@@ -171,11 +191,11 @@ class SubHeartflow:
else:
related_memory_info = ""
related_info,grouped_results = await self.get_prompt_info(chat_observe_info + message_txt, 0.4)
print(related_info)
for topic, results in grouped_results.items():
related_info, grouped_results = await self.get_prompt_info(chat_observe_info + message_txt, 0.4)
# print(related_info)
for _topic, results in grouped_results.items():
for result in results:
print(result)
# print(result)
self.running_knowledges.append(result)
# print(f"相关记忆:{related_memory_info}")
@@ -184,7 +204,7 @@ class SubHeartflow:
prompt = ""
# prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n"
prompt += f"{self.personality_info}\n"
prompt += f"{prompt_personality}\n"
prompt += f"你刚刚在做的事情是:{schedule_info}\n"
if related_memory_info:
prompt += f"你想起来你之前见过的回忆:{related_memory_info}\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n"
@@ -207,6 +227,23 @@ class SubHeartflow:
async def do_thinking_after_reply(self, reply_content, chat_talking_prompt):
# print("麦麦回复之后脑袋转起来了")
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
current_thinking_info = self.current_mind
mood_info = self.current_state.mood
@@ -219,7 +256,7 @@ class SubHeartflow:
prompt = ""
# prompt += f"你现在正在做的事情是:{schedule_info}\n"
prompt += f"{self.personality_info}\n"
prompt += f"{prompt_personality}\n"
prompt += f"现在你正在上网和qq群里的网友们聊天群里正在聊的话题是{chat_observe_info}\n"
prompt += f"刚刚你的想法是{current_thinking_info}"
prompt += f"你现在看到了网友们发的新消息:{message_new_info}\n"
@@ -238,12 +275,28 @@ class SubHeartflow:
self.last_reply_time = time.time()
async def judge_willing(self):
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
# print("麦麦闹情绪了1")
current_thinking_info = self.current_mind
mood_info = self.current_state.mood
# print("麦麦闹情绪了2")
prompt = ""
prompt += f"{self.personality_info}\n"
prompt += f"{prompt_personality}\n"
prompt += "现在你正在上网和qq群里的网友们聊天"
prompt += f"你现在的想法是{current_thinking_info}"
prompt += f"你现在{mood_info}"
@@ -263,13 +316,12 @@ class SubHeartflow:
def update_current_mind(self, reponse):
self.past_mind.append(self.current_mind)
self.current_mind = reponse
async def get_prompt_info(self, message: str, threshold: float):
start_time = time.time()
related_info = ""
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 1. 先从LLM获取主题类似于记忆系统的做法
topics = []
# try:
@@ -277,7 +329,7 @@ class SubHeartflow:
# hippocampus = HippocampusManager.get_instance()._hippocampus
# topic_num = min(5, max(1, int(len(message) * 0.1)))
# topics_response = await hippocampus.llm_topic_judge.generate_response(hippocampus.find_topic_llm(message, topic_num))
# # 提取关键词
# topics = re.findall(r"<([^>]+)>", topics_response[0])
# if not topics:
@@ -288,7 +340,7 @@ class SubHeartflow:
# for topic in ",".join(topics).replace("", ",").replace("、", ",").replace(" ", ",").split(",")
# if topic.strip()
# ]
# logger.info(f"从LLM提取的主题: {', '.join(topics)}")
# except Exception as e:
# logger.error(f"从LLM提取主题失败: {str(e)}")
@@ -296,34 +348,34 @@ class SubHeartflow:
# words = jieba.cut(message)
# topics = [word for word in words if len(word) > 1][:5]
# logger.info(f"使用jieba提取的主题: {', '.join(topics)}")
# 如果无法提取到主题,直接使用整个消息
if not topics:
logger.info("未能提取到任何主题,使用整个消息进行查询")
logger.debug("未能提取到任何主题,使用整个消息进行查询")
embedding = await get_embedding(message, request_type="info_retrieval")
if not embedding:
logger.error("获取消息嵌入向量失败")
return ""
related_info = self.get_info_from_db(embedding, limit=3, threshold=threshold)
logger.info(f"知识库检索完成,总耗时: {time.time() - start_time:.3f}")
return related_info, {}
# 2. 对每个主题进行知识库查询
logger.info(f"开始处理{len(topics)}个主题的知识库查询")
# 优化批量获取嵌入向量减少API调用
embeddings = {}
topics_batch = [topic for topic in topics if len(topic) > 0]
if message: # 确保消息非空
topics_batch.append(message)
# 批量获取嵌入向量
embed_start_time = time.time()
for text in topics_batch:
if not text or len(text.strip()) == 0:
continue
try:
embedding = await get_embedding(text, request_type="info_retrieval")
if embedding:
@@ -332,17 +384,17 @@ class SubHeartflow:
logger.warning(f"获取'{text}'的嵌入向量失败")
except Exception as e:
logger.error(f"获取'{text}'的嵌入向量时发生错误: {str(e)}")
logger.info(f"批量获取嵌入向量完成,耗时: {time.time() - embed_start_time:.3f}")
if not embeddings:
logger.error("所有嵌入向量获取失败")
return ""
# 3. 对每个主题进行知识库查询
all_results = []
query_start_time = time.time()
# 首先添加原始消息的查询结果
if message in embeddings:
original_results = self.get_info_from_db(embeddings[message], limit=3, threshold=threshold, return_raw=True)
@@ -351,12 +403,12 @@ class SubHeartflow:
result["topic"] = "原始消息"
all_results.extend(original_results)
logger.info(f"原始消息查询到{len(original_results)}条结果")
# 然后添加每个主题的查询结果
for topic in topics:
if not topic or topic not in embeddings:
continue
try:
topic_results = self.get_info_from_db(embeddings[topic], limit=3, threshold=threshold, return_raw=True)
if topic_results:
@@ -367,9 +419,9 @@ class SubHeartflow:
logger.info(f"主题'{topic}'查询到{len(topic_results)}条结果")
except Exception as e:
logger.error(f"查询主题'{topic}'时发生错误: {str(e)}")
logger.info(f"知识库查询完成,耗时: {time.time() - query_start_time:.3f}秒,共获取{len(all_results)}条结果")
# 4. 去重和过滤
process_start_time = time.time()
unique_contents = set()
@@ -379,14 +431,16 @@ class SubHeartflow:
if content not in unique_contents:
unique_contents.add(content)
filtered_results.append(result)
# 5. 按相似度排序
filtered_results.sort(key=lambda x: x["similarity"], reverse=True)
# 6. 限制总数量最多10条
filtered_results = filtered_results[:10]
logger.info(f"结果处理完成,耗时: {time.time() - process_start_time:.3f}秒,过滤后剩余{len(filtered_results)}条结果")
logger.info(
f"结果处理完成,耗时: {time.time() - process_start_time:.3f}秒,过滤后剩余{len(filtered_results)}条结果"
)
# 7. 格式化输出
if filtered_results:
format_start_time = time.time()
@@ -396,24 +450,26 @@ class SubHeartflow:
if topic not in grouped_results:
grouped_results[topic] = []
grouped_results[topic].append(result)
# 按主题组织输出
for topic, results in grouped_results.items():
related_info += f"【主题: {topic}\n"
for i, result in enumerate(results, 1):
similarity = result["similarity"]
for _i, result in enumerate(results, 1):
_similarity = result["similarity"]
content = result["content"].strip()
# 调试:为内容添加序号和相似度信息
# related_info += f"{i}. [{similarity:.2f}] {content}\n"
related_info += f"{content}\n"
related_info += "\n"
logger.info(f"格式化输出完成,耗时: {time.time() - format_start_time:.3f}")
logger.info(f"知识库检索总耗时: {time.time() - start_time:.3f}")
return related_info,grouped_results
def get_info_from_db(self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False) -> Union[str, list]:
logger.info(f"格式化输出完成,耗时: {time.time() - format_start_time:.3f}")
logger.info(f"知识库检索总耗时: {time.time() - start_time:.3f}")
return related_info, grouped_results
def get_info_from_db(
self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False
) -> Union[str, list]:
if not query_embedding:
return "" if not return_raw else []
# 使用余弦相似度计算

View File

@@ -0,0 +1,127 @@
from dataclasses import dataclass
from typing import List
import random
@dataclass
class Identity:
"""身份特征类"""
identity_detail: List[str] # 身份细节描述
height: int # 身高(厘米)
weight: int # 体重(千克)
age: int # 年龄
gender: str # 性别
appearance: str # 外貌特征
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(
self,
identity_detail: List[str] = None,
height: int = 0,
weight: int = 0,
age: int = 0,
gender: str = "",
appearance: str = "",
):
"""初始化身份特征
Args:
identity_detail: 身份细节描述列表
height: 身高(厘米)
weight: 体重(千克)
age: 年龄
gender: 性别
appearance: 外貌特征
"""
if identity_detail is None:
identity_detail = []
self.identity_detail = identity_detail
self.height = height
self.weight = weight
self.age = age
self.gender = gender
self.appearance = appearance
@classmethod
def get_instance(cls) -> "Identity":
"""获取Identity单例实例
Returns:
Identity: 单例实例
"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
@classmethod
def initialize(
cls, identity_detail: List[str], height: int, weight: int, age: int, gender: str, appearance: str
) -> "Identity":
"""初始化身份特征
Args:
identity_detail: 身份细节描述列表
height: 身高(厘米)
weight: 体重(千克)
age: 年龄
gender: 性别
appearance: 外貌特征
Returns:
Identity: 初始化后的身份特征实例
"""
instance = cls.get_instance()
instance.identity_detail = identity_detail
instance.height = height
instance.weight = weight
instance.age = age
instance.gender = gender
instance.appearance = appearance
return instance
def get_prompt(self, x_person, level):
"""
获取身份特征的prompt
"""
if x_person == 2:
prompt_identity = ""
elif x_person == 1:
prompt_identity = ""
else:
prompt_identity = ""
if level == 1:
identity_detail = self.identity_detail
random.shuffle(identity_detail)
prompt_identity += identity_detail[0]
elif level == 2:
for detail in identity_detail:
prompt_identity += f",{detail}"
prompt_identity += ""
return prompt_identity
def to_dict(self) -> dict:
"""将身份特征转换为字典格式"""
return {
"identity_detail": self.identity_detail,
"height": self.height,
"weight": self.weight,
"age": self.age,
"gender": self.gender,
"appearance": self.appearance,
}
@classmethod
def from_dict(cls, data: dict) -> "Identity":
"""从字典创建身份特征实例"""
instance = cls.get_instance()
for key, value in data.items():
setattr(instance, key, value)
return instance

View File

@@ -0,0 +1,107 @@
from typing import Optional
from .personality import Personality
from .identity import Identity
class Individuality:
"""个体特征管理类"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.personality: Optional[Personality] = None
self.identity: Optional[Identity] = None
@classmethod
def get_instance(cls) -> "Individuality":
"""获取Individuality单例实例
Returns:
Individuality: 单例实例
"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def initialize(
self,
bot_nickname: str,
personality_core: str,
personality_sides: list,
identity_detail: list,
height: int,
weight: int,
age: int,
gender: str,
appearance: str,
) -> None:
"""初始化个体特征
Args:
bot_nickname: 机器人昵称
personality_core: 人格核心特点
personality_sides: 人格侧面描述
identity_detail: 身份细节描述
height: 身高(厘米)
weight: 体重(千克)
age: 年龄
gender: 性别
appearance: 外貌特征
"""
# 初始化人格
self.personality = Personality.initialize(
bot_nickname=bot_nickname, personality_core=personality_core, personality_sides=personality_sides
)
# 初始化身份
self.identity = Identity.initialize(
identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance
)
def to_dict(self) -> dict:
"""将个体特征转换为字典格式"""
return {
"personality": self.personality.to_dict() if self.personality else None,
"identity": self.identity.to_dict() if self.identity else None,
}
@classmethod
def from_dict(cls, data: dict) -> "Individuality":
"""从字典创建个体特征实例"""
instance = cls.get_instance()
if data.get("personality"):
instance.personality = Personality.from_dict(data["personality"])
if data.get("identity"):
instance.identity = Identity.from_dict(data["identity"])
return instance
def get_prompt(self, type, x_person, level):
"""
获取个体特征的prompt
"""
if type == "personality":
return self.personality.get_prompt(x_person, level)
elif type == "identity":
return self.identity.get_prompt(x_person, level)
else:
return ""
def get_traits(self, factor):
"""
获取个体特征的特质
"""
if factor == "openness":
return self.personality.openness
elif factor == "conscientiousness":
return self.personality.conscientiousness
elif factor == "extraversion":
return self.personality.extraversion
elif factor == "agreeableness":
return self.personality.agreeableness
elif factor == "neuroticism":
return self.personality.neuroticism

View File

@@ -0,0 +1,123 @@
import asyncio
import os
import time
from typing import Tuple, Union
import aiohttp
import requests
from src.common.logger import get_module_logger
logger = get_module_logger("offline_llm")
class LLM_request_off:
def __init__(self, model_name="Pro/deepseek-ai/DeepSeek-V3", **kwargs):
self.model_name = model_name
self.params = kwargs
self.api_key = os.getenv("SILICONFLOW_KEY")
self.base_url = os.getenv("SILICONFLOW_BASE_URL")
if not self.api_key or not self.base_url:
raise ValueError("环境变量未正确加载SILICONFLOW_KEY 或 SILICONFLOW_BASE_URL 未设置")
# logger.info(f"API URL: {self.base_url}") # 使用 logger 记录 base_url
def generate_response(self, prompt: str) -> Union[str, Tuple[str, str]]:
"""根据输入的提示生成模型的响应"""
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
# 构建请求体
data = {
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.4,
**self.params,
}
# 发送请求到完整的 chat/completions 端点
api_url = f"{self.base_url.rstrip('/')}/chat/completions"
logger.info(f"Request URL: {api_url}") # 记录请求的 URL
max_retries = 3
base_wait_time = 15 # 基础等待时间(秒)
for retry in range(max_retries):
try:
response = requests.post(api_url, headers=headers, json=data)
if response.status_code == 429:
wait_time = base_wait_time * (2**retry) # 指数退避
logger.warning(f"遇到请求限制(429),等待{wait_time}秒后重试...")
time.sleep(wait_time)
continue
response.raise_for_status() # 检查其他响应状态
result = response.json()
if "choices" in result and len(result["choices"]) > 0:
content = result["choices"][0]["message"]["content"]
reasoning_content = result["choices"][0]["message"].get("reasoning_content", "")
return content, reasoning_content
return "没有返回结果", ""
except Exception as e:
if retry < max_retries - 1: # 如果还有重试机会
wait_time = base_wait_time * (2**retry)
logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}")
time.sleep(wait_time)
else:
logger.error(f"请求失败: {str(e)}")
return f"请求失败: {str(e)}", ""
logger.error("达到最大重试次数,请求仍然失败")
return "达到最大重试次数,请求仍然失败", ""
async def generate_response_async(self, prompt: str) -> Union[str, Tuple[str, str]]:
"""异步方式根据输入的提示生成模型的响应"""
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
# 构建请求体
data = {
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.5,
**self.params,
}
# 发送请求到完整的 chat/completions 端点
api_url = f"{self.base_url.rstrip('/')}/chat/completions"
logger.info(f"Request URL: {api_url}") # 记录请求的 URL
max_retries = 3
base_wait_time = 15
async with aiohttp.ClientSession() as session:
for retry in range(max_retries):
try:
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
response.raise_for_status() # 检查其他响应状态
result = await response.json()
if "choices" in result and len(result["choices"]) > 0:
content = result["choices"][0]["message"]["content"]
reasoning_content = result["choices"][0]["message"].get("reasoning_content", "")
return content, reasoning_content
return "没有返回结果", ""
except Exception as e:
if retry < max_retries - 1: # 如果还有重试机会
wait_time = base_wait_time * (2**retry)
logger.error(f"[回复]请求失败,等待{wait_time}秒后重试... 错误: {str(e)}")
await asyncio.sleep(wait_time)
else:
logger.error(f"请求失败: {str(e)}")
return f"请求失败: {str(e)}", ""
logger.error("达到最大重试次数,请求仍然失败")
return "达到最大重试次数,请求仍然失败", ""

View File

@@ -0,0 +1,311 @@
from typing import Dict, List
import json
import os
from dotenv import load_dotenv
import sys
import toml
import random
from tqdm import tqdm
# 添加项目根目录到 Python 路径
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
sys.path.append(root_path)
# 加载配置文件
config_path = os.path.join(root_path, "config", "bot_config.toml")
with open(config_path, "r", encoding="utf-8") as f:
config = toml.load(f)
# 现在可以导入src模块
from src.individuality.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa E402
from src.individuality.questionnaire import FACTOR_DESCRIPTIONS # noqa E402
from src.individuality.offline_llm import LLM_request_off # noqa E402
# 加载环境变量
env_path = os.path.join(root_path, ".env")
if os.path.exists(env_path):
print(f"{env_path} 加载环境变量")
load_dotenv(env_path)
else:
print(f"未找到环境变量文件: {env_path}")
print("将使用默认配置")
def adapt_scene(scene: str) -> str:
personality_core = config["personality"]["personality_core"]
personality_sides = config["personality"]["personality_sides"]
personality_side = random.choice(personality_sides)
identity_details = config["identity"]["identity_detail"]
identity_detail = random.choice(identity_details)
"""
根据config中的属性改编场景使其更适合当前角色
Args:
scene: 原始场景描述
Returns:
str: 改编后的场景描述
"""
try:
prompt = f"""
这是一个参与人格测评的角色形象:
- 昵称: {config["bot"]["nickname"]}
- 性别: {config["identity"]["gender"]}
- 年龄: {config["identity"]["age"]}
- 外貌: {config["identity"]["appearance"]}
- 性格核心: {personality_core}
- 性格侧面: {personality_side}
- 身份细节: {identity_detail}
请根据上述形象,改编以下场景,在测评中,用户将根据该场景给出上述角色形象的反应:
{scene}
保持场景的本质不变,但最好贴近生活且具体,并且让它更适合这个角色。
改编后的场景应该自然、连贯,并考虑角色的年龄、身份和性格特点。只返回改编后的场景描述,不要包含其他说明。注意{config["bot"]["nickname"]}是面对这个场景的人,而不是场景的其他人。场景中不会有其描述,
现在,请你给出改编后的场景描述
"""
llm = LLM_request_off(model_name=config["model"]["llm_normal"]["name"])
adapted_scene, _ = llm.generate_response(prompt)
# 检查返回的场景是否为空或错误信息
if not adapted_scene or "错误" in adapted_scene or "失败" in adapted_scene:
print("场景改编失败,将使用原始场景")
return scene
return adapted_scene
except Exception as e:
print(f"场景改编过程出错:{str(e)},将使用原始场景")
return scene
class PersonalityEvaluator_direct:
def __init__(self):
self.personality_traits = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0}
self.scenarios = []
self.final_scores = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0}
self.dimension_counts = {trait: 0 for trait in self.final_scores.keys()}
# 为每个人格特质获取对应的场景
for trait in PERSONALITY_SCENES:
scenes = get_scene_by_factor(trait)
if not scenes:
continue
# 从每个维度选择3个场景
import random
scene_keys = list(scenes.keys())
selected_scenes = random.sample(scene_keys, min(3, len(scene_keys)))
for scene_key in selected_scenes:
scene = scenes[scene_key]
# 为每个场景添加评估维度
# 主维度是当前特质,次维度随机选择一个其他特质
other_traits = [t for t in PERSONALITY_SCENES if t != trait]
secondary_trait = random.choice(other_traits)
self.scenarios.append(
{"场景": scene["scenario"], "评估维度": [trait, secondary_trait], "场景编号": scene_key}
)
self.llm = LLM_request_off()
def evaluate_response(self, scenario: str, response: str, dimensions: List[str]) -> Dict[str, float]:
"""
使用 DeepSeek AI 评估用户对特定场景的反应
"""
# 构建维度描述
dimension_descriptions = []
for dim in dimensions:
desc = FACTOR_DESCRIPTIONS.get(dim, "")
if desc:
dimension_descriptions.append(f"- {dim}{desc}")
dimensions_text = "\n".join(dimension_descriptions)
prompt = f"""请根据以下场景和用户描述评估用户在大五人格模型中的相关维度得分1-6分
场景描述:
{scenario}
用户回应:
{response}
需要评估的维度说明:
{dimensions_text}
请按照以下格式输出评估结果仅输出JSON格式
{{
"{dimensions[0]}": 分数,
"{dimensions[1]}": 分数
}}
评分标准:
1 = 非常不符合该维度特征
2 = 比较不符合该维度特征
3 = 有点不符合该维度特征
4 = 有点符合该维度特征
5 = 比较符合该维度特征
6 = 非常符合该维度特征
请根据用户的回应结合场景和维度说明进行评分。确保分数在1-6之间并给出合理的评估。"""
try:
ai_response, _ = self.llm.generate_response(prompt)
# 尝试从AI响应中提取JSON部分
start_idx = ai_response.find("{")
end_idx = ai_response.rfind("}") + 1
if start_idx != -1 and end_idx != 0:
json_str = ai_response[start_idx:end_idx]
scores = json.loads(json_str)
# 确保所有分数在1-6之间
return {k: max(1, min(6, float(v))) for k, v in scores.items()}
else:
print("AI响应格式不正确使用默认评分")
return {dim: 3.5 for dim in dimensions}
except Exception as e:
print(f"评估过程出错:{str(e)}")
return {dim: 3.5 for dim in dimensions}
def run_evaluation(self):
"""
运行整个评估过程
"""
print(f"欢迎使用{config['bot']['nickname']}形象创建程序!")
print("接下来将给您呈现一系列有关您bot的场景共15个")
print("请想象您的bot在以下场景下会做什么并描述您的bot的反应。")
print("每个场景都会进行不同方面的评估。")
print("\n角色基本信息:")
print(f"- 昵称:{config['bot']['nickname']}")
print(f"- 性格核心:{config['personality']['personality_core']}")
print(f"- 性格侧面:{config['personality']['personality_sides']}")
print(f"- 身份细节:{config['identity']['identity_detail']}")
print("\n准备好了吗?按回车键开始...")
input()
total_scenarios = len(self.scenarios)
progress_bar = tqdm(
total=total_scenarios,
desc="场景进度",
ncols=100,
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]",
)
for _i, scenario_data in enumerate(self.scenarios, 1):
# print(f"\n{'-' * 20} 场景 {i}/{total_scenarios} - {scenario_data['场景编号']} {'-' * 20}")
# 改编场景,使其更适合当前角色
print(f"{config['bot']['nickname']}祈祷中...")
adapted_scene = adapt_scene(scenario_data["场景"])
scenario_data["改编场景"] = adapted_scene
print(adapted_scene)
print(f"\n请描述{config['bot']['nickname']}在这种情况下会如何反应:")
response = input().strip()
if not response:
print("反应描述不能为空!")
continue
print("\n正在评估您的描述...")
scores = self.evaluate_response(adapted_scene, response, scenario_data["评估维度"])
# 更新最终分数
for dimension, score in scores.items():
self.final_scores[dimension] += score
self.dimension_counts[dimension] += 1
print("\n当前评估结果:")
print("-" * 30)
for dimension, score in scores.items():
print(f"{dimension}: {score}/6")
# 更新进度条
progress_bar.update(1)
# if i < total_scenarios:
# print("\n按回车键继续下一个场景...")
# input()
progress_bar.close()
# 计算平均分
for dimension in self.final_scores:
if self.dimension_counts[dimension] > 0:
self.final_scores[dimension] = round(self.final_scores[dimension] / self.dimension_counts[dimension], 2)
print("\n" + "=" * 50)
print(f" {config['bot']['nickname']}的人格特征评估结果 ".center(50))
print("=" * 50)
for trait, score in self.final_scores.items():
print(f"{trait}: {score}/6".ljust(20) + f"测试场景数:{self.dimension_counts[trait]}".rjust(30))
print("=" * 50)
# 返回评估结果
return self.get_result()
def get_result(self):
"""
获取评估结果
"""
return {
"final_scores": self.final_scores,
"dimension_counts": self.dimension_counts,
"scenarios": self.scenarios,
"bot_info": {
"nickname": config["bot"]["nickname"],
"gender": config["identity"]["gender"],
"age": config["identity"]["age"],
"height": config["identity"]["height"],
"weight": config["identity"]["weight"],
"appearance": config["identity"]["appearance"],
"personality_core": config["personality"]["personality_core"],
"personality_sides": config["personality"]["personality_sides"],
"identity_detail": config["identity"]["identity_detail"],
},
}
def main():
evaluator = PersonalityEvaluator_direct()
result = evaluator.run_evaluation()
# 准备简化的结果数据
simplified_result = {
"openness": round(result["final_scores"]["开放性"] / 6, 1), # 转换为0-1范围
"conscientiousness": round(result["final_scores"]["严谨性"] / 6, 1),
"extraversion": round(result["final_scores"]["外向性"] / 6, 1),
"agreeableness": round(result["final_scores"]["宜人性"] / 6, 1),
"neuroticism": round(result["final_scores"]["神经质"] / 6, 1),
"bot_nickname": config["bot"]["nickname"],
}
# 确保目录存在
save_dir = os.path.join(root_path, "data", "personality")
os.makedirs(save_dir, exist_ok=True)
# 创建文件名,替换可能的非法字符
bot_name = config["bot"]["nickname"]
# 替换Windows文件名中不允许的字符
for char in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]:
bot_name = bot_name.replace(char, "_")
file_name = f"{bot_name}_personality.per"
save_path = os.path.join(save_dir, file_name)
# 保存简化的结果
with open(save_path, "w", encoding="utf-8") as f:
json.dump(simplified_result, f, ensure_ascii=False, indent=4)
print(f"\n结果已保存到 {save_path}")
# 同时保存完整结果到results目录
os.makedirs("results", exist_ok=True)
with open("results/personality_result.json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,146 @@
from dataclasses import dataclass
from typing import Dict, List
import json
from pathlib import Path
import random
@dataclass
class Personality:
"""人格特质类"""
openness: float # 开放性
conscientiousness: float # 尽责性
extraversion: float # 外向性
agreeableness: float # 宜人性
neuroticism: float # 神经质
bot_nickname: str # 机器人昵称
personality_core: str # 人格核心特点
personality_sides: List[str] # 人格侧面描述
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, personality_core: str = "", personality_sides: List[str] = None):
if personality_sides is None:
personality_sides = []
self.personality_core = personality_core
self.personality_sides = personality_sides
@classmethod
def get_instance(cls) -> "Personality":
"""获取Personality单例实例
Returns:
Personality: 单例实例
"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def _init_big_five_personality(self):
"""初始化大五人格特质"""
# 构建文件路径
personality_file = Path("data/personality") / f"{self.bot_nickname}_personality.per"
# 如果文件存在,读取文件
if personality_file.exists():
with open(personality_file, "r", encoding="utf-8") as f:
personality_data = json.load(f)
self.openness = personality_data.get("openness", 0.5)
self.conscientiousness = personality_data.get("conscientiousness", 0.5)
self.extraversion = personality_data.get("extraversion", 0.5)
self.agreeableness = personality_data.get("agreeableness", 0.5)
self.neuroticism = personality_data.get("neuroticism", 0.5)
else:
# 如果文件不存在根据personality_core和personality_core来设置大五人格特质
if "活泼" in self.personality_core or "开朗" in self.personality_sides:
self.extraversion = 0.8
self.neuroticism = 0.2
else:
self.extraversion = 0.3
self.neuroticism = 0.5
if "认真" in self.personality_core or "负责" in self.personality_sides:
self.conscientiousness = 0.9
else:
self.conscientiousness = 0.5
if "友善" in self.personality_core or "温柔" in self.personality_sides:
self.agreeableness = 0.9
else:
self.agreeableness = 0.5
if "创新" in self.personality_core or "开放" in self.personality_sides:
self.openness = 0.8
else:
self.openness = 0.5
@classmethod
def initialize(cls, bot_nickname: str, personality_core: str, personality_sides: List[str]) -> "Personality":
"""初始化人格特质
Args:
bot_nickname: 机器人昵称
personality_core: 人格核心特点
personality_sides: 人格侧面描述
Returns:
Personality: 初始化后的人格特质实例
"""
instance = cls.get_instance()
instance.bot_nickname = bot_nickname
instance.personality_core = personality_core
instance.personality_sides = personality_sides
instance._init_big_five_personality()
return instance
def to_dict(self) -> Dict:
"""将人格特质转换为字典格式"""
return {
"openness": self.openness,
"conscientiousness": self.conscientiousness,
"extraversion": self.extraversion,
"agreeableness": self.agreeableness,
"neuroticism": self.neuroticism,
"bot_nickname": self.bot_nickname,
"personality_core": self.personality_core,
"personality_sides": self.personality_sides,
}
@classmethod
def from_dict(cls, data: Dict) -> "Personality":
"""从字典创建人格特质实例"""
instance = cls.get_instance()
for key, value in data.items():
setattr(instance, key, value)
return instance
def get_prompt(self, x_person, level):
# 开始构建prompt
if x_person == 2:
prompt_personality = ""
elif x_person == 1:
prompt_personality = ""
else:
prompt_personality = ""
# person
prompt_personality += self.personality_core
if level == 2:
personality_sides = self.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
elif level == 3:
personality_sides = self.personality_sides
for side in personality_sides:
prompt_personality += f",{side}"
prompt_personality += ""
return prompt_personality

View File

@@ -0,0 +1,43 @@
import json
from typing import Dict
import os
def load_scenes() -> Dict:
"""
从JSON文件加载场景数据
Returns:
Dict: 包含所有场景的字典
"""
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "template_scene.json")
with open(json_path, "r", encoding="utf-8") as f:
return json.load(f)
PERSONALITY_SCENES = load_scenes()
def get_scene_by_factor(factor: str) -> Dict:
"""
根据人格因子获取对应的情景测试
Args:
factor (str): 人格因子名称
Returns:
Dict: 包含情景描述的字典
"""
return PERSONALITY_SCENES.get(factor, None)
def get_all_scenes() -> Dict:
"""
获取所有情景测试
Returns:
Dict: 所有情景测试的字典
"""
return PERSONALITY_SCENES

View File

@@ -0,0 +1,112 @@
{
"外向性": {
"场景1": {
"scenario": "你刚刚搬到一个新的城市工作。今天是你入职的第一天,在公司的电梯里,一位同事微笑着和你打招呼:\n\n同事「嗨你是新来的同事吧我是市场部的小林。」\n\n同事看起来很友善还主动介绍说「待会午饭时间我们部门有几个人准备一起去楼下新开的餐厅你要一起来吗可以认识一下其他同事。」",
"explanation": "这个场景通过职场社交情境,观察个体对于新环境、新社交圈的态度和反应倾向。"
},
"场景2": {
"scenario": "在大学班级群里,班长发起了一个组织班级联谊活动的投票:\n\n班长「大家好下周末我们准备举办一次班级联谊活动地点在学校附近的KTV。想请大家报名参加也欢迎大家邀请其他班级的同学」\n\n已经有几个同学在群里积极响应有人@你问你要不要一起参加。",
"explanation": "通过班级活动场景,观察个体对群体社交活动的参与意愿。"
},
"场景3": {
"scenario": "你在社交平台上发布了一条动态,收到了很多陌生网友的评论和私信:\n\n网友A「你说的这个观点很有意思想和你多交流一下。」\n\n网友B「我也对这个话题很感兴趣要不要建个群一起讨论」",
"explanation": "通过网络社交场景,观察个体对线上社交的态度。"
},
"场景4": {
"scenario": "你暗恋的对象今天主动来找你:\n\n对方「那个...我最近在准备一个演讲比赛,听说你口才很好。能不能请你帮我看看演讲稿,顺便给我一些建议?如果你有时间的话,可以一起吃个饭聊聊。」",
"explanation": "通过恋爱情境,观察个体在面对心仪对象时的社交表现。"
},
"场景5": {
"scenario": "在一次线下读书会上,主持人突然点名让你分享读后感:\n\n主持人「听说你对这本书很有见解能不能和大家分享一下你的想法」\n\n现场有二十多个陌生的读书爱好者都期待地看着你。",
"explanation": "通过即兴发言场景,观察个体的社交表现欲和公众表达能力。"
}
},
"神经质": {
"场景1": {
"scenario": "你正在准备一个重要的项目演示这关系到你的晋升机会。就在演示前30分钟你收到了主管发来的消息\n\n主管「临时有个变动CEO也会来听你的演示。他对这个项目特别感兴趣。」\n\n正当你准备回复时主管又发来一条「对了能不能把演示时间压缩到15分钟CEO下午还有其他安排。你之前准备的是30分钟的版本对吧」",
"explanation": "这个场景通过突发的压力情境,观察个体在面对计划外变化时的情绪反应和调节能力。"
},
"场景2": {
"scenario": "期末考试前一天晚上,你收到了好朋友发来的消息:\n\n好朋友「不好意思这么晚打扰你...我看你平时成绩很好,能不能帮我解答几个问题?我真的很担心明天的考试。」\n\n你看了看时间已经是晚上11点而你原本计划的复习还没完成。",
"explanation": "通过考试压力场景,观察个体在时间紧张时的情绪管理。"
},
"场景3": {
"scenario": "你在社交媒体上发表的一个观点引发了争议,有不少人开始批评你:\n\n网友A「这种观点也好意思说出来真是无知。」\n\n网友B「建议楼主先去补补课再来发言。」\n\n评论区里的负面评论越来越多还有人开始人身攻击。",
"explanation": "通过网络争议场景,观察个体面对批评时的心理承受能力。"
},
"场景4": {
"scenario": "你和恋人约好今天一起看电影,但在约定时间前半小时,对方发来消息:\n\n恋人「对不起我临时有点事可能要迟到一会儿。」\n\n二十分钟后对方又发来消息「可能要再等等抱歉」\n\n电影快要开始了但对方还是没有出现。",
"explanation": "通过恋爱情境,观察个体对不确定性的忍耐程度。"
},
"场景5": {
"scenario": "在一次重要的小组展示中,你的组员在演示途中突然卡壳了:\n\n组员小声对你说「我忘词了接下来的部分是什么来着...」\n\n台下的老师和同学都在等待气氛有些尴尬。",
"explanation": "通过公开场合的突发状况,观察个体的应急反应和压力处理能力。"
}
},
"严谨性": {
"场景1": {
"scenario": "你是团队的项目负责人,刚刚接手了一个为期两个月的重要项目。在第一次团队会议上:\n\n小王「老大我觉得两个月时间很充裕我们先做着看吧遇到问题再解决。」\n\n小张「要不要先列个时间表不过感觉太详细的计划也没必要点到为止就行。」\n\n小李「客户那边说如果能提前完成有奖励我觉得我们可以先做快一点的部分。」",
"explanation": "这个场景通过项目管理情境,体现个体在工作方法、计划性和责任心方面的特征。"
},
"场景2": {
"scenario": "期末小组作业,组长让大家分工完成一份研究报告。在截止日期前三天:\n\n组员A「我的部分大概写完了感觉还行。」\n\n组员B「我这边可能还要一天才能完成最近太忙了。」\n\n组员C发来一份没有任何引用出处、可能存在抄袭的内容「我写完了你们看看怎么样」",
"explanation": "通过学习场景,观察个体对学术规范和质量要求的重视程度。"
},
"场景3": {
"scenario": "你在一个兴趣小组的群聊中,大家正在讨论举办一次线下活动:\n\n成员A「到时候见面就知道具体怎么玩了」\n\n成员B「对啊随意一点挺好的。」\n\n成员C「人来了自然就热闹了。」",
"explanation": "通过活动组织场景,观察个体对活动计划的态度。"
},
"场景4": {
"scenario": "你的好友小明邀请你一起参加一个重要的演出活动,他说:\n\n小明「到时候我们就即兴发挥吧不用排练了我相信我们的默契。」\n\n距离演出还有三天但节目内容、配乐和服装都还没有确定。",
"explanation": "通过演出准备场景,观察个体的计划性和对不确定性的接受程度。"
},
"场景5": {
"scenario": "在一个重要的团队项目中,你发现一个同事的工作存在明显错误:\n\n同事「差不多就行了反正领导也看不出来。」\n\n这个错误可能不会立即造成问题但长期来看可能会影响项目质量。",
"explanation": "通过工作质量场景,观察个体对细节和标准的坚持程度。"
}
},
"开放性": {
"场景1": {
"scenario": "周末下午,你的好友小美兴致勃勃地给你打电话:\n\n小美「我刚发现一个特别有意思的沉浸式艺术展不是传统那种挂画的展览而是把整个空间都变成了艺术品。观众要穿特制的服装还要带上VR眼镜好像还有AI实时互动」\n\n小美继续说「虽然票价不便宜但听说体验很独特。网上评价两极分化有人说是前所未有的艺术革新也有人说是哗众取宠。要不要周末一起去体验一下」",
"explanation": "这个场景通过新型艺术体验,反映个体对创新事物的接受程度和尝试意愿。"
},
"场景2": {
"scenario": "在一节创意写作课上,老师提出了一个特别的作业:\n\n老师「下周的作业是用AI写作工具协助创作一篇小说。你们可以自由探索如何与AI合作打破传统写作方式。」\n\n班上随即展开了激烈讨论有人认为这是对创作的亵渎也有人对这种新形式感到兴奋。",
"explanation": "通过新技术应用场景,观察个体对创新学习方式的态度。"
},
"场景3": {
"scenario": "在社交媒体上,你看到一个朋友分享了一种新的学习方式:\n\n「最近我在尝试'沉浸式学习',就是完全投入到一个全新的领域。比如学习一门陌生的语言,或者尝试完全不同的职业技能。虽然过程会很辛苦,但这种打破舒适圈的感觉真的很棒!」\n\n评论区里争论不断有人认为这种学习方式效率高也有人觉得太激进。",
"explanation": "通过新型学习方式,观察个体对创新和挑战的态度。"
},
"场景4": {
"scenario": "你的朋友向你推荐了一种新的饮食方式:\n\n朋友「我最近在尝试'未来食品'比如人造肉、3D打印食物、昆虫蛋白等。这不仅对环境友好营养也很均衡。要不要一起来尝试看看」\n\n这个提议让你感到好奇又犹豫你之前从未尝试过这些新型食物。",
"explanation": "通过饮食创新场景,观察个体对新事物的接受度和尝试精神。"
},
"场景5": {
"scenario": "在一次朋友聚会上,大家正在讨论未来职业规划:\n\n朋友A「我准备辞职去做自媒体专门介绍一些小众的文化和艺术。」\n\n朋友B「我想去学习生物科技准备转行做人造肉研发。」\n\n朋友C「我在考虑加入一个区块链创业项目虽然风险很大。」",
"explanation": "通过职业选择场景,观察个体对新兴领域的探索意愿。"
}
},
"宜人性": {
"场景1": {
"scenario": "在回家的公交车上,你遇到这样一幕:\n\n一位老奶奶颤颤巍巍地上了车车上座位已经坐满了。她站在你旁边看起来很疲惫。这时你听到前排两个年轻人的对话\n\n年轻人A「那个老太太好像站不稳看起来挺累的。」\n\n年轻人B「现在的老年人真是...我看她包里还有菜,肯定是去菜市场买完菜回来的,这么多人都不知道叫子女开车接送。」\n\n就在这时老奶奶一个趔趄差点摔倒。她扶住了扶手但包里的东西洒了一些出来。",
"explanation": "这个场景通过公共场合的助人情境,体现个体的同理心和对他人需求的关注程度。"
},
"场景2": {
"scenario": "在班级群里,有同学发起为生病住院的同学捐款:\n\n同学A「大家好小林最近得了重病住院医药费很贵家里负担很重。我们要不要一起帮帮他」\n\n同学B「我觉得这是他家里的事我们不方便参与吧。」\n\n同学C「但是都是同学一场帮帮忙也是应该的。」",
"explanation": "通过同学互助场景,观察个体的助人意愿和同理心。"
},
"场景3": {
"scenario": "在一个网络讨论组里,有人发布了求助信息:\n\n求助者「最近心情很低落感觉生活很压抑不知道该怎么办...」\n\n评论区里已经有一些回复\n「生活本来就是这样想开点」\n「你这样子太消极了要积极面对。」\n「谁还没点烦心事啊过段时间就好了。」",
"explanation": "通过网络互助场景,观察个体的共情能力和安慰方式。"
},
"场景4": {
"scenario": "你的朋友向你倾诉工作压力:\n\n朋友「最近工作真的好累感觉快坚持不下去了...」\n\n但今天你也遇到了很多烦心事心情也不太好。",
"explanation": "通过感情关系场景,观察个体在自身状态不佳时的关怀能力。"
},
"场景5": {
"scenario": "在一次团队项目中,新来的同事小王因为经验不足,造成了一个严重的错误。在部门会议上:\n\n主管「这个错误造成了很大的损失是谁负责的这部分」\n\n小王看起来很紧张欲言又止。你知道是他造成的错误同时你也是这个项目的共同负责人。",
"explanation": "通过职场情境,观察个体在面对他人过错时的态度和处理方式。"
}
}
}

View File

@@ -15,6 +15,7 @@ from .plugins.config.config import global_config
from .plugins.chat.bot import chat_bot
from .common.logger import get_module_logger
from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality
logger = get_module_logger("main")
@@ -26,6 +27,7 @@ class MainSystem:
self.mood_manager = MoodManager.get_instance()
self.hippocampus_manager = HippocampusManager.get_instance()
self._message_manager_started = False
self.individuality = Individuality.get_instance()
# 使用消息API替代直接的FastAPI实例
from .plugins.message import global_api
@@ -79,7 +81,7 @@ class MainSystem:
# 初始化日程
bot_schedule.initialize(
name=global_config.BOT_NICKNAME,
personality=global_config.PROMPT_PERSONALITY,
personality=global_config.personality_core,
behavior=global_config.PROMPT_SCHEDULE_GEN,
interval=global_config.SCHEDULE_DOING_UPDATE_INTERVAL,
)
@@ -88,6 +90,20 @@ class MainSystem:
# 启动FastAPI服务器
self.app.register_message_handler(chat_bot.message_process)
# 初始化个体特征
self.individuality.initialize(
bot_nickname=global_config.BOT_NICKNAME,
personality_core=global_config.personality_core,
personality_sides=global_config.personality_sides,
identity_detail=global_config.identity_detail,
height=global_config.height,
weight=global_config.weight,
age=global_config.age,
gender=global_config.gender,
appearance=global_config.appearance,
)
logger.success("个体特征初始化成功")
try:
# 启动心流系统
asyncio.create_task(heartflow.heartflow_start_working())
@@ -116,17 +132,17 @@ class MainSystem:
async def build_memory_task(self):
"""记忆构建任务"""
while True:
await asyncio.sleep(global_config.build_memory_interval)
logger.info("正在进行记忆构建")
await HippocampusManager.get_instance().build_memory()
await asyncio.sleep(global_config.build_memory_interval)
async def forget_memory_task(self):
"""记忆遗忘任务"""
while True:
await asyncio.sleep(global_config.forget_memory_interval)
print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...")
await HippocampusManager.get_instance().forget_memory(percentage=global_config.memory_forget_percentage)
print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成")
await asyncio.sleep(global_config.forget_memory_interval)
async def print_mood_task(self):
"""打印情绪状态"""

View File

@@ -0,0 +1,139 @@
from typing import Tuple
from src.common.logger import get_module_logger
from ..models.utils_model import LLM_request
from ..config.config import global_config
from .chat_observer import ChatObserver
from .pfc_utils import get_items_from_json
from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
logger = get_module_logger("action_planner")
class ActionPlannerInfo:
def __init__(self):
self.done_action = []
self.goal_list = []
self.knowledge_list = []
self.memory_list = []
class ActionPlanner:
"""行动规划器"""
def __init__(self, stream_id: str):
self.llm = LLM_request(
model=global_config.llm_normal,
temperature=0.7,
max_tokens=1000,
request_type="action_planning"
)
self.personality_info = Individuality.get_instance().get_prompt(type = "personality", x_person = 2, level = 2)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
async def plan(
self,
observation_info: ObservationInfo,
conversation_info: ConversationInfo
) -> Tuple[str, str]:
"""规划下一步行动
Args:
observation_info: 决策信息
conversation_info: 对话信息
Returns:
Tuple[str, str]: (行动类型, 行动原因)
"""
# 构建提示词
logger.debug(f"开始规划行动:当前目标: {conversation_info.goal_list}")
#构建对话目标
if conversation_info.goal_list:
goal, reasoning = conversation_info.goal_list[-1]
else:
goal = "目前没有明确对话目标"
reasoning = "目前没有明确对话目标,最好思考一个对话目标"
# 获取聊天历史记录
chat_history_list = observation_info.chat_history
chat_history_text = ""
for msg in chat_history_list:
chat_history_text += f"{msg}\n"
if observation_info.new_messages_count > 0:
new_messages_list = observation_info.unprocessed_messages
chat_history_text += f"{observation_info.new_messages_count}条新消息:\n"
for msg in new_messages_list:
chat_history_text += f"{msg}\n"
observation_info.clear_unprocessed_messages()
personality_text = f"你的名字是{self.name}{self.personality_info}"
# 构建action历史文本
action_history_list = conversation_info.done_action
action_history_text = "你之前做的事情是:"
for action in action_history_list:
action_history_text += f"{action}\n"
prompt = f"""{personality_text}。现在你在参与一场QQ聊天请分析以下内容根据信息决定下一步行动
当前对话目标:{goal}
产生该对话目标的原因:{reasoning}
{action_history_text}
最近的对话记录:
{chat_history_text}
请你接下去想想要你要做什么,可以发言,可以等待,可以倾听,可以调取知识。注意不同行动类型的要求,不要重复发言:
行动类型:
fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择
wait: 当你做出了发言,对方尚未回复时等待对方的回复
listening: 倾听对方发言,当你认为对方发言尚未结束时采用
direct_reply: 不符合上述情况,回复对方,注意不要过多或者重复发言
rethink_goal: 重新思考对话目标,当发现对话目标不合适时选择,会重新思考对话目标
请以JSON格式输出包含以下字段
1. action: 行动类型,注意你之前的行为
2. reason: 选择该行动的原因,注意你之前的行为(简要解释)
注意请严格按照JSON格式输出不要包含任何其他内容。"""
logger.debug(f"发送到LLM的提示词: {prompt}")
try:
content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"LLM原始返回内容: {content}")
# 使用简化函数提取JSON内容
success, result = get_items_from_json(
content,
"action", "reason",
default_values={"action": "direct_reply", "reason": "没有明确原因"}
)
if not success:
return "direct_reply", "JSON解析失败选择直接回复"
action = result["action"]
reason = result["reason"]
# 验证action类型
if action not in ["direct_reply", "fetch_knowledge", "wait", "listening", "rethink_goal"]:
logger.warning(f"未知的行动类型: {action}默认使用listening")
action = "listening"
logger.info(f"规划的行动: {action}")
logger.info(f"行动原因: {reason}")
return action, reason
except Exception as e:
logger.error(f"规划行动时出错: {str(e)}")
return "direct_reply", "发生错误,选择直接回复"

View File

@@ -1,89 +1,164 @@
import time
import asyncio
from typing import Optional, Dict, Any, List, Tuple
from typing import Optional, Dict, Any, List, Tuple
from src.common.logger import get_module_logger
from src.common.database import db
from ..message.message_base import UserInfo
from ..config.config import global_config
from .chat_states import NotificationManager, create_new_message_notification, create_cold_chat_notification
from .message_storage import MessageStorage, MongoDBMessageStorage
logger = get_module_logger("chat_observer")
class ChatObserver:
"""聊天状态观察器"""
# 类级别的实例管理
_instances: Dict[str, 'ChatObserver'] = {}
_instances: Dict[str, "ChatObserver"] = {}
@classmethod
def get_instance(cls, stream_id: str) -> 'ChatObserver':
def get_instance(cls, stream_id: str, message_storage: Optional[MessageStorage] = None) -> 'ChatObserver':
"""获取或创建观察器实例
Args:
stream_id: 聊天流ID
message_storage: 消息存储实现如果为None则使用MongoDB实现
Returns:
ChatObserver: 观察器实例
"""
if stream_id not in cls._instances:
cls._instances[stream_id] = cls(stream_id)
cls._instances[stream_id] = cls(stream_id, message_storage)
return cls._instances[stream_id]
def __init__(self, stream_id: str):
def __init__(self, stream_id: str, message_storage: Optional[MessageStorage] = None):
"""初始化观察器
Args:
stream_id: 聊天流ID
message_storage: 消息存储实现如果为None则使用MongoDB实现
"""
if stream_id in self._instances:
raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.")
self.stream_id = stream_id
self.message_storage = message_storage or MongoDBMessageStorage()
self.last_user_speak_time: Optional[float] = None # 对方上次发言时间
self.last_bot_speak_time: Optional[float] = None # 机器人上次发言时间
self.last_check_time: float = time.time() # 上次查看聊天记录时间
self.last_message_read: Optional[str] = None # 最后读取的消息ID
self.last_message_time: Optional[float] = None # 最后一条消息的时间戳
self.waiting_start_time: Optional[float] = None # 等待开始时间
self.waiting_start_time: float = time.time() # 等待开始时间,初始化为当前时间
# 消息历史记录
self.message_history: List[Dict[str, Any]] = [] # 所有消息历史
self.last_message_id: Optional[str] = None # 最后一条消息的ID
self.message_count: int = 0 # 消息计数
self.last_message_id: Optional[str] = None # 最后一条消息的ID
self.message_count: int = 0 # 消息计数
# 运行状态
self._running: bool = False
self._task: Optional[asyncio.Task] = None
self._update_event = asyncio.Event() # 触发更新的事件
self._update_complete = asyncio.Event() # 更新完成的事件
def check(self) -> bool:
"""检查距离上一次观察之后是否有了新消息
# 通知管理器
self.notification_manager = NotificationManager()
# 冷场检查配置
self.cold_chat_threshold: float = 60.0 # 60秒无消息判定为冷场
self.last_cold_chat_check: float = time.time()
self.is_cold_chat_state: bool = False
self.update_event = asyncio.Event()
self.update_interval = 5 # 更新间隔(秒)
self.message_cache = []
self.update_running = False
async def check(self) -> bool:
"""检查距离上一次观察之后是否有了新消息
Returns:
bool: 是否有新消息
"""
logger.debug(f"检查距离上一次观察之后是否有了新消息: {self.last_check_time}")
query = {
"chat_id": self.stream_id,
"time": {"$gt": self.last_check_time}
}
# 只需要查询是否存在,不需要获取具体消息
new_message_exists = db.messages.find_one(query) is not None
new_message_exists = await self.message_storage.has_new_messages(
self.stream_id,
self.last_check_time
)
if new_message_exists:
logger.debug("发现新消息")
self.last_check_time = time.time()
return new_message_exists
def get_new_message(self) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
async def _add_message_to_history(self, message: Dict[str, Any]):
"""添加消息到历史记录并发送通知
Args:
message: 消息数据
"""
self.message_history.append(message)
self.last_message_id = message["message_id"]
self.last_message_time = message["time"] # 更新最后消息时间
self.message_count += 1
# 更新说话时间
user_info = UserInfo.from_dict(message.get("user_info", {}))
if user_info.user_id == global_config.BOT_QQ:
self.last_bot_speak_time = message["time"]
else:
self.last_user_speak_time = message["time"]
# 发送新消息通知
notification = create_new_message_notification(
sender="chat_observer",
target="pfc",
message=message
)
await self.notification_manager.send_notification(notification)
# 检查并更新冷场状态
await self._check_cold_chat()
async def _check_cold_chat(self):
"""检查是否处于冷场状态并发送通知"""
current_time = time.time()
# 每10秒检查一次冷场状态
if current_time - self.last_cold_chat_check < 10:
return
self.last_cold_chat_check = current_time
# 判断是否冷场
is_cold = False
if self.last_message_time is None:
is_cold = True
else:
is_cold = (current_time - self.last_message_time) > self.cold_chat_threshold
# 如果冷场状态发生变化,发送通知
if is_cold != self.is_cold_chat_state:
self.is_cold_chat_state = is_cold
notification = create_cold_chat_notification(
sender="chat_observer",
target="pfc",
is_cold=is_cold
)
await self.notification_manager.send_notification(notification)
async def get_new_message(self) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""获取上一次观察的时间点后的新消息,插入到历史记录中,并返回新消息和历史记录两个对象"""
messages = self.get_message_history(self.last_check_time)
messages = await self.message_storage.get_messages_after(
self.stream_id,
self.last_message_read
)
for message in messages:
self._add_message_to_history(message)
await self._add_message_to_history(message)
return messages, self.message_history
def new_message_after(self, time_point: float) -> bool:
@@ -95,122 +170,100 @@ class ChatObserver:
Returns:
bool: 是否有新消息
"""
logger.debug(f"判断是否在指定时间点后有新消息: {self.last_message_time} > {time_point}")
return self.last_message_time is None or self.last_message_time > time_point
def _add_message_to_history(self, message: Dict[str, Any]):
"""添加消息到历史记录
Args:
message: 消息数据
"""
self.message_history.append(message)
self.last_message_id = message["message_id"]
self.last_message_time = message["time"] # 更新最后消息时间
self.message_count += 1
# 更新说话时间
user_info = UserInfo.from_dict(message.get("user_info", {}))
if user_info.user_id == global_config.BOT_QQ:
self.last_bot_speak_time = message["time"]
else:
self.last_user_speak_time = message["time"]
if time_point is None:
logger.warning("time_point 为 None返回 False")
return False
if self.last_message_time is None:
logger.debug("没有最后消息时间,返回 False")
return False
has_new = self.last_message_time > time_point
logger.debug(f"判断是否在指定时间点后有新消息: {self.last_message_time} > {time_point} = {has_new}")
return has_new
def get_message_history(
self,
start_time: Optional[float] = None,
end_time: Optional[float] = None,
limit: Optional[int] = None,
user_id: Optional[str] = None
user_id: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""获取消息历史
Args:
start_time: 开始时间戳
end_time: 结束时间戳
limit: 限制返回消息数量
user_id: 指定用户ID
Returns:
List[Dict[str, Any]]: 消息列表
"""
filtered_messages = self.message_history
if start_time is not None:
filtered_messages = [m for m in filtered_messages if m["time"] >= start_time]
if end_time is not None:
filtered_messages = [m for m in filtered_messages if m["time"] <= end_time]
if user_id is not None:
filtered_messages = [
m for m in filtered_messages
if UserInfo.from_dict(m.get("user_info", {})).user_id == user_id
m for m in filtered_messages if UserInfo.from_dict(m.get("user_info", {})).user_id == user_id
]
if limit is not None:
filtered_messages = filtered_messages[-limit:]
return filtered_messages
async def _fetch_new_messages(self) -> List[Dict[str, Any]]:
"""获取新消息
Returns:
List[Dict[str, Any]]: 新消息列表
"""
query = {"chat_id": self.stream_id}
if self.last_message_read:
# 获取ID大于last_message_read的消息
last_message = db.messages.find_one({"message_id": self.last_message_read})
if last_message:
query["time"] = {"$gt": last_message["time"]}
new_messages = list(
db.messages.find(query).sort("time", 1)
new_messages = await self.message_storage.get_messages_after(
self.stream_id,
self.last_message_read
)
if new_messages:
self.last_message_read = new_messages[-1]["message_id"]
return new_messages
async def _fetch_new_messages_before(self, time_point: float) -> List[Dict[str, Any]]:
"""获取指定时间点之前的消息
Args:
time_point: 时间戳
Returns:
List[Dict[str, Any]]: 最多5条消息
"""
query = {
"chat_id": self.stream_id,
"time": {"$lt": time_point}
}
new_messages = list(
db.messages.find(query).sort("time", -1).limit(5) # 倒序获取5条
new_messages = await self.message_storage.get_messages_before(
self.stream_id,
time_point
)
# 将消息按时间正序排列
new_messages.reverse()
if new_messages:
self.last_message_read = new_messages[-1]["message_id"]
return new_messages
'''主要观察循环'''
async def _update_loop(self):
"""更新循环"""
try:
start_time = time.time()
messages = await self._fetch_new_messages_before(start_time)
for message in messages:
self._add_message_to_history(message)
await self._add_message_to_history(message)
except Exception as e:
logger.error(f"缓冲消息出错: {e}")
while self._running:
try:
# 等待事件或超时1秒
@@ -218,35 +271,35 @@ class ChatObserver:
await asyncio.wait_for(self._update_event.wait(), timeout=1)
except asyncio.TimeoutError:
pass # 超时后也执行一次检查
self._update_event.clear() # 重置触发事件
self._update_complete.clear() # 重置完成事件
# 获取新消息
new_messages = await self._fetch_new_messages()
if new_messages:
# 处理新消息
for message in new_messages:
self._add_message_to_history(message)
await self._add_message_to_history(message)
# 设置完成事件
self._update_complete.set()
except Exception as e:
logger.error(f"更新循环出错: {e}")
self._update_complete.set() # 即使出错也要设置完成事件
def trigger_update(self):
"""触发一次立即更新"""
self._update_event.set()
async def wait_for_update(self, timeout: float = 5.0) -> bool:
"""等待更新完成
Args:
timeout: 超时时间(秒)
Returns:
bool: 是否成功完成更新False表示超时
"""
@@ -256,16 +309,16 @@ class ChatObserver:
except asyncio.TimeoutError:
logger.warning(f"等待更新完成超时({timeout}秒)")
return False
def start(self):
"""启动观察器"""
if self._running:
return
self._running = True
self._task = asyncio.create_task(self._update_loop())
logger.info(f"ChatObserver for {self.stream_id} started")
def stop(self):
"""停止观察器"""
self._running = False
@@ -274,15 +327,15 @@ class ChatObserver:
if self._task:
self._task.cancel()
logger.info(f"ChatObserver for {self.stream_id} stopped")
async def process_chat_history(self, messages: list):
"""处理聊天历史
Args:
messages: 消息列表
"""
self.update_check_time()
for msg in messages:
try:
user_info = UserInfo.from_dict(msg.get("user_info", {}))
@@ -292,31 +345,99 @@ class ChatObserver:
self.update_user_speak_time(msg["time"])
except Exception as e:
logger.warning(f"处理消息时间时出错: {e}")
continue
continue
def update_check_time(self):
"""更新查看时间"""
self.last_check_time = time.time()
def update_bot_speak_time(self, speak_time: Optional[float] = None):
"""更新机器人说话时间"""
self.last_bot_speak_time = speak_time or time.time()
def update_user_speak_time(self, speak_time: Optional[float] = None):
"""更新用户说话时间"""
self.last_user_speak_time = speak_time or time.time()
def get_time_info(self) -> str:
"""获取时间信息文本"""
current_time = time.time()
time_info = ""
if self.last_bot_speak_time:
bot_speak_ago = current_time - self.last_bot_speak_time
time_info += f"\n距离你上次发言已经过去了{int(bot_speak_ago)}"
if self.last_user_speak_time:
user_speak_ago = current_time - self.last_user_speak_time
time_info += f"\n距离对方上次发言已经过去了{int(user_speak_ago)}"
return time_info
def start_periodic_update(self):
"""启动观察器的定期更新"""
if not self.update_running:
self.update_running = True
asyncio.create_task(self._periodic_update())
async def _periodic_update(self):
"""定期更新消息历史"""
try:
while self.update_running:
await self._update_message_history()
await asyncio.sleep(self.update_interval)
except Exception as e:
logger.error(f"定期更新消息历史时出错: {str(e)}")
async def _update_message_history(self) -> bool:
"""更新消息历史
Returns:
bool: 是否有新消息
"""
try:
messages = await self.message_storage.get_messages_for_stream(
self.stream_id,
limit=50
)
if not messages:
return False
# 检查是否有新消息
has_new_messages = False
if messages and (not self.message_cache or messages[0]["message_id"] != self.message_cache[0]["message_id"]):
has_new_messages = True
self.message_cache = messages
if has_new_messages:
self.update_event.set()
self.update_event.clear()
return True
return False
except Exception as e:
logger.error(f"更新消息历史时出错: {str(e)}")
return False
def get_cached_messages(self, limit: int = 50) -> List[Dict[str, Any]]:
"""获取缓存的消息历史
Args:
limit: 获取的最大消息数量默认50
Returns:
List[Dict[str, Any]]: 缓存的消息历史列表
"""
return self.message_cache[:limit]
def get_last_message(self) -> Optional[Dict[str, Any]]:
"""获取最后一条消息
Returns:
Optional[Dict[str, Any]]: 最后一条消息如果没有则返回None
"""
if not self.message_cache:
return None
return self.message_cache[0]

View File

@@ -0,0 +1,267 @@
from enum import Enum, auto
from typing import Optional, Dict, Any, List, Set
from dataclasses import dataclass
from datetime import datetime
from abc import ABC, abstractmethod
class ChatState(Enum):
"""聊天状态枚举"""
NORMAL = auto() # 正常状态
NEW_MESSAGE = auto() # 有新消息
COLD_CHAT = auto() # 冷场状态
ACTIVE_CHAT = auto() # 活跃状态
BOT_SPEAKING = auto() # 机器人正在说话
USER_SPEAKING = auto() # 用户正在说话
SILENT = auto() # 沉默状态
ERROR = auto() # 错误状态
class NotificationType(Enum):
"""通知类型枚举"""
NEW_MESSAGE = auto() # 新消息通知
COLD_CHAT = auto() # 冷场通知
ACTIVE_CHAT = auto() # 活跃通知
BOT_SPEAKING = auto() # 机器人说话通知
USER_SPEAKING = auto() # 用户说话通知
MESSAGE_DELETED = auto() # 消息删除通知
USER_JOINED = auto() # 用户加入通知
USER_LEFT = auto() # 用户离开通知
ERROR = auto() # 错误通知
@dataclass
class ChatStateInfo:
"""聊天状态信息"""
state: ChatState
last_message_time: Optional[float] = None
last_message_content: Optional[str] = None
last_speaker: Optional[str] = None
message_count: int = 0
cold_duration: float = 0.0 # 冷场持续时间(秒)
active_duration: float = 0.0 # 活跃持续时间(秒)
@dataclass
class Notification:
"""通知基类"""
type: NotificationType
timestamp: float
sender: str # 发送者标识
target: str # 接收者标识
data: Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"type": self.type.name,
"timestamp": self.timestamp,
"data": self.data
}
@dataclass
class StateNotification(Notification):
"""持续状态通知"""
is_active: bool = True
def to_dict(self) -> Dict[str, Any]:
base_dict = super().to_dict()
base_dict["is_active"] = self.is_active
return base_dict
class NotificationHandler(ABC):
"""通知处理器接口"""
@abstractmethod
async def handle_notification(self, notification: Notification):
"""处理通知"""
pass
class NotificationManager:
"""通知管理器"""
def __init__(self):
# 按接收者和通知类型存储处理器
self._handlers: Dict[str, Dict[NotificationType, List[NotificationHandler]]] = {}
self._active_states: Set[NotificationType] = set()
self._notification_history: List[Notification] = []
def register_handler(self, target: str, notification_type: NotificationType, handler: NotificationHandler):
"""注册通知处理器
Args:
target: 接收者标识(例如:"pfc"
notification_type: 要处理的通知类型
handler: 处理器实例
"""
if target not in self._handlers:
self._handlers[target] = {}
if notification_type not in self._handlers[target]:
self._handlers[target][notification_type] = []
self._handlers[target][notification_type].append(handler)
def unregister_handler(self, target: str, notification_type: NotificationType, handler: NotificationHandler):
"""注销通知处理器
Args:
target: 接收者标识
notification_type: 通知类型
handler: 要注销的处理器实例
"""
if target in self._handlers and notification_type in self._handlers[target]:
handlers = self._handlers[target][notification_type]
if handler in handlers:
handlers.remove(handler)
# 如果该类型的处理器列表为空,删除该类型
if not handlers:
del self._handlers[target][notification_type]
# 如果该目标没有任何处理器,删除该目标
if not self._handlers[target]:
del self._handlers[target]
async def send_notification(self, notification: Notification):
"""发送通知"""
self._notification_history.append(notification)
# 如果是状态通知,更新活跃状态
if isinstance(notification, StateNotification):
if notification.is_active:
self._active_states.add(notification.type)
else:
self._active_states.discard(notification.type)
# 调用目标接收者的处理器
target = notification.target
if target in self._handlers:
handlers = self._handlers[target].get(notification.type, [])
for handler in handlers:
await handler.handle_notification(notification)
def get_active_states(self) -> Set[NotificationType]:
"""获取当前活跃的状态"""
return self._active_states.copy()
def is_state_active(self, state_type: NotificationType) -> bool:
"""检查特定状态是否活跃"""
return state_type in self._active_states
def get_notification_history(self,
sender: Optional[str] = None,
target: Optional[str] = None,
limit: Optional[int] = None) -> List[Notification]:
"""获取通知历史
Args:
sender: 过滤特定发送者的通知
target: 过滤特定接收者的通知
limit: 限制返回数量
"""
history = self._notification_history
if sender:
history = [n for n in history if n.sender == sender]
if target:
history = [n for n in history if n.target == target]
if limit is not None:
history = history[-limit:]
return history
# 一些常用的通知创建函数
def create_new_message_notification(sender: str, target: str, message: Dict[str, Any]) -> Notification:
"""创建新消息通知"""
return Notification(
type=NotificationType.NEW_MESSAGE,
timestamp=datetime.now().timestamp(),
sender=sender,
target=target,
data={
"message_id": message.get("message_id"),
"content": message.get("content"),
"sender": message.get("sender"),
"time": message.get("time")
}
)
def create_cold_chat_notification(sender: str, target: str, is_cold: bool) -> StateNotification:
"""创建冷场状态通知"""
return StateNotification(
type=NotificationType.COLD_CHAT,
timestamp=datetime.now().timestamp(),
sender=sender,
target=target,
data={"is_cold": is_cold},
is_active=is_cold
)
def create_active_chat_notification(sender: str, target: str, is_active: bool) -> StateNotification:
"""创建活跃状态通知"""
return StateNotification(
type=NotificationType.ACTIVE_CHAT,
timestamp=datetime.now().timestamp(),
sender=sender,
target=target,
data={"is_active": is_active},
is_active=is_active
)
class ChatStateManager:
"""聊天状态管理器"""
def __init__(self):
self.current_state = ChatState.NORMAL
self.state_info = ChatStateInfo(state=ChatState.NORMAL)
self.state_history: list[ChatStateInfo] = []
def update_state(self, new_state: ChatState, **kwargs):
"""更新聊天状态
Args:
new_state: 新的状态
**kwargs: 其他状态信息
"""
self.current_state = new_state
self.state_info.state = new_state
# 更新其他状态信息
for key, value in kwargs.items():
if hasattr(self.state_info, key):
setattr(self.state_info, key, value)
# 记录状态历史
self.state_history.append(self.state_info)
def get_current_state_info(self) -> ChatStateInfo:
"""获取当前状态信息"""
return self.state_info
def get_state_history(self) -> list[ChatStateInfo]:
"""获取状态历史"""
return self.state_history
def is_cold_chat(self, threshold: float = 60.0) -> bool:
"""判断是否处于冷场状态
Args:
threshold: 冷场阈值(秒)
Returns:
bool: 是否冷场
"""
if not self.state_info.last_message_time:
return True
current_time = datetime.now().timestamp()
return (current_time - self.state_info.last_message_time) > threshold
def is_active_chat(self, threshold: float = 5.0) -> bool:
"""判断是否处于活跃状态
Args:
threshold: 活跃阈值(秒)
Returns:
bool: 是否活跃
"""
if not self.state_info.last_message_time:
return False
current_time = datetime.now().timestamp()
return (current_time - self.state_info.last_message_time) <= threshold

View File

@@ -0,0 +1,245 @@
import asyncio
import datetime
from typing import Dict, Any
from ..chat.message import Message
from .pfc_types import ConversationState
from .pfc import ChatObserver, GoalAnalyzer, Waiter, DirectMessageSender
from src.common.logger import get_module_logger
from .action_planner import ActionPlanner
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
from .reply_generator import ReplyGenerator
from ..chat.chat_stream import ChatStream
from ..message.message_base import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from .pfc_KnowledgeFetcher import KnowledgeFetcher
import traceback
logger = get_module_logger("pfc_conversation")
class Conversation:
"""对话类,负责管理单个对话的状态和行为"""
def __init__(self, stream_id: str):
"""初始化对话实例
Args:
stream_id: 聊天流ID
"""
self.stream_id = stream_id
self.state = ConversationState.INIT
self.should_continue = False
# 回复相关
self.generated_reply = ""
async def _initialize(self):
"""初始化实例,注册所有组件"""
try:
self.action_planner = ActionPlanner(self.stream_id)
self.goal_analyzer = GoalAnalyzer(self.stream_id)
self.reply_generator = ReplyGenerator(self.stream_id)
self.knowledge_fetcher = KnowledgeFetcher()
self.waiter = Waiter(self.stream_id)
self.direct_sender = DirectMessageSender()
# 获取聊天流信息
self.chat_stream = chat_manager.get_stream(self.stream_id)
self.stop_action_planner = False
except Exception as e:
logger.error(f"初始化对话实例:注册运行组件失败: {e}")
logger.error(traceback.format_exc())
raise
try:
#决策所需要的信息,包括自身自信和观察信息两部分
#注册观察器和观测信息
self.chat_observer = ChatObserver.get_instance(self.stream_id)
self.chat_observer.start()
self.observation_info = ObservationInfo()
self.observation_info.bind_to_chat_observer(self.stream_id)
#对话信息
self.conversation_info = ConversationInfo()
except Exception as e:
logger.error(f"初始化对话实例:注册信息组件失败: {e}")
logger.error(traceback.format_exc())
raise
# 组件准备完成,启动该论对话
self.should_continue = True
asyncio.create_task(self.start())
async def start(self):
"""开始对话流程"""
try:
logger.info("对话系统启动中...")
asyncio.create_task(self._plan_and_action_loop())
except Exception as e:
logger.error(f"启动对话系统失败: {e}")
raise
async def _plan_and_action_loop(self):
"""思考步PFC核心循环模块"""
# 获取最近的消息历史
while self.should_continue:
# 使用决策信息来辅助行动规划
action, reason = await self.action_planner.plan(
self.observation_info,
self.conversation_info
)
if self._check_new_messages_after_planning():
continue
# 执行行动
await self._handle_action(action, reason, self.observation_info, self.conversation_info)
def _check_new_messages_after_planning(self):
"""检查在规划后是否有新消息"""
if self.observation_info.new_messages_count > 0:
logger.info(f"发现{self.observation_info.new_messages_count}条新消息,可能需要重新考虑行动")
# 如果需要,可以在这里添加逻辑来根据新消息重新决定行动
return True
return False
def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message:
"""将消息字典转换为Message对象"""
try:
chat_info = msg_dict.get("chat_info", {})
chat_stream = ChatStream.from_dict(chat_info)
user_info = UserInfo.from_dict(msg_dict.get("user_info", {}))
return Message(
message_id=msg_dict["message_id"],
chat_stream=chat_stream,
time=msg_dict["time"],
user_info=user_info,
processed_plain_text=msg_dict.get("processed_plain_text", ""),
detailed_plain_text=msg_dict.get("detailed_plain_text", "")
)
except Exception as e:
logger.warning(f"转换消息时出错: {e}")
raise
async def _handle_action(self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo):
"""处理规划的行动"""
logger.info(f"执行行动: {action}, 原因: {reason}")
# 记录action历史先设置为stop完成后再设置为done
conversation_info.done_action.append({
"action": action,
"reason": reason,
"status": "start",
"time": datetime.datetime.now().strftime("%H:%M:%S")
})
if action == "direct_reply":
self.state = ConversationState.GENERATING
self.generated_reply = await self.reply_generator.generate(
observation_info,
conversation_info
)
# # 检查回复是否合适
# is_suitable, reason, need_replan = await self.reply_generator.check_reply(
# self.generated_reply,
# self.current_goal
# )
if self._check_new_messages_after_planning():
return None
await self._send_reply()
conversation_info.done_action.append({
"action": action,
"reason": reason,
"status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S")
})
elif action == "fetch_knowledge":
self.state = ConversationState.FETCHING
knowledge = "TODO:知识"
topic = "TODO:关键词"
logger.info(f"假装获取到知识{knowledge},关键词是: {topic}")
if knowledge:
if topic not in self.conversation_info.knowledge_list:
self.conversation_info.knowledge_list.append({
"topic": topic,
"knowledge": knowledge
})
else:
self.conversation_info.knowledge_list[topic] += knowledge
elif action == "rethink_goal":
self.state = ConversationState.RETHINKING
await self.goal_analyzer.analyze_goal(conversation_info, observation_info)
elif action == "listening":
self.state = ConversationState.LISTENING
logger.info("倾听对方发言...")
if await self.waiter.wait(): # 如果返回True表示超时
await self._send_timeout_message()
await self._stop_conversation()
else: # wait
self.state = ConversationState.WAITING
logger.info("等待更多信息...")
if await self.waiter.wait(): # 如果返回True表示超时
await self._send_timeout_message()
await self._stop_conversation()
async def _send_timeout_message(self):
"""发送超时结束消息"""
try:
messages = self.chat_observer.get_cached_messages(limit=1)
if not messages:
return
latest_message = self._convert_to_message(messages[0])
await self.direct_sender.send_message(
chat_stream=self.chat_stream,
content="TODO:超时消息",
reply_to_message=latest_message
)
except Exception as e:
logger.error(f"发送超时消息失败: {str(e)}")
async def _send_reply(self):
"""发送回复"""
if not self.generated_reply:
logger.warning("没有生成回复")
return
messages = self.chat_observer.get_cached_messages(limit=1)
if not messages:
logger.warning("没有最近的消息可以回复")
return
latest_message = self._convert_to_message(messages[0])
try:
await self.direct_sender.send_message(
chat_stream=self.chat_stream,
content=self.generated_reply,
reply_to_message=latest_message
)
self.chat_observer.trigger_update() # 触发立即更新
if not await self.chat_observer.wait_for_update():
logger.warning("等待消息更新超时")
self.state = ConversationState.ANALYZING
except Exception as e:
logger.error(f"发送消息失败: {str(e)}")
self.state = ConversationState.ANALYZING

View File

@@ -0,0 +1,8 @@
class ConversationInfo:
def __init__(self):
self.done_action = []
self.goal_list = []
self.knowledge_list = []
self.memory_list = []

View File

@@ -0,0 +1,49 @@
from typing import Optional
from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream
from ..chat.message import Message
from ..message.message_base import Seg
from src.plugins.chat.message import MessageSending
logger = get_module_logger("message_sender")
class DirectMessageSender:
"""直接消息发送器"""
def __init__(self):
pass
async def send_message(
self,
chat_stream: ChatStream,
content: str,
reply_to_message: Optional[Message] = None,
) -> None:
"""发送消息到聊天流
Args:
chat_stream: 聊天流
content: 消息内容
reply_to_message: 要回复的消息(可选)
"""
try:
# 创建消息内容
segments = [Seg(type="text", data={"text": content})]
# 检查是否需要引用回复
if reply_to_message:
reply_id = reply_to_message.message_id
message_sending = MessageSending(
segments=segments,
reply_to_id=reply_id
)
else:
message_sending = MessageSending(segments=segments)
# 发送消息
await chat_stream.send_message(message_sending)
logger.info(f"消息已发送: {content}")
except Exception as e:
logger.error(f"发送消息失败: {str(e)}")
raise

View File

@@ -0,0 +1,134 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from src.common.database import db
class MessageStorage(ABC):
"""消息存储接口"""
@abstractmethod
async def get_messages_after(self, chat_id: str, message_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""获取指定消息ID之后的所有消息
Args:
chat_id: 聊天ID
message_id: 消息ID如果为None则获取所有消息
Returns:
List[Dict[str, Any]]: 消息列表
"""
pass
@abstractmethod
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
"""获取指定时间点之前的消息
Args:
chat_id: 聊天ID
time_point: 时间戳
limit: 最大消息数量
Returns:
List[Dict[str, Any]]: 消息列表
"""
pass
@abstractmethod
async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
"""检查是否有新消息
Args:
chat_id: 聊天ID
after_time: 时间戳
Returns:
bool: 是否有新消息
"""
pass
class MongoDBMessageStorage(MessageStorage):
"""MongoDB消息存储实现"""
def __init__(self):
self.db = db
async def get_messages_after(self, chat_id: str, message_id: Optional[str] = None) -> List[Dict[str, Any]]:
query = {"chat_id": chat_id}
if message_id:
# 获取ID大于message_id的消息
last_message = self.db.messages.find_one({"message_id": message_id})
if last_message:
query["time"] = {"$gt": last_message["time"]}
return list(
self.db.messages.find(query).sort("time", 1)
)
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
query = {
"chat_id": chat_id,
"time": {"$lt": time_point}
}
messages = list(
self.db.messages.find(query).sort("time", -1).limit(limit)
)
# 将消息按时间正序排列
messages.reverse()
return messages
async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
query = {
"chat_id": chat_id,
"time": {"$gt": after_time}
}
return self.db.messages.find_one(query) is not None
# # 创建一个内存消息存储实现,用于测试
# class InMemoryMessageStorage(MessageStorage):
# """内存消息存储实现,主要用于测试"""
# def __init__(self):
# self.messages: Dict[str, List[Dict[str, Any]]] = {}
# async def get_messages_after(self, chat_id: str, message_id: Optional[str] = None) -> List[Dict[str, Any]]:
# if chat_id not in self.messages:
# return []
# messages = self.messages[chat_id]
# if not message_id:
# return messages
# # 找到message_id的索引
# try:
# index = next(i for i, m in enumerate(messages) if m["message_id"] == message_id)
# return messages[index + 1:]
# except StopIteration:
# return []
# async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
# if chat_id not in self.messages:
# return []
# messages = [
# m for m in self.messages[chat_id]
# if m["time"] < time_point
# ]
# return messages[-limit:]
# async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
# if chat_id not in self.messages:
# return False
# return any(m["time"] > after_time for m in self.messages[chat_id])
# # 测试辅助方法
# def add_message(self, chat_id: str, message: Dict[str, Any]):
# """添加测试消息"""
# if chat_id not in self.messages:
# self.messages[chat_id] = []
# self.messages[chat_id].append(message)
# self.messages[chat_id].sort(key=lambda m: m["time"])

View File

@@ -0,0 +1,71 @@
from typing import TYPE_CHECKING
from src.common.logger import get_module_logger
from .chat_states import NotificationHandler, Notification, NotificationType
if TYPE_CHECKING:
from .conversation import Conversation
logger = get_module_logger("notification_handler")
class PFCNotificationHandler(NotificationHandler):
"""PFC通知处理器"""
def __init__(self, conversation: 'Conversation'):
"""初始化PFC通知处理器
Args:
conversation: 对话实例
"""
self.conversation = conversation
async def handle_notification(self, notification: Notification):
"""处理通知
Args:
notification: 通知对象
"""
logger.debug(f"收到通知: {notification.type.name}, 数据: {notification.data}")
# 根据通知类型执行不同的处理
if notification.type == NotificationType.NEW_MESSAGE:
# 新消息通知
await self._handle_new_message(notification)
elif notification.type == NotificationType.COLD_CHAT:
# 冷聊天通知
await self._handle_cold_chat(notification)
elif notification.type == NotificationType.COMMAND:
# 命令通知
await self._handle_command(notification)
else:
logger.warning(f"未知的通知类型: {notification.type.name}")
async def _handle_new_message(self, notification: Notification):
"""处理新消息通知
Args:
notification: 通知对象
"""
# 更新决策信息
observation_info = self.conversation.observation_info
observation_info.last_message_time = notification.data.get("time", 0)
observation_info.add_unprocessed_message(notification.data)
# 手动触发观察器更新
self.conversation.chat_observer.trigger_update()
async def _handle_cold_chat(self, notification: Notification):
"""处理冷聊天通知
Args:
notification: 通知对象
"""
# 获取冷聊天信息
cold_duration = notification.data.get("duration", 0)
# 更新决策信息
observation_info = self.conversation.observation_info
observation_info.conversation_cold_duration = cold_duration
logger.info(f"对话已冷: {cold_duration}")

View File

@@ -0,0 +1,246 @@
#Programmable Friendly Conversationalist
#Prefrontal cortex
from typing import List, Optional, Dict, Any, Set
from ..message.message_base import UserInfo
import time
from dataclasses import dataclass, field
from src.common.logger import get_module_logger
from .chat_observer import ChatObserver
from .chat_states import NotificationHandler
logger = get_module_logger("observation_info")
class ObservationInfoHandler(NotificationHandler):
"""ObservationInfo的通知处理器"""
def __init__(self, observation_info: 'ObservationInfo'):
"""初始化处理器
Args:
observation_info: 要更新的ObservationInfo实例
"""
self.observation_info = observation_info
async def handle_notification(self, notification: Dict[str, Any]):
"""处理通知
Args:
notification: 通知数据
"""
notification_type = notification.get("type")
data = notification.get("data", {})
if notification_type == "NEW_MESSAGE":
# 处理新消息通知
logger.debug(f"收到新消息通知data: {data}")
message = data.get("message", {})
self.observation_info.update_from_message(message)
# self.observation_info.has_unread_messages = True
# self.observation_info.new_unread_message.append(message.get("processed_plain_text", ""))
elif notification_type == "COLD_CHAT":
# 处理冷场通知
is_cold = data.get("is_cold", False)
self.observation_info.update_cold_chat_status(is_cold, time.time())
elif notification_type == "ACTIVE_CHAT":
# 处理活跃通知
is_active = data.get("is_active", False)
self.observation_info.is_cold = not is_active
elif notification_type == "BOT_SPEAKING":
# 处理机器人说话通知
self.observation_info.is_typing = False
self.observation_info.last_bot_speak_time = time.time()
elif notification_type == "USER_SPEAKING":
# 处理用户说话通知
self.observation_info.is_typing = False
self.observation_info.last_user_speak_time = time.time()
elif notification_type == "MESSAGE_DELETED":
# 处理消息删除通知
message_id = data.get("message_id")
self.observation_info.unprocessed_messages = [
msg for msg in self.observation_info.unprocessed_messages
if msg.get("message_id") != message_id
]
elif notification_type == "USER_JOINED":
# 处理用户加入通知
user_id = data.get("user_id")
if user_id:
self.observation_info.active_users.add(user_id)
elif notification_type == "USER_LEFT":
# 处理用户离开通知
user_id = data.get("user_id")
if user_id:
self.observation_info.active_users.discard(user_id)
elif notification_type == "ERROR":
# 处理错误通知
error_msg = data.get("error", "")
logger.error(f"收到错误通知: {error_msg}")
@dataclass
class ObservationInfo:
"""决策信息类用于收集和管理来自chat_observer的通知信息"""
#data_list
chat_history: List[str] = field(default_factory=list)
unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list)
active_users: Set[str] = field(default_factory=set)
#data
last_bot_speak_time: Optional[float] = None
last_user_speak_time: Optional[float] = None
last_message_time: Optional[float] = None
last_message_content: str = ""
last_message_sender: Optional[str] = None
bot_id: Optional[str] = None
new_messages_count: int = 0
cold_chat_duration: float = 0.0
#state
is_typing: bool = False
has_unread_messages: bool = False
is_cold_chat: bool = False
changed: bool = False
# #spec
# meta_plan_trigger: bool = False
def __post_init__(self):
"""初始化后创建handler"""
self.chat_observer = None
self.handler = ObservationInfoHandler(self)
def bind_to_chat_observer(self, stream_id: str):
"""绑定到指定的chat_observer
Args:
stream_id: 聊天流ID
"""
self.chat_observer = ChatObserver.get_instance(stream_id)
self.chat_observer.notification_manager.register_handler(
target="observation_info",
notification_type="NEW_MESSAGE",
handler=self.handler
)
self.chat_observer.notification_manager.register_handler(
target="observation_info",
notification_type="COLD_CHAT",
handler=self.handler
)
def unbind_from_chat_observer(self):
"""解除与chat_observer的绑定"""
if self.chat_observer:
self.chat_observer.notification_manager.unregister_handler(
target="observation_info",
notification_type="NEW_MESSAGE",
handler=self.handler
)
self.chat_observer.notification_manager.unregister_handler(
target="observation_info",
notification_type="COLD_CHAT",
handler=self.handler
)
self.chat_observer = None
def update_from_message(self, message: Dict[str, Any]):
"""从消息更新信息
Args:
message: 消息数据
"""
logger.debug(f"更新信息from_message: {message}")
self.last_message_time = message["time"]
self.last_message_content = message.get("processed_plain_text", "")
user_info = UserInfo.from_dict(message.get("user_info", {}))
self.last_message_sender = user_info.user_id
if user_info.user_id == self.bot_id:
self.last_bot_speak_time = message["time"]
else:
self.last_user_speak_time = message["time"]
self.active_users.add(user_info.user_id)
self.new_messages_count += 1
self.unprocessed_messages.append(message)
self.update_changed()
def update_changed(self):
"""更新changed状态"""
self.changed = True
# self.meta_plan_trigger = True
def update_cold_chat_status(self, is_cold: bool, current_time: float):
"""更新冷场状态
Args:
is_cold: 是否冷场
current_time: 当前时间
"""
self.is_cold_chat = is_cold
if is_cold and self.last_message_time:
self.cold_chat_duration = current_time - self.last_message_time
def get_active_duration(self) -> float:
"""获取当前活跃时长
Returns:
float: 最后一条消息到现在的时长(秒)
"""
if not self.last_message_time:
return 0.0
return time.time() - self.last_message_time
def get_user_response_time(self) -> Optional[float]:
"""获取用户响应时间
Returns:
Optional[float]: 用户最后发言到现在的时长如果没有用户发言则返回None
"""
if not self.last_user_speak_time:
return None
return time.time() - self.last_user_speak_time
def get_bot_response_time(self) -> Optional[float]:
"""获取机器人响应时间
Returns:
Optional[float]: 机器人最后发言到现在的时长如果没有机器人发言则返回None
"""
if not self.last_bot_speak_time:
return None
return time.time() - self.last_bot_speak_time
def clear_unprocessed_messages(self):
"""清空未处理消息列表"""
# 将未处理消息添加到历史记录中
for message in self.unprocessed_messages:
if "processed_plain_text" in message:
self.chat_history.append(message["processed_plain_text"])
# 清空未处理消息列表
self.has_unread_messages = False
self.unprocessed_messages.clear()
self.new_messages_count = 0
def add_unprocessed_message(self, message: Dict[str, Any]):
"""添加未处理的消息
Args:
message: 消息数据
"""
# 防止重复添加同一消息
message_id = message.get("message_id")
if message_id and not any(m.get("message_id") == message_id for m in self.unprocessed_messages):
self.unprocessed_messages.append(message)
self.new_messages_count += 1
# 同时更新其他消息相关信息
self.update_from_message(message)

File diff suppressed because it is too large Load Diff

View File

@@ -7,24 +7,22 @@ from ..chat.message import Message
logger = get_module_logger("knowledge_fetcher")
class KnowledgeFetcher:
"""知识调取器"""
def __init__(self):
self.llm = LLM_request(
model=global_config.llm_normal,
temperature=0.7,
max_tokens=1000,
request_type="knowledge_fetch"
model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="knowledge_fetch"
)
async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]:
"""获取相关知识
Args:
query: 查询内容
chat_history: 聊天历史
Returns:
Tuple[str, str]: (获取的知识, 知识来源)
"""
@@ -33,16 +31,16 @@ class KnowledgeFetcher:
for msg in chat_history:
# sender = msg.message_info.user_info.user_nickname or f"用户{msg.message_info.user_info.user_id}"
chat_history_text += f"{msg.detailed_plain_text}\n"
# 从记忆中获取相关知识
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
text=f"{query}\n{chat_history_text}",
max_memory_num=3,
max_memory_length=2,
max_depth=3,
fast_retrieval=False
fast_retrieval=False,
)
if related_memory:
knowledge = ""
sources = []
@@ -50,5 +48,5 @@ class KnowledgeFetcher:
knowledge += memory[1] + "\n"
sources.append(f"记忆片段{memory[0]}")
return knowledge.strip(), "".join(sources)
return "未找到相关知识", "无记忆匹配"
return "未找到相关知识", "无记忆匹配"

View File

@@ -0,0 +1,97 @@
from typing import Dict, Optional
from src.common.logger import get_module_logger
from .conversation import Conversation
import traceback
logger = get_module_logger("pfc_manager")
class PFCManager:
"""PFC对话管理器负责管理所有对话实例"""
# 单例模式
_instance = None
# 会话实例管理
_instances: Dict[str, Conversation] = {}
_initializing: Dict[str, bool] = {}
@classmethod
def get_instance(cls) -> 'PFCManager':
"""获取管理器单例
Returns:
PFCManager: 管理器实例
"""
if cls._instance is None:
cls._instance = PFCManager()
return cls._instance
async def get_or_create_conversation(self, stream_id: str) -> Optional[Conversation]:
"""获取或创建对话实例
Args:
stream_id: 聊天流ID
Returns:
Optional[Conversation]: 对话实例创建失败则返回None
"""
# 检查是否已经有实例
if stream_id in self._initializing and self._initializing[stream_id]:
logger.debug(f"会话实例正在初始化中: {stream_id}")
return None
if stream_id in self._instances:
logger.debug(f"使用现有会话实例: {stream_id}")
return self._instances[stream_id]
try:
# 创建新实例
logger.info(f"创建新的对话实例: {stream_id}")
self._initializing[stream_id] = True
# 创建实例
conversation_instance = Conversation(stream_id)
self._instances[stream_id] = conversation_instance
# 启动实例初始化
await self._initialize_conversation(conversation_instance)
except Exception as e:
logger.error(f"创建会话实例失败: {stream_id}, 错误: {e}")
return None
return conversation_instance
async def _initialize_conversation(self, conversation: Conversation):
"""初始化会话实例
Args:
conversation: 要初始化的会话实例
"""
stream_id = conversation.stream_id
try:
logger.info(f"开始初始化会话实例: {stream_id}")
# 启动初始化流程
await conversation._initialize()
# 标记初始化完成
self._initializing[stream_id] = False
logger.info(f"会话实例 {stream_id} 初始化完成")
except Exception as e:
logger.error(f"管理器初始化会话实例失败: {stream_id}, 错误: {e}")
logger.error(traceback.format_exc())
# 清理失败的初始化
async def get_conversation(self, stream_id: str) -> Optional[Conversation]:
"""获取已存在的会话实例
Args:
stream_id: 聊天流ID
Returns:
Optional[Conversation]: 会话实例不存在则返回None
"""
return self._instances.get(stream_id)

View File

@@ -0,0 +1,21 @@
from enum import Enum
from typing import Literal
class ConversationState(Enum):
"""对话状态"""
INIT = "初始化"
RETHINKING = "重新思考"
ANALYZING = "分析历史"
PLANNING = "规划目标"
GENERATING = "生成回复"
CHECKING = "检查回复"
SENDING = "发送消息"
FETCHING = "获取知识"
WAITING = "等待"
LISTENING = "倾听"
ENDED = "结束"
JUDGING = "判断"
ActionType = Literal["direct_reply", "fetch_knowledge", "wait"]

View File

@@ -1,40 +1,41 @@
import json
import re
from typing import Dict, Any, Optional, List, Tuple, Union
from typing import Dict, Any, Optional, Tuple
from src.common.logger import get_module_logger
logger = get_module_logger("pfc_utils")
def get_items_from_json(
content: str,
*items: str,
default_values: Optional[Dict[str, Any]] = None,
required_types: Optional[Dict[str, type]] = None
required_types: Optional[Dict[str, type]] = None,
) -> Tuple[bool, Dict[str, Any]]:
"""从文本中提取JSON内容并获取指定字段
Args:
content: 包含JSON的文本
*items: 要提取的字段名
default_values: 字段的默认值,格式为 {字段名: 默认值}
required_types: 字段的必需类型,格式为 {字段名: 类型}
Returns:
Tuple[bool, Dict[str, Any]]: (是否成功, 提取的字段字典)
"""
content = content.strip()
result = {}
# 设置默认值
if default_values:
result.update(default_values)
# 尝试解析JSON
try:
json_data = json.loads(content)
except json.JSONDecodeError:
# 如果直接解析失败尝试查找和提取JSON部分
json_pattern = r'\{[^{}]*\}'
json_pattern = r"\{[^{}]*\}"
json_match = re.search(json_pattern, content)
if json_match:
try:
@@ -45,28 +46,28 @@ def get_items_from_json(
else:
logger.error("无法在返回内容中找到有效的JSON")
return False, result
# 提取字段
for item in items:
if item in json_data:
result[item] = json_data[item]
# 验证必需字段
if not all(item in result for item in items):
logger.error(f"JSON缺少必要字段实际内容: {json_data}")
return False, result
# 验证字段类型
if required_types:
for field, expected_type in required_types.items():
if field in result and not isinstance(result[field], expected_type):
logger.error(f"{field} 必须是 {expected_type.__name__} 类型")
return False, result
# 验证字符串字段不为空
for field in items:
if isinstance(result[field], str) and not result[field].strip():
logger.error(f"{field} 不能为空")
return False, result
return True, result
return True, result

View File

@@ -9,38 +9,31 @@ from ..message.message_base import UserInfo
logger = get_module_logger("reply_checker")
class ReplyChecker:
"""回复检查器"""
def __init__(self, stream_id: str):
self.llm = LLM_request(
model=global_config.llm_normal,
temperature=0.7,
max_tokens=1000,
request_type="reply_check"
model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="reply_check"
)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
self.max_retries = 2 # 最大重试次数
async def check(
self,
reply: str,
goal: str,
retry_count: int = 0
) -> Tuple[bool, str, bool]:
async def check(self, reply: str, goal: str, retry_count: int = 0) -> Tuple[bool, str, bool]:
"""检查生成的回复是否合适
Args:
reply: 生成的回复
goal: 对话目标
retry_count: 当前重试次数
Returns:
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
"""
# 获取最新的消息记录
messages = self.chat_observer.get_message_history(limit=5)
messages = self.chat_observer.get_cached_messages(limit=5)
chat_history_text = ""
for msg in messages:
time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S")
@@ -49,7 +42,7 @@ class ReplyChecker:
if sender == self.name:
sender = "你说"
chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n"
prompt = f"""请检查以下回复是否合适:
当前对话目标:{goal}
@@ -83,7 +76,7 @@ class ReplyChecker:
try:
content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"检查回复的原始返回: {content}")
# 清理内容尝试提取JSON部分
content = content.strip()
try:
@@ -92,7 +85,8 @@ class ReplyChecker:
except json.JSONDecodeError:
# 如果直接解析失败尝试查找和提取JSON部分
import re
json_pattern = r'\{[^{}]*\}'
json_pattern = r"\{[^{}]*\}"
json_match = re.search(json_pattern, content)
if json_match:
try:
@@ -109,33 +103,33 @@ class ReplyChecker:
reason = content[:100] if content else "无法解析响应"
need_replan = "重新规划" in content.lower() or "目标不适合" in content.lower()
return is_suitable, reason, need_replan
# 验证JSON字段
suitable = result.get("suitable", None)
reason = result.get("reason", "未提供原因")
need_replan = result.get("need_replan", False)
# 如果suitable字段是字符串转换为布尔值
if isinstance(suitable, str):
suitable = suitable.lower() == "true"
# 如果suitable字段不存在或不是布尔值从reason中判断
if suitable is None:
suitable = "不合适" not in reason.lower() and "违规" not in reason.lower()
# 如果不合适且未达到最大重试次数,返回需要重试
if not suitable and retry_count < self.max_retries:
return False, reason, False
# 如果不合适且已达到最大重试次数,返回需要重新规划
if not suitable and retry_count >= self.max_retries:
return False, f"多次重试后仍不合适: {reason}", True
return suitable, reason, need_replan
except Exception as e:
logger.error(f"检查回复时出错: {e}")
# 如果出错且已达到最大重试次数,建议重新规划
if retry_count >= self.max_retries:
return False, "多次检查失败,建议重新规划", True
return False, f"检查过程出错,建议重试: {str(e)}", False
return False, f"检查过程出错,建议重试: {str(e)}", False

View File

@@ -0,0 +1,126 @@
from typing import Tuple
from src.common.logger import get_module_logger
from ..models.utils_model import LLM_request
from ..config.config import global_config
from .chat_observer import ChatObserver
from .reply_checker import ReplyChecker
from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
logger = get_module_logger("reply_generator")
class ReplyGenerator:
"""回复生成器"""
def __init__(self, stream_id: str):
self.llm = LLM_request(
model=global_config.llm_normal,
temperature=0.7,
max_tokens=300,
request_type="reply_generation"
)
self.personality_info = Individuality.get_instance().get_prompt(type = "personality", x_person = 2, level = 2)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
self.reply_checker = ReplyChecker(stream_id)
async def generate(
self,
observation_info: ObservationInfo,
conversation_info: ConversationInfo
) -> str:
"""生成回复
Args:
goal: 对话目标
chat_history: 聊天历史
knowledge_cache: 知识缓存
previous_reply: 上一次生成的回复(如果有)
retry_count: 当前重试次数
Returns:
str: 生成的回复
"""
# 构建提示词
logger.debug(f"开始生成回复:当前目标: {conversation_info.goal_list}")
goal_list = conversation_info.goal_list
goal_text = ""
for goal, reason in goal_list:
goal_text += f"目标:{goal};"
goal_text += f"原因:{reason}\n"
# 获取聊天历史记录
chat_history_list = observation_info.chat_history
chat_history_text = ""
for msg in chat_history_list:
chat_history_text += f"{msg}\n"
# 整理知识缓存
knowledge_text = ""
knowledge_list = conversation_info.knowledge_list
for knowledge in knowledge_list:
knowledge_text += f"知识:{knowledge}\n"
personality_text = f"你的名字是{self.name}{self.personality_info}"
prompt = f"""{personality_text}。现在你在参与一场QQ聊天请根据以下信息生成回复
当前对话目标:{goal_text}
{knowledge_text}
最近的聊天记录:
{chat_history_text}
请根据上述信息,以你的性格特征生成一个自然、得体的回复。回复应该:
1. 符合对话目标,以""的角度发言
2. 体现你的性格特征
3. 自然流畅,像正常聊天一样,简短
4. 适当利用相关知识,但不要生硬引用
请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清""和对方说的话,不要把""说的话当做对方说的话,这是你自己说的话。
请你回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话
请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )。
请直接输出回复内容,不需要任何额外格式。"""
try:
content, _ = await self.llm.generate_response_async(prompt)
logger.info(f"生成的回复: {content}")
# is_new = self.chat_observer.check()
# logger.debug(f"再看一眼聊天记录,{'有' if is_new else '没有'}新消息")
# 如果有新消息,重新生成回复
# if is_new:
# logger.info("检测到新消息,重新生成回复")
# return await self.generate(
# goal, chat_history, knowledge_cache,
# None, retry_count
# )
return content
except Exception as e:
logger.error(f"生成回复时出错: {e}")
return "抱歉,我现在有点混乱,让我重新思考一下..."
async def check_reply(
self,
reply: str,
goal: str,
retry_count: int = 0
) -> Tuple[bool, str, bool]:
"""检查回复是否合适
Args:
reply: 生成的回复
goal: 对话目标
retry_count: 当前重试次数
Returns:
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
"""
return await self.reply_checker.check(reply, goal, retry_count)

45
src/plugins/PFC/waiter.py Normal file
View File

@@ -0,0 +1,45 @@
from src.common.logger import get_module_logger
from .chat_observer import ChatObserver
logger = get_module_logger("waiter")
class Waiter:
"""等待器,用于等待对话流中的事件"""
def __init__(self, stream_id: str):
self.stream_id = stream_id
self.chat_observer = ChatObserver.get_instance(stream_id)
async def wait(self, timeout: float = 20.0) -> bool:
"""等待用户回复或超时
Args:
timeout: 超时时间(秒)
Returns:
bool: 如果因为超时返回则为True否则为False
"""
try:
message_before = self.chat_observer.get_last_message()
# 等待新消息
logger.debug(f"等待新消息,超时时间: {timeout}")
is_timeout = await self.chat_observer.wait_for_update(timeout=timeout)
if is_timeout:
logger.debug("等待超时,没有收到新消息")
return True
# 检查是否是新消息
message_after = self.chat_observer.get_last_message()
if message_before and message_after and message_before.get("message_id") == message_after.get("message_id"):
# 如果消息ID相同说明没有新消息
logger.debug("没有收到新消息")
return True
logger.debug("收到新消息")
return False
except Exception as e:
logger.error(f"等待时出错: {str(e)}")
return True

View File

@@ -12,5 +12,5 @@ __all__ = [
"chat_manager",
"message_manager",
"MessageStorage",
"auto_speak_manager"
"auto_speak_manager",
]

View File

@@ -1,14 +1,14 @@
from ..moods.moods import MoodManager # 导入情绪管理器
from ..config.config import global_config
from .message import MessageRecv
from ..PFC.pfc import Conversation, ConversationState
from ..PFC.pfc_manager import PFCManager
from .chat_stream import chat_manager
from ..chat_module.only_process.only_message_process import MessageProcessor
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ..chat_module.think_flow_chat.think_flow_chat import ThinkFlowChat
from ..chat_module.reasoning_chat.reasoning_chat import ReasoningChat
import asyncio
import traceback
# 定义日志配置
chat_config = LogConfig(
@@ -30,38 +30,27 @@ class ChatBot:
self.think_flow_chat = ThinkFlowChat()
self.reasoning_chat = ReasoningChat()
self.only_process_chat = MessageProcessor()
# 创建初始化PFC管理器的任务会在_ensure_started时执行
self.pfc_manager = PFCManager.get_instance()
async def _ensure_started(self):
"""确保所有任务已启动"""
if not self._started:
logger.info("确保ChatBot所有任务已启动")
self._started = True
async def _create_PFC_chat(self, message: MessageRecv):
try:
chat_id = str(message.chat_stream.stream_id)
if global_config.enable_pfc_chatting:
# 获取或创建对话实例
conversation = await Conversation.get_instance(chat_id)
if conversation is None:
logger.error(f"创建或获取对话实例失败: {chat_id}")
return
# 如果是新创建的实例,启动对话系统
if conversation.state == ConversationState.INIT:
asyncio.create_task(conversation.start())
logger.info(f"为聊天 {chat_id} 创建新的对话实例")
elif conversation.state == ConversationState.ENDED:
# 如果实例已经结束,重新创建
await Conversation.remove_instance(chat_id)
conversation = await Conversation.get_instance(chat_id)
if conversation is None:
logger.error(f"重新创建对话实例失败: {chat_id}")
return
asyncio.create_task(conversation.start())
logger.info(f"为聊天 {chat_id} 重新创建对话实例")
await self.pfc_manager.get_or_create_conversation(chat_id)
except Exception as e:
logger.error(f"创建PFC聊天失败: {e}")
logger.error(f"创建PFC聊天失败: {e}")
async def message_process(self, message_data: str) -> None:
"""处理转化后的统一格式消息
@@ -70,16 +59,16 @@ class ChatBot:
- 包含思维流状态管理
- 在回复前进行观察和状态更新
- 回复后更新思维流状态
2. reasoning模式使用推理系统进行回复
- 直接使用意愿管理器计算回复概率
- 没有思维流相关的状态管理
- 更简单直接的回复逻辑
3. pfc_chatting模式仅进行消息处理
- 不进行任何回复
- 只处理和存储消息
所有模式都包含:
- 消息过滤
- 记忆激活
@@ -89,6 +78,9 @@ class ChatBot:
- 性能计时
"""
try:
# 确保所有任务已启动
await self._ensure_started()
message = MessageRecv(message_data)
groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info
@@ -97,7 +89,7 @@ class ChatBot:
if userinfo.user_id in global_config.ban_user_id:
logger.debug(f"用户{userinfo.user_id}被禁止回复")
return
if global_config.enable_pfc_chatting:
try:
if groupinfo is None and global_config.enable_friend_chat:
@@ -126,7 +118,7 @@ class ChatBot:
logger.error(f"处理PFC消息失败: {e}")
else:
if groupinfo is None and global_config.enable_friend_chat:
# 私聊处理流程
# 私聊处理流程
# await self._handle_private_chat(message)
if global_config.response_mode == "heart_flow":
await self.think_flow_chat.process_message(message_data)
@@ -144,6 +136,7 @@ class ChatBot:
logger.error(f"未知的回复模式,请检查配置文件!!: {global_config.response_mode}")
except Exception as e:
logger.error(f"预处理消息失败: {e}")
traceback.print_exc()
# 创建全局ChatBot实例

View File

@@ -38,11 +38,11 @@ class EmojiManager:
self.llm_emotion_judge = LLM_request(
model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji"
) # 更高的温度更少的token后续可以根据情绪来调整温度
self.emoji_num = 0
self.emoji_num_max = global_config.max_emoji_num
self.emoji_num_max_reach_deletion = global_config.max_reach_deletion
logger.info("启动表情包管理器")
def _ensure_emoji_dir(self):
@@ -51,7 +51,7 @@ class EmojiManager:
def _update_emoji_count(self):
"""更新表情包数量统计
检查数据库中的表情包数量并更新到 self.emoji_num
"""
try:
@@ -249,7 +249,22 @@ class EmojiManager:
f for f in os.listdir(emoji_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
]
# 检查当前表情包数量
self._update_emoji_count()
if self.emoji_num >= self.emoji_num_max:
logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),跳过注册")
return
# 计算还可以注册的数量
remaining_slots = self.emoji_num_max - self.emoji_num
logger.info(f"[注册] 还可以注册 {remaining_slots} 个表情包")
for filename in files_to_process:
# 如果已经达到上限,停止注册
if self.emoji_num >= self.emoji_num_max:
logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),停止注册")
break
image_path = os.path.join(emoji_dir, filename)
# 获取图片的base64编码和哈希值
@@ -340,6 +355,10 @@ class EmojiManager:
logger.success(f"[注册] 新表情包: {filename}")
logger.info(f"[描述] {description}")
# 更新当前表情包数量
self.emoji_num += 1
logger.info(f"[统计] 当前表情包数量: {self.emoji_num}/{self.emoji_num_max}")
# 保存到images数据库
image_doc = {
"hash": image_hash,
@@ -357,7 +376,6 @@ class EmojiManager:
except Exception:
logger.exception("[错误] 扫描表情包失败")
def check_emoji_file_integrity(self):
"""检查表情包文件完整性
@@ -432,7 +450,7 @@ class EmojiManager:
def check_emoji_file_full(self):
"""检查表情包文件是否完整,如果数量超出限制且允许删除,则删除多余的表情包
删除规则:
1. 优先删除创建时间更早的表情包
2. 优先删除使用次数少的表情包,但使用次数多的也有小概率被删除
@@ -441,23 +459,23 @@ class EmojiManager:
self._ensure_db()
# 更新表情包数量
self._update_emoji_count()
# 检查是否超出限制
if self.emoji_num <= self.emoji_num_max:
return
# 如果超出限制但不允许删除,则只记录警告
if not global_config.max_reach_deletion:
logger.warning(f"[警告] 表情包数量({self.emoji_num})超出限制({self.emoji_num_max}),但未开启自动删除")
return
# 计算需要删除的数量
delete_count = self.emoji_num - self.emoji_num_max
logger.info(f"[清理] 需要删除 {delete_count} 个表情包")
# 获取所有表情包,按时间戳升序(旧的在前)排序
all_emojis = list(db.emoji.find().sort([("timestamp", 1)]))
# 计算权重:使用次数越多,被删除的概率越小
weights = []
max_usage = max((emoji.get("usage_count", 0) for emoji in all_emojis), default=1)
@@ -466,11 +484,11 @@ class EmojiManager:
# 使用指数衰减函数计算权重,使用次数越多权重越小
weight = 1.0 / (1.0 + usage_count / max(1, max_usage))
weights.append(weight)
# 根据权重随机选择要删除的表情包
to_delete = []
remaining_indices = list(range(len(all_emojis)))
while len(to_delete) < delete_count and remaining_indices:
# 计算当前剩余表情包的权重
current_weights = [weights[i] for i in remaining_indices]
@@ -478,13 +496,13 @@ class EmojiManager:
total_weight = sum(current_weights)
if total_weight == 0:
break
normalized_weights = [w/total_weight for w in current_weights]
normalized_weights = [w / total_weight for w in current_weights]
# 随机选择一个表情包
selected_idx = random.choices(remaining_indices, weights=normalized_weights, k=1)[0]
to_delete.append(all_emojis[selected_idx])
remaining_indices.remove(selected_idx)
# 删除选中的表情包
deleted_count = 0
for emoji in to_delete:
@@ -493,26 +511,26 @@ class EmojiManager:
if "path" in emoji and os.path.exists(emoji["path"]):
os.remove(emoji["path"])
logger.info(f"[删除] 文件: {emoji['path']} (使用次数: {emoji.get('usage_count', 0)})")
# 删除数据库记录
db.emoji.delete_one({"_id": emoji["_id"]})
deleted_count += 1
# 同时从images集合中删除
if "hash" in emoji:
db.images.delete_one({"hash": emoji["hash"]})
except Exception as e:
logger.error(f"[错误] 删除表情包失败: {str(e)}")
continue
# 更新表情包数量
self._update_emoji_count()
logger.success(f"[清理] 已删除 {deleted_count} 个表情包,当前数量: {self.emoji_num}")
except Exception as e:
logger.error(f"[错误] 检查表情包数量失败: {str(e)}")
async def start_periodic_check_register(self):
"""定期检查表情包完整性和数量"""
while True:
@@ -523,7 +541,7 @@ class EmojiManager:
logger.info("[扫描] 开始扫描新表情包...")
if self.emoji_num < self.emoji_num_max:
await self.scan_new_emojis()
if (self.emoji_num > self.emoji_num_max):
if self.emoji_num > self.emoji_num_max:
logger.warning(f"[警告] 表情包数量超过最大限制: {self.emoji_num} > {self.emoji_num_max},跳过注册")
if not global_config.max_reach_deletion:
logger.warning("表情包数量超过最大限制,终止注册")
@@ -532,7 +550,7 @@ class EmojiManager:
logger.warning("表情包数量超过最大限制,开始删除表情包")
self.check_emoji_file_full()
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
async def delete_all_images(self):
"""删除 data/image 目录下的所有文件"""
try:
@@ -540,10 +558,10 @@ class EmojiManager:
if not os.path.exists(image_dir):
logger.warning(f"[警告] 目录不存在: {image_dir}")
return
deleted_count = 0
failed_count = 0
# 遍历目录下的所有文件
for filename in os.listdir(image_dir):
file_path = os.path.join(image_dir, filename)
@@ -555,11 +573,12 @@ class EmojiManager:
except Exception as e:
failed_count += 1
logger.error(f"[错误] 删除文件失败 {file_path}: {str(e)}")
logger.success(f"[清理] 已删除 {deleted_count} 个文件,失败 {failed_count}")
except Exception as e:
logger.error(f"[错误] 删除图片目录失败: {str(e)}")
# 创建全局单例
emoji_manager = EmojiManager()

View File

@@ -3,7 +3,7 @@ from src.common.logger import get_module_logger
import asyncio
from dataclasses import dataclass, field
from .message import MessageRecv
from ..message.message_base import BaseMessageInfo
from ..message.message_base import BaseMessageInfo, GroupInfo
import hashlib
from typing import Dict
from collections import OrderedDict
@@ -13,9 +13,10 @@ from ..config.config import global_config
logger = get_module_logger("message_buffer")
@dataclass
class CacheMessages:
message: MessageRecv
message: MessageRecv
cache_determination: asyncio.Event = field(default_factory=asyncio.Event) # 判断缓冲是否产生结果
result: str = "U"
@@ -25,22 +26,26 @@ class MessageBuffer:
self.buffer_pool: Dict[str, OrderedDict[str, CacheMessages]] = {}
self.lock = asyncio.Lock()
def get_person_id_(self, platform:str, user_id:str, group_id:str):
def get_person_id_(self, platform: str, user_id: str, group_info: GroupInfo):
"""获取唯一id"""
group_id = group_id or "私聊"
if group_info:
group_id = group_info.group_id
else:
group_id = "私聊"
key = f"{platform}_{user_id}_{group_id}"
return hashlib.md5(key.encode()).hexdigest()
async def start_caching_messages(self, message:MessageRecv):
async def start_caching_messages(self, message: MessageRecv):
"""添加消息,启动缓冲"""
if not global_config.message_buffer:
person_id = person_info_manager.get_person_id(message.message_info.user_info.platform,
message.message_info.user_info.user_id)
person_id = person_info_manager.get_person_id(
message.message_info.user_info.platform, message.message_info.user_info.user_id
)
asyncio.create_task(self.save_message_interval(person_id, message.message_info))
return
person_id_ = self.get_person_id_(message.message_info.platform,
message.message_info.user_info.user_id,
message.message_info.group_info.group_id)
person_id_ = self.get_person_id_(
message.message_info.platform, message.message_info.user_info.user_id, message.message_info.group_info
)
async with self.lock:
if person_id_ not in self.buffer_pool:
@@ -61,25 +66,24 @@ class MessageBuffer:
break
elif msg.result == "F":
recent_F_count += 1
# 判断条件最近T之后有超过3-5条F
if (recent_F_count >= random.randint(3, 5)):
if recent_F_count >= random.randint(3, 5):
new_msg = CacheMessages(message=message, result="T")
new_msg.cache_determination.set()
self.buffer_pool[person_id_][message.message_info.message_id] = new_msg
logger.debug(f"快速处理消息(已堆积{recent_F_count}条F): {message.message_info.message_id}")
return
# 添加新消息
self.buffer_pool[person_id_][message.message_info.message_id] = CacheMessages(message=message)
# 启动3秒缓冲计时器
person_id = person_info_manager.get_person_id(message.message_info.user_info.platform,
message.message_info.user_info.user_id)
person_id = person_info_manager.get_person_id(
message.message_info.user_info.platform, message.message_info.user_info.user_id
)
asyncio.create_task(self.save_message_interval(person_id, message.message_info))
asyncio.create_task(self._debounce_processor(person_id_,
message.message_info.message_id,
person_id))
asyncio.create_task(self._debounce_processor(person_id_, message.message_info.message_id, person_id))
async def _debounce_processor(self, person_id_: str, message_id: str, person_id: str):
"""等待3秒无新消息"""
@@ -89,36 +93,33 @@ class MessageBuffer:
return
interval_time = max(0.5, int(interval_time) / 1000)
await asyncio.sleep(interval_time)
async with self.lock:
if (person_id_ not in self.buffer_pool or
message_id not in self.buffer_pool[person_id_]):
if person_id_ not in self.buffer_pool or message_id not in self.buffer_pool[person_id_]:
logger.debug(f"消息已被清理msgid: {message_id}")
return
cache_msg = self.buffer_pool[person_id_][message_id]
if cache_msg.result == "U":
cache_msg.result = "T"
cache_msg.cache_determination.set()
async def query_buffer_result(self, message:MessageRecv) -> bool:
async def query_buffer_result(self, message: MessageRecv) -> bool:
"""查询缓冲结果,并清理"""
if not global_config.message_buffer:
return True
person_id_ = self.get_person_id_(message.message_info.platform,
message.message_info.user_info.user_id,
message.message_info.group_info.group_id)
person_id_ = self.get_person_id_(
message.message_info.platform, message.message_info.user_info.user_id, message.message_info.group_info
)
async with self.lock:
user_msgs = self.buffer_pool.get(person_id_, {})
cache_msg = user_msgs.get(message.message_info.message_id)
if not cache_msg:
logger.debug(f"查询异常消息不存在msgid: {message.message_info.message_id}")
return False # 消息不存在或已清理
try:
await asyncio.wait_for(cache_msg.cache_determination.wait(), timeout=10)
result = cache_msg.result == "T"
@@ -141,9 +142,8 @@ class MessageBuffer:
keep_msgs[msg_id] = msg
elif msg.result == "F":
# 收集F消息的文本内容
if (hasattr(msg.message, 'processed_plain_text')
and msg.message.processed_plain_text):
if msg.message.message_segment.type == "text":
if hasattr(msg.message, "processed_plain_text") and msg.message.processed_plain_text:
if msg.message.message_segment.type == "text":
combined_text.append(msg.message.processed_plain_text)
elif msg.message.message_segment.type != "text":
is_update = False
@@ -154,20 +154,20 @@ class MessageBuffer:
if combined_text and combined_text[0] != message.processed_plain_text and is_update:
if type == "text":
message.processed_plain_text = "".join(combined_text)
logger.debug(f"整合了{len(combined_text)-1}条F消息的内容到当前消息")
logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容到当前消息")
elif type == "emoji":
combined_text.pop()
message.processed_plain_text = "".join(combined_text)
message.is_emoji = False
logger.debug(f"整合了{len(combined_text)-1}条F消息的内容覆盖当前emoji消息")
logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容覆盖当前emoji消息")
self.buffer_pool[person_id_] = keep_msgs
return result
except asyncio.TimeoutError:
logger.debug(f"查询超时消息id {message.message_info.message_id}")
return False
async def save_message_interval(self, person_id:str, message:BaseMessageInfo):
async def save_message_interval(self, person_id: str, message: BaseMessageInfo):
message_interval_list = await person_info_manager.get_value(person_id, "msg_interval_list")
now_time_ms = int(round(time.time() * 1000))
if len(message_interval_list) < 1000:
@@ -176,12 +176,12 @@ class MessageBuffer:
message_interval_list.pop(0)
message_interval_list.append(now_time_ms)
data = {
"platform" : message.platform,
"user_id" : message.user_info.user_id,
"nickname" : message.user_info.user_nickname,
"konw_time" : int(time.time())
"platform": message.platform,
"user_id": message.user_info.user_id,
"nickname": message.user_info.user_nickname,
"konw_time": int(time.time()),
}
await person_info_manager.update_one_field(person_id, "msg_interval_list", message_interval_list, data)
message_buffer = MessageBuffer()
message_buffer = MessageBuffer()

View File

@@ -68,7 +68,8 @@ class Message_Sender:
typing_time = calculate_typing_time(
input_string=message.processed_plain_text,
thinking_start_time=message.thinking_start_time,
is_emoji=message.is_emoji)
is_emoji=message.is_emoji,
)
logger.debug(f"{message.processed_plain_text},{typing_time},计算输入时间结束")
await asyncio.sleep(typing_time)
logger.debug(f"{message.processed_plain_text},{typing_time},等待输入时间结束")
@@ -227,7 +228,7 @@ class MessageManager:
await message_earliest.process()
# print(f"message_earliest.thinking_start_tim22222e:{message_earliest.thinking_start_time}")
await message_sender.send_message(message_earliest)
await self.storage.store_message(message_earliest, message_earliest.chat_stream)

View File

@@ -42,13 +42,37 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> bool:
"""检查消息是否提到了机器人"""
keywords = [global_config.BOT_NICKNAME]
nicknames = global_config.BOT_ALIAS_NAMES
for keyword in keywords:
if keyword in message.processed_plain_text:
return True
for nickname in nicknames:
if nickname in message.processed_plain_text:
return True
return False
reply_probability = 0
is_at = False
is_mentioned = False
# 判断是否被@
if re.search(f"@[\s\S]*?id:{global_config.BOT_QQ}", message.processed_plain_text):
is_at = True
is_mentioned = True
if is_at and global_config.at_bot_inevitable_reply:
reply_probability = 1
logger.info("被@回复概率设置为100%")
else:
if not is_mentioned:
# 判断是否被回复
if re.match(f"回复[\s\S]*?\({global_config.BOT_QQ}\)的消息,说:", message.processed_plain_text):
is_mentioned = True
# 判断内容中是否被提及
message_content = re.sub(r"\@[\s\S]*?(\d+)", "", message.processed_plain_text)
message_content = re.sub(r"回复[\s\S]*?\((\d+)\)的消息,说: ", "", message_content)
for keyword in keywords:
if keyword in message_content:
is_mentioned = True
for nickname in nicknames:
if nickname in message_content:
is_mentioned = True
if is_mentioned and global_config.mentioned_bot_inevitable_reply:
reply_probability = 1
logger.info("被提及回复概率设置为100%")
return is_mentioned, reply_probability
async def get_embedding(text, request_type="embedding"):
@@ -334,7 +358,13 @@ def process_llm_response(text: str) -> List[str]:
return sentences
def calculate_typing_time(input_string: str, thinking_start_time: float, chinese_time: float = 0.2, english_time: float = 0.1, is_emoji: bool = False) -> float:
def calculate_typing_time(
input_string: str,
thinking_start_time: float,
chinese_time: float = 0.2,
english_time: float = 0.1,
is_emoji: bool = False,
) -> float:
"""
计算输入字符串所需的时间,中文和英文字符有不同的输入时间
input_string (str): 输入的字符串
@@ -368,19 +398,18 @@ def calculate_typing_time(input_string: str, thinking_start_time: float, chinese
total_time += chinese_time
else: # 其他字符(如英文)
total_time += english_time
if is_emoji:
total_time = 1
if time.time() - thinking_start_time > 10:
total_time = 1
# print(f"thinking_start_time:{thinking_start_time}")
# print(f"nowtime:{time.time()}")
# print(f"nowtime - thinking_start_time:{time.time() - thinking_start_time}")
# print(f"{total_time}")
return total_time # 加上回车时间
@@ -510,39 +539,32 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) -
try:
# 获取开始时间之前最新的一条消息
start_message = db.messages.find_one(
{
"chat_id": stream_id,
"time": {"$lte": start_time}
},
sort=[("time", -1), ("_id", -1)] # 按时间倒序_id倒序最后插入的在前
{"chat_id": stream_id, "time": {"$lte": start_time}},
sort=[("time", -1), ("_id", -1)], # 按时间倒序_id倒序最后插入的在前
)
# 获取结束时间最近的一条消息
# 先找到结束时间点的所有消息
end_time_messages = list(db.messages.find(
{
"chat_id": stream_id,
"time": {"$lte": end_time}
},
sort=[("time", -1)] # 先按时间倒序
).limit(10)) # 限制查询数量,避免性能问题
end_time_messages = list(
db.messages.find(
{"chat_id": stream_id, "time": {"$lte": end_time}},
sort=[("time", -1)], # 先按时间倒序
).limit(10)
) # 限制查询数量,避免性能问题
if not end_time_messages:
logger.warning(f"未找到结束时间 {end_time} 之前的消息")
return 0, 0
# 找到最大时间
max_time = end_time_messages[0]["time"]
# 在最大时间的消息中找最后插入的_id最大的
end_message = max(
[msg for msg in end_time_messages if msg["time"] == max_time],
key=lambda x: x["_id"]
)
end_message = max([msg for msg in end_time_messages if msg["time"] == max_time], key=lambda x: x["_id"])
if not start_message:
logger.warning(f"未找到开始时间 {start_time} 之前的消息")
return 0, 0
# 调试输出
# print("\n=== 消息范围信息 ===")
# print("Start message:", {
@@ -562,20 +584,16 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) -
# 如果结束消息的时间等于开始时间返回0
if end_message["time"] == start_message["time"]:
return 0, 0
# 获取并打印这个时间范围内的所有消息
# print("\n=== 时间范围内的所有消息 ===")
all_messages = list(db.messages.find(
{
"chat_id": stream_id,
"time": {
"$gte": start_message["time"],
"$lte": end_message["time"]
}
},
sort=[("time", 1), ("_id", 1)] # 按时间正序_id正序
))
all_messages = list(
db.messages.find(
{"chat_id": stream_id, "time": {"$gte": start_message["time"], "$lte": end_message["time"]}},
sort=[("time", 1), ("_id", 1)], # 按时间正序_id正序
)
)
count = 0
total_length = 0
for msg in all_messages:
@@ -590,10 +608,10 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) -
# "text_length": text_length,
# "_id": str(msg.get("_id"))
# })
# 如果时间不同需要把end_message本身也计入
return count - 1, total_length
except Exception as e:
logger.error(f"计算消息数量时出错: {str(e)}")
return 0, 0

View File

@@ -239,13 +239,13 @@ class ImageManager:
# 解码base64
gif_data = base64.b64decode(gif_base64)
gif = Image.open(io.BytesIO(gif_data))
# 收集所有帧
frames = []
try:
while True:
gif.seek(len(frames))
frame = gif.convert('RGB')
frame = gif.convert("RGB")
frames.append(frame.copy())
except EOFError:
pass
@@ -264,18 +264,19 @@ class ImageManager:
# 获取单帧的尺寸
frame_width, frame_height = selected_frames[0].size
# 计算目标尺寸,保持宽高比
target_height = 200 # 固定高度
target_width = int((target_height / frame_height) * frame_width)
# 调整所有帧的大小
resized_frames = [frame.resize((target_width, target_height), Image.Resampling.LANCZOS)
for frame in selected_frames]
resized_frames = [
frame.resize((target_width, target_height), Image.Resampling.LANCZOS) for frame in selected_frames
]
# 创建拼接图像
total_width = target_width * len(resized_frames)
combined_image = Image.new('RGB', (total_width, target_height))
combined_image = Image.new("RGB", (total_width, target_height))
# 水平拼接图像
for idx, frame in enumerate(resized_frames):
@@ -283,11 +284,11 @@ class ImageManager:
# 转换为base64
buffer = io.BytesIO()
combined_image.save(buffer, format='JPEG', quality=85)
result_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
combined_image.save(buffer, format="JPEG", quality=85)
result_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
return result_base64
except Exception as e:
logger.error(f"GIF转换失败: {str(e)}")
return None

View File

@@ -7,12 +7,13 @@ from datetime import datetime
logger = get_module_logger("pfc_message_processor")
class MessageProcessor:
"""消息处理器,负责处理接收到的消息并存储"""
def __init__(self):
self.storage = MessageStorage()
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
"""检查消息中是否包含过滤词"""
for word in global_config.ban_words:
@@ -34,10 +35,10 @@ class MessageProcessor:
logger.info(f"[正则表达式过滤]消息匹配到{pattern}filtered")
return True
return False
async def process_message(self, message: MessageRecv) -> None:
"""处理消息并存储
Args:
message: 消息对象
"""
@@ -55,12 +56,9 @@ class MessageProcessor:
# 存储消息
await self.storage.store_message(message, chat)
# 打印消息信息
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
# 将时间戳转换为datetime对象
current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S")
logger.info(
f"[{current_time}][{mes_name}]"
f"{chat.user_info.user_nickname}: {message.processed_plain_text}"
)
logger.info(f"[{current_time}][{mes_name}]{chat.user_info.user_nickname}: {message.processed_plain_text}")

View File

@@ -27,6 +27,7 @@ chat_config = LogConfig(
logger = get_module_logger("reasoning_chat", config=chat_config)
class ReasoningChat:
def __init__(self):
self.storage = MessageStorage()
@@ -186,7 +187,8 @@ class ReasoningChat:
logger.info("触发缓冲,已炸飞消息列")
return
is_mentioned = is_mentioned_bot_in_message(message)
# 处理提及
is_mentioned, reply_probability = is_mentioned_bot_in_message(message)
# 计算回复意愿
current_willing = willing_manager.get_willing(chat_stream=chat)
@@ -194,7 +196,7 @@ class ReasoningChat:
# 意愿激活
timer1 = time.time()
reply_probability = await willing_manager.change_reply_willing_received(
real_reply_probability = await willing_manager.change_reply_willing_received(
chat_stream=chat,
is_mentioned_bot=is_mentioned,
config=global_config,
@@ -202,6 +204,8 @@ class ReasoningChat:
interested_rate=interested_rate,
sender_id=str(message.message_info.user_info.user_id),
)
if reply_probability != 1 or (groupinfo and (groupinfo.group_id not in global_config.talk_allowed_groups)):
reply_probability = real_reply_probability
timer2 = time.time()
timing_results["意愿激活"] = timer2 - timer1
@@ -221,13 +225,13 @@ class ReasoningChat:
do_reply = False
if random() < reply_probability:
do_reply = True
# 创建思考消息
timer1 = time.time()
thinking_id = await self._create_thinking_message(message, chat, userinfo, messageinfo)
timer2 = time.time()
timing_results["创建思考消息"] = timer2 - timer1
# 生成回复
timer1 = time.time()
response_set = await self.gpt.generate_response(message)

View File

@@ -40,7 +40,7 @@ class ResponseGenerator:
async def generate_response(self, message: MessageThinking) -> Optional[Union[str, List[str]]]:
"""根据当前模型类型选择对应的生成函数"""
#从global_config中获取模型概率值并选择模型
# 从global_config中获取模型概率值并选择模型
if random.random() < global_config.MODEL_R1_PROBABILITY:
self.current_model_type = "深深地"
current_model = self.model_reasoning
@@ -51,7 +51,6 @@ class ResponseGenerator:
logger.info(
f"{self.current_model_type}思考:{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)
@@ -189,4 +188,4 @@ class ResponseGenerator:
# print(f"得到了处理后的llm返回{processed_response}")
return processed_response
return processed_response

View File

@@ -1,14 +1,12 @@
import random
import time
from typing import Optional, Union
import re
import jieba
import numpy as np
from ....common.database import db
from ...chat.utils import get_embedding, get_recent_group_detailed_plain_text, get_recent_group_speaker
from ...chat.chat_stream import chat_manager
from ...moods.moods import MoodManager
from ....individuality.individuality import Individuality
from ...memory_system.Hippocampus import HippocampusManager
from ...schedule.schedule_generator import bot_schedule
from ...config.config import global_config
@@ -26,19 +24,32 @@ class PromptBuilder:
async def _build_prompt(
self, chat_stream, message_txt: str, sender_name: str = "某人", stream_id: Optional[int] = None
) -> tuple[str, str]:
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
# 关系
who_chat_in_group = [(chat_stream.user_info.platform,
chat_stream.user_info.user_id,
chat_stream.user_info.user_nickname)]
who_chat_in_group = [
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)
]
who_chat_in_group += get_recent_group_speaker(
stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id),
limit=global_config.MAX_CONTEXT_SIZE,
)
relation_prompt = ""
for person in who_chat_in_group:
relation_prompt += await relationship_manager.build_relationship_info(person)
@@ -53,7 +64,7 @@ class PromptBuilder:
mood_prompt = mood_manager.get_prompt()
# logger.info(f"心情prompt: {mood_prompt}")
# 调取记忆
memory_prompt = ""
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
@@ -70,7 +81,7 @@ class PromptBuilder:
# print(f"相关记忆:{related_memory_info}")
# 日程构建
schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}'''
schedule_prompt = f"""你现在正在做的事情是:{bot_schedule.get_current_num_task(num=1, time_info=False)}"""
# 获取聊天上下文
chat_in_group = True
@@ -105,20 +116,6 @@ class PromptBuilder:
)
keywords_reaction_prompt += rule.get("reaction", "") + ""
# 人格选择
personality = global_config.PROMPT_PERSONALITY
probability_1 = global_config.PERSONALITY_1
probability_2 = global_config.PERSONALITY_2
personality_choice = random.random()
if personality_choice < probability_1: # 第一种风格
prompt_personality = personality[0]
elif personality_choice < probability_1 + probability_2: # 第二种风格
prompt_personality = personality[1]
else: # 第三种人格
prompt_personality = personality[2]
# 中文高手(新加的好玩功能)
prompt_ger = ""
if random.random() < 0.04:
@@ -143,7 +140,7 @@ class PromptBuilder:
涉及政治敏感以及违法违规的内容请规避。"""
logger.info("开始构建prompt")
prompt = f"""
{relation_prompt_all}
{memory_prompt}
@@ -165,7 +162,7 @@ class PromptBuilder:
start_time = time.time()
related_info = ""
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 1. 先从LLM获取主题类似于记忆系统的做法
topics = []
# try:
@@ -173,7 +170,7 @@ class PromptBuilder:
# hippocampus = HippocampusManager.get_instance()._hippocampus
# topic_num = min(5, max(1, int(len(message) * 0.1)))
# topics_response = await hippocampus.llm_topic_judge.generate_response(hippocampus.find_topic_llm(message, topic_num))
# # 提取关键词
# topics = re.findall(r"<([^>]+)>", topics_response[0])
# if not topics:
@@ -184,7 +181,7 @@ class PromptBuilder:
# for topic in ",".join(topics).replace("", ",").replace("、", ",").replace(" ", ",").split(",")
# if topic.strip()
# ]
# logger.info(f"从LLM提取的主题: {', '.join(topics)}")
# except Exception as e:
# logger.error(f"从LLM提取主题失败: {str(e)}")
@@ -192,7 +189,7 @@ class PromptBuilder:
# words = jieba.cut(message)
# topics = [word for word in words if len(word) > 1][:5]
# logger.info(f"使用jieba提取的主题: {', '.join(topics)}")
# 如果无法提取到主题,直接使用整个消息
if not topics:
logger.info("未能提取到任何主题,使用整个消息进行查询")
@@ -200,26 +197,26 @@ class PromptBuilder:
if not embedding:
logger.error("获取消息嵌入向量失败")
return ""
related_info = self.get_info_from_db(embedding, limit=3, threshold=threshold)
logger.info(f"知识库检索完成,总耗时: {time.time() - start_time:.3f}")
return related_info
# 2. 对每个主题进行知识库查询
logger.info(f"开始处理{len(topics)}个主题的知识库查询")
# 优化批量获取嵌入向量减少API调用
embeddings = {}
topics_batch = [topic for topic in topics if len(topic) > 0]
if message: # 确保消息非空
topics_batch.append(message)
# 批量获取嵌入向量
embed_start_time = time.time()
for text in topics_batch:
if not text or len(text.strip()) == 0:
continue
try:
embedding = await get_embedding(text, request_type="prompt_build")
if embedding:
@@ -228,17 +225,17 @@ class PromptBuilder:
logger.warning(f"获取'{text}'的嵌入向量失败")
except Exception as e:
logger.error(f"获取'{text}'的嵌入向量时发生错误: {str(e)}")
logger.info(f"批量获取嵌入向量完成,耗时: {time.time() - embed_start_time:.3f}")
if not embeddings:
logger.error("所有嵌入向量获取失败")
return ""
# 3. 对每个主题进行知识库查询
all_results = []
query_start_time = time.time()
# 首先添加原始消息的查询结果
if message in embeddings:
original_results = self.get_info_from_db(embeddings[message], limit=3, threshold=threshold, return_raw=True)
@@ -247,12 +244,12 @@ class PromptBuilder:
result["topic"] = "原始消息"
all_results.extend(original_results)
logger.info(f"原始消息查询到{len(original_results)}条结果")
# 然后添加每个主题的查询结果
for topic in topics:
if not topic or topic not in embeddings:
continue
try:
topic_results = self.get_info_from_db(embeddings[topic], limit=3, threshold=threshold, return_raw=True)
if topic_results:
@@ -263,9 +260,9 @@ class PromptBuilder:
logger.info(f"主题'{topic}'查询到{len(topic_results)}条结果")
except Exception as e:
logger.error(f"查询主题'{topic}'时发生错误: {str(e)}")
logger.info(f"知识库查询完成,耗时: {time.time() - query_start_time:.3f}秒,共获取{len(all_results)}条结果")
# 4. 去重和过滤
process_start_time = time.time()
unique_contents = set()
@@ -275,14 +272,16 @@ class PromptBuilder:
if content not in unique_contents:
unique_contents.add(content)
filtered_results.append(result)
# 5. 按相似度排序
filtered_results.sort(key=lambda x: x["similarity"], reverse=True)
# 6. 限制总数量最多10条
filtered_results = filtered_results[:10]
logger.info(f"结果处理完成,耗时: {time.time() - process_start_time:.3f}秒,过滤后剩余{len(filtered_results)}条结果")
logger.info(
f"结果处理完成,耗时: {time.time() - process_start_time:.3f}秒,过滤后剩余{len(filtered_results)}条结果"
)
# 7. 格式化输出
if filtered_results:
format_start_time = time.time()
@@ -292,24 +291,26 @@ class PromptBuilder:
if topic not in grouped_results:
grouped_results[topic] = []
grouped_results[topic].append(result)
# 按主题组织输出
for topic, results in grouped_results.items():
related_info += f"【主题: {topic}\n"
for i, result in enumerate(results, 1):
similarity = result["similarity"]
for _i, result in enumerate(results, 1):
_similarity = result["similarity"]
content = result["content"].strip()
# 调试:为内容添加序号和相似度信息
# related_info += f"{i}. [{similarity:.2f}] {content}\n"
related_info += f"{content}\n"
related_info += "\n"
logger.info(f"格式化输出完成,耗时: {time.time() - format_start_time:.3f}")
logger.info(f"知识库检索总耗时: {time.time() - start_time:.3f}")
return related_info
def get_info_from_db(self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False) -> Union[str, list]:
def get_info_from_db(
self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False
) -> Union[str, list]:
if not query_embedding:
return "" if not return_raw else []
# 使用余弦相似度计算

View File

@@ -28,6 +28,7 @@ chat_config = LogConfig(
logger = get_module_logger("think_flow_chat", config=chat_config)
class ThinkFlowChat:
def __init__(self):
self.storage = MessageStorage()
@@ -96,7 +97,7 @@ class ThinkFlowChat:
)
if not mark_head:
mark_head = True
# print(f"thinking_start_time:{bot_message.thinking_start_time}")
message_set.add_message(bot_message)
message_manager.add_message(message_set)
@@ -110,7 +111,7 @@ class ThinkFlowChat:
if emoji_raw:
emoji_path, description = emoji_raw
emoji_cq = image_path_to_base64(emoji_path)
# logger.info(emoji_cq)
thinking_time_point = round(message.message_info.time, 2)
@@ -130,7 +131,7 @@ class ThinkFlowChat:
is_head=False,
is_emoji=True,
)
# logger.info("22222222222222")
message_manager.add_message(bot_message)
@@ -180,7 +181,7 @@ class ThinkFlowChat:
await message.process()
logger.debug(f"消息处理成功{message.processed_plain_text}")
# 过滤词/正则表达式过滤
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
message.raw_message, chat, userinfo
@@ -190,7 +191,7 @@ class ThinkFlowChat:
await self.storage.store_message(message, chat)
logger.debug(f"存储成功{message.processed_plain_text}")
# 记忆激活
timer1 = time.time()
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
@@ -211,22 +212,21 @@ class ThinkFlowChat:
logger.info("触发缓冲,已炸飞消息列")
return
is_mentioned = is_mentioned_bot_in_message(message)
# 处理提及
is_mentioned, reply_probability = is_mentioned_bot_in_message(message)
# 计算回复意愿
current_willing_old = willing_manager.get_willing(chat_stream=chat)
# current_willing_new = (heartflow.get_subheartflow(chat.stream_id).current_state.willing - 5) / 4
# current_willing = (current_willing_old + current_willing_new) / 2
# current_willing = (current_willing_old + current_willing_new) / 2
# 有点bug
current_willing = current_willing_old
willing_manager.set_willing(chat.stream_id, current_willing)
# 意愿激活
timer1 = time.time()
reply_probability = await willing_manager.change_reply_willing_received(
real_reply_probability = await willing_manager.change_reply_willing_received(
chat_stream=chat,
is_mentioned_bot=is_mentioned,
config=global_config,
@@ -234,6 +234,8 @@ class ThinkFlowChat:
interested_rate=interested_rate,
sender_id=str(message.message_info.user_info.user_id),
)
if reply_probability != 1 or (groupinfo and (groupinfo.group_id not in global_config.talk_allowed_groups)):
reply_probability = real_reply_probability
timer2 = time.time()
timing_results["意愿激活"] = timer2 - timer1
logger.debug(f"意愿激活: {reply_probability}")
@@ -255,7 +257,7 @@ class ThinkFlowChat:
if random() < reply_probability:
try:
do_reply = True
# 创建思考消息
try:
timer1 = time.time()
@@ -264,9 +266,9 @@ class ThinkFlowChat:
timing_results["创建思考消息"] = timer2 - timer1
except Exception as e:
logger.error(f"心流创建思考消息失败: {e}")
try:
# 观察
# 观察
timer1 = time.time()
await heartflow.get_subheartflow(chat.stream_id).do_observe()
timer2 = time.time()
@@ -277,12 +279,14 @@ class ThinkFlowChat:
# 思考前脑内状态
try:
timer1 = time.time()
await heartflow.get_subheartflow(chat.stream_id).do_thinking_before_reply(message.processed_plain_text)
await heartflow.get_subheartflow(chat.stream_id).do_thinking_before_reply(
message.processed_plain_text
)
timer2 = time.time()
timing_results["思考前脑内状态"] = timer2 - timer1
except Exception as e:
logger.error(f"心流思考前脑内状态失败: {e}")
# 生成回复
timer1 = time.time()
response_set = await self.gpt.generate_response(message)

View File

@@ -35,7 +35,6 @@ class ResponseGenerator:
async def generate_response(self, message: MessageThinking) -> Optional[Union[str, List[str]]]:
"""根据当前模型类型选择对应的生成函数"""
logger.info(
f"思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
)
@@ -178,4 +177,3 @@ class ResponseGenerator:
# print(f"得到了处理后的llm返回{processed_response}")
return processed_response

View File

@@ -1,16 +1,13 @@
import random
import time
from typing import Optional
from ...memory_system.Hippocampus import HippocampusManager
from ...moods.moods import MoodManager
from ...schedule.schedule_generator import bot_schedule
from ...config.config import global_config
from ...chat.utils import get_recent_group_detailed_plain_text, get_recent_group_speaker
from ...chat.chat_stream import chat_manager
from src.common.logger import get_module_logger
from ...person_info.relationship_manager import relationship_manager
from ....individuality.individuality import Individuality
from src.heart_flow.heartflow import heartflow
logger = get_module_logger("prompt")
@@ -24,21 +21,21 @@ class PromptBuilder:
async def _build_prompt(
self, chat_stream, message_txt: str, sender_name: str = "某人", stream_id: Optional[int] = None
) -> tuple[str, str]:
current_mind_info = heartflow.get_subheartflow(stream_id).current_mind
# 开始构建prompt
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1)
prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1)
# 关系
who_chat_in_group = [(chat_stream.user_info.platform,
chat_stream.user_info.user_id,
chat_stream.user_info.user_nickname)]
who_chat_in_group = [
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)
]
who_chat_in_group += get_recent_group_speaker(
stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id),
limit=global_config.MAX_CONTEXT_SIZE,
)
relation_prompt = ""
for person in who_chat_in_group:
relation_prompt += await relationship_manager.build_relationship_info(person)
@@ -90,20 +87,6 @@ class PromptBuilder:
)
keywords_reaction_prompt += rule.get("reaction", "") + ""
# 人格选择
personality = global_config.PROMPT_PERSONALITY
probability_1 = global_config.PERSONALITY_1
probability_2 = global_config.PERSONALITY_2
personality_choice = random.random()
if personality_choice < probability_1: # 第一种风格
prompt_personality = personality[0]
elif personality_choice < probability_1 + probability_2: # 第二种风格
prompt_personality = personality[1]
else: # 第三种人格
prompt_personality = personality[2]
# 中文高手(新加的好玩功能)
prompt_ger = ""
if random.random() < 0.04:
@@ -116,7 +99,7 @@ class PromptBuilder:
涉及政治敏感以及违法违规的内容请规避。"""
logger.info("开始构建prompt")
prompt = f"""
{relation_prompt_all}\n
{chat_target}
@@ -124,82 +107,14 @@ class PromptBuilder:
你刚刚脑子里在想:
{current_mind_info}
现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n
你的网名叫{global_config.BOT_NICKNAME},有人也叫你{"/".join(global_config.BOT_ALIAS_NAMES)}{prompt_personality}
你的网名叫{global_config.BOT_NICKNAME},有人也叫你{"/".join(global_config.BOT_ALIAS_NAMES)}{prompt_personality} {prompt_identity}
你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,
尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger}
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话
请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
{moderation_prompt}不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )。"""
return prompt
def _build_initiative_prompt_select(self, group_id, probability_1=0.8, probability_2=0.1):
current_date = time.strftime("%Y-%m-%d", time.localtime())
current_time = time.strftime("%H:%M:%S", time.localtime())
bot_schedule_now_time, bot_schedule_now_activity = bot_schedule.get_current_task()
prompt_date = f"""今天是{current_date},现在是{current_time},你今天的日程是:
{bot_schedule.today_schedule}
你现在正在{bot_schedule_now_activity}
"""
chat_talking_prompt = ""
if group_id:
chat_talking_prompt = get_recent_group_detailed_plain_text(
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}")
# 获取主动发言的话题
all_nodes = HippocampusManager.get_instance().memory_graph.dots
all_nodes = filter(lambda dot: len(dot[1]["memory_items"]) > 3, all_nodes)
nodes_for_select = random.sample(all_nodes, 5)
topics = [info[0] for info in nodes_for_select]
# 激活prompt构建
activate_prompt = ""
activate_prompt = "以上是群里正在进行的聊天。"
personality = global_config.PROMPT_PERSONALITY
prompt_personality = ""
personality_choice = random.random()
if personality_choice < probability_1: # 第一种人格
prompt_personality = f"""{activate_prompt}你的网名叫{global_config.BOT_NICKNAME}{personality[0]}"""
elif personality_choice < probability_1 + probability_2: # 第二种人格
prompt_personality = f"""{activate_prompt}你的网名叫{global_config.BOT_NICKNAME}{personality[1]}"""
else: # 第三种人格
prompt_personality = f"""{activate_prompt}你的网名叫{global_config.BOT_NICKNAME}{personality[2]}"""
topics_str = ",".join(f'"{topics}"')
prompt_for_select = (
f"你现在想在群里发言,回忆了一下,想到几个话题,分别是{topics_str},综合当前状态以及群内气氛,"
f"请你在其中选择一个合适的话题,注意只需要输出话题,除了话题什么也不要输出(双引号也不要输出)"
)
prompt_initiative_select = f"{prompt_date}\n{prompt_personality}\n{prompt_for_select}"
prompt_regular = f"{prompt_date}\n{prompt_personality}"
return prompt_initiative_select, nodes_for_select, prompt_regular
def _build_initiative_prompt_check(self, selected_node, prompt_regular):
memory = random.sample(selected_node["memory_items"], 3)
memory = "\n".join(memory)
prompt_for_check = (
f"{prompt_regular}你现在想在群里发言,回忆了一下,想到一个话题,是{selected_node['concept']}"
f"关于这个话题的记忆有\n{memory}\n,以这个作为主题发言合适吗?请在把握群里的聊天内容的基础上,"
f"综合群内的氛围如果认为应该发言请输出yes否则输出no请注意是决定是否需要发言而不是编写回复内容"
f"除了yes和no不要输出任何回复内容。"
)
return prompt_for_check, memory
def _build_initiative_prompt(self, selected_node, prompt_regular, memory):
prompt_for_initiative = (
f"{prompt_regular}你现在想在群里发言,回忆了一下,想到一个话题,是{selected_node['concept']}"
f"关于这个话题的记忆有\n{memory}\n,请在把握群里的聊天内容的基础上,综合群内的氛围,"
f"以日常且口语化的口吻,简短且随意一点进行发言,不要说的太有条理,可以有个性。"
f"记住不要输出多余内容(包括前后缀,冒号和引号,括号,表情,@等)"
)
return prompt_for_initiative
prompt_builder = PromptBuilder()

View File

@@ -3,6 +3,7 @@ import tomlkit
from pathlib import Path
from datetime import datetime
def update_config():
print("开始更新配置文件...")
# 获取根目录路径
@@ -25,11 +26,11 @@ def update_config():
print(f"发现旧配置文件: {old_config_path}")
with open(old_config_path, "r", encoding="utf-8") as f:
old_config = tomlkit.load(f)
# 生成带时间戳的新文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
old_backup_path = old_config_dir / f"bot_config_{timestamp}.toml"
# 移动旧配置文件到old目录
shutil.move(old_config_path, old_backup_path)
print(f"已备份旧配置文件到: {old_backup_path}")

View File

@@ -26,8 +26,9 @@ logger = get_module_logger("config", config=config_config)
#考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = True
mai_version_main = "0.6.1"
mai_version_fix = "snapshot-2"
mai_version_main = "0.6.2"
mai_version_fix = "snapshot-1"
if mai_version_fix:
if is_test:
mai_version = f"test-{mai_version_main}-{mai_version_fix}"
@@ -39,6 +40,7 @@ else:
else:
mai_version = mai_version_main
def update_config():
# 获取根目录路径
root_dir = Path(__file__).parent.parent.parent.parent
@@ -54,7 +56,7 @@ def update_config():
# 检查配置文件是否存在
if not old_config_path.exists():
logger.info("配置文件不存在,从模板创建新配置")
#创建文件夹
# 创建文件夹
old_config_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(template_path, old_config_path)
logger.info(f"已创建新配置文件,请填写后重新运行: {old_config_path}")
@@ -84,7 +86,7 @@ def update_config():
# 生成带时间戳的新文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
old_backup_path = old_config_dir / f"bot_config_{timestamp}.toml"
# 移动旧配置文件到old目录
shutil.move(old_config_path, old_backup_path)
logger.info(f"已备份旧配置文件到: {old_backup_path}")
@@ -127,6 +129,7 @@ def update_config():
f.write(tomlkit.dumps(new_config))
logger.info("配置文件更新完成")
logger = get_module_logger("config")
@@ -148,14 +151,26 @@ class BotConfig:
ban_user_id = set()
# personality
PROMPT_PERSONALITY = [
"用一句话或几句话描述性格特点和其他特征",
"例如,是一个热爱国家热爱党的新时代好青年",
"例如,曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧",
]
PERSONALITY_1: float = 0.6 # 第一种人格概率
PERSONALITY_2: float = 0.3 # 第二种人格概率
PERSONALITY_3: float = 0.1 # 第三种人格概率
personality_core = "用一句话或几句话描述人格的核心特点" # 建议20字以内谁再写3000字小作文敲谁脑袋
personality_sides: List[str] = field(
default_factory=lambda: [
"用一句话或几句话描述人格的一些侧面",
"用一句话或几句话描述人格的一些侧面",
"用一句话或几句话描述人格的一些侧面",
]
)
# identity
identity_detail: List[str] = field(
default_factory=lambda: [
"身份特点",
"身份特点",
]
)
height: int = 170 # 身高 单位厘米
weight: int = 50 # 体重 单位千克
age: int = 20 # 年龄 单位岁
gender: str = "" # 性别
appearance: str = "用几句话描述外貌特征" # 外貌特征
# schedule
ENABLE_SCHEDULE_GEN: bool = False # 是否启用日程生成
@@ -173,20 +188,22 @@ class BotConfig:
ban_words = set()
ban_msgs_regex = set()
#heartflow
# heartflow
# enable_heartflow: bool = False # 是否启用心流
sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒
sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
sub_heart_flow_stop_time: int = 600 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
heart_flow_update_interval: int = 300 # 心流更新频率,间隔 单位秒
# willing
willing_mode: str = "classical" # 意愿模式
response_willing_amplifier: float = 1.0 # 回复意愿放大系数
response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数
down_frequency_rate: float = 3 # 降低回复频率的群组回复意愿降低系数
emoji_response_penalty: float = 0.0 # 表情包回复惩罚
mentioned_bot_inevitable_reply: bool = False # 提及 bot 必然回复
at_bot_inevitable_reply: bool = False # @bot 必然回复
# response
response_mode: str = "heart_flow" # 回复策略
@@ -344,17 +361,21 @@ class BotConfig:
"""从TOML配置文件加载配置"""
config = cls()
def personality(parent: dict):
personality_config = parent["personality"]
personality = personality_config.get("prompt_personality")
if len(personality) >= 2:
logger.info(f"载入自定义人格:{personality}")
config.PROMPT_PERSONALITY = personality_config.get("prompt_personality", config.PROMPT_PERSONALITY)
if config.INNER_VERSION in SpecifierSet(">=1.2.4"):
config.personality_core = personality_config.get("personality_core", config.personality_core)
config.personality_sides = personality_config.get("personality_sides", config.personality_sides)
config.PERSONALITY_1 = personality_config.get("personality_1_probability", config.PERSONALITY_1)
config.PERSONALITY_2 = personality_config.get("personality_2_probability", config.PERSONALITY_2)
config.PERSONALITY_3 = personality_config.get("personality_3_probability", config.PERSONALITY_3)
def identity(parent: dict):
identity_config = parent["identity"]
if config.INNER_VERSION in SpecifierSet(">=1.2.4"):
config.identity_detail = identity_config.get("identity_detail", config.identity_detail)
config.height = identity_config.get("height", config.height)
config.weight = identity_config.get("weight", config.weight)
config.age = identity_config.get("age", config.age)
config.gender = identity_config.get("gender", config.gender)
config.appearance = identity_config.get("appearance", config.appearance)
def schedule(parent: dict):
schedule_config = parent["schedule"]
@@ -403,13 +424,21 @@ class BotConfig:
config.max_response_length = response_config.get("max_response_length", config.max_response_length)
if config.INNER_VERSION in SpecifierSet(">=1.0.4"):
config.response_mode = response_config.get("response_mode", config.response_mode)
def heartflow(parent: dict):
heartflow_config = parent["heartflow"]
config.sub_heart_flow_update_interval = heartflow_config.get("sub_heart_flow_update_interval", config.sub_heart_flow_update_interval)
config.sub_heart_flow_freeze_time = heartflow_config.get("sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time)
config.sub_heart_flow_stop_time = heartflow_config.get("sub_heart_flow_stop_time", config.sub_heart_flow_stop_time)
config.heart_flow_update_interval = heartflow_config.get("heart_flow_update_interval", config.heart_flow_update_interval)
config.sub_heart_flow_update_interval = heartflow_config.get(
"sub_heart_flow_update_interval", config.sub_heart_flow_update_interval
)
config.sub_heart_flow_freeze_time = heartflow_config.get(
"sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time
)
config.sub_heart_flow_stop_time = heartflow_config.get(
"sub_heart_flow_stop_time", config.sub_heart_flow_stop_time
)
config.heart_flow_update_interval = heartflow_config.get(
"heart_flow_update_interval", config.heart_flow_update_interval
)
def willing(parent: dict):
willing_config = parent["willing"]
@@ -426,6 +455,13 @@ class BotConfig:
config.emoji_response_penalty = willing_config.get(
"emoji_response_penalty", config.emoji_response_penalty
)
if config.INNER_VERSION in SpecifierSet(">=1.2.5"):
config.mentioned_bot_inevitable_reply = willing_config.get(
"mentioned_bot_inevitable_reply", config.mentioned_bot_inevitable_reply
)
config.at_bot_inevitable_reply = willing_config.get(
"at_bot_inevitable_reply", config.at_bot_inevitable_reply
)
def model(parent: dict):
# 加载模型配置
@@ -611,6 +647,7 @@ class BotConfig:
"bot": {"func": bot, "support": ">=0.0.0"},
"groups": {"func": groups, "support": ">=0.0.0"},
"personality": {"func": personality, "support": ">=0.0.0"},
"identity": {"func": identity, "support": ">=1.2.4"},
"schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False},
"message": {"func": message, "support": ">=0.0.0"},
"willing": {"func": willing, "support": ">=0.0.9", "necessary": False},

View File

@@ -14,6 +14,7 @@ from src.common.logger import get_module_logger, LogConfig, MEMORY_STYLE_CONFIG
from src.plugins.memory_system.sample_distribution import MemoryBuildScheduler # 分布生成器
from .memory_config import MemoryConfig
def get_closest_chat_from_db(length: int, timestamp: str):
# print(f"获取最接近指定时间戳的聊天记录,长度: {length}, 时间戳: {timestamp}")
# print(f"当前时间: {timestamp},转换后时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}")

View File

@@ -179,7 +179,6 @@ class LLM_request:
# logger.debug(f"{logger_msg}发送请求到URL: {api_url}")
# logger.info(f"使用模型: {self.model_name}")
# 构建请求体
if image_base64:
payload = await self._build_payload(prompt, image_base64, image_format)
@@ -205,13 +204,17 @@ class LLM_request:
# 处理需要重试的状态码
if response.status in policy["retry_codes"]:
wait_time = policy["base_wait"] * (2**retry)
logger.warning(f"模型 {self.model_name} 错误码: {response.status}, 等待 {wait_time}秒后重试")
logger.warning(
f"模型 {self.model_name} 错误码: {response.status}, 等待 {wait_time}秒后重试"
)
if response.status == 413:
logger.warning("请求体过大,尝试压缩...")
image_base64 = compress_base64_image_by_scale(image_base64)
payload = await self._build_payload(prompt, image_base64, image_format)
elif response.status in [500, 503]:
logger.error(f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}")
logger.error(
f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
)
raise RuntimeError("服务器负载过高模型恢复失败QAQ")
else:
logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...")
@@ -219,7 +222,9 @@ class LLM_request:
await asyncio.sleep(wait_time)
continue
elif response.status in policy["abort_codes"]:
logger.error(f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}")
logger.error(
f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
)
# 尝试获取并记录服务器返回的详细错误信息
try:
error_json = await response.json()
@@ -257,7 +262,9 @@ class LLM_request:
):
old_model_name = self.model_name
self.model_name = self.model_name[4:] # 移除"Pro/"前缀
logger.warning(f"检测到403错误模型从 {old_model_name} 降级为 {self.model_name}")
logger.warning(
f"检测到403错误模型从 {old_model_name} 降级为 {self.model_name}"
)
# 对全局配置进行更新
if global_config.llm_normal.get("name") == old_model_name:
@@ -266,7 +273,9 @@ class LLM_request:
if global_config.llm_reasoning.get("name") == old_model_name:
global_config.llm_reasoning["name"] = self.model_name
logger.warning(f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}")
logger.warning(
f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}"
)
# 更新payload中的模型名
if payload and "model" in payload:
@@ -328,7 +337,14 @@ class LLM_request:
await response.release()
# 返回已经累积的内容
result = {
"choices": [{"message": {"content": accumulated_content, "reasoning_content": reasoning_content}}],
"choices": [
{
"message": {
"content": accumulated_content,
"reasoning_content": reasoning_content,
}
}
],
"usage": usage,
}
return (
@@ -345,7 +361,14 @@ class LLM_request:
logger.error(f"清理资源时发生错误: {cleanup_error}")
# 返回已经累积的内容
result = {
"choices": [{"message": {"content": accumulated_content, "reasoning_content": reasoning_content}}],
"choices": [
{
"message": {
"content": accumulated_content,
"reasoning_content": reasoning_content,
}
}
],
"usage": usage,
}
return (
@@ -360,7 +383,9 @@ class LLM_request:
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
# 构造一个伪result以便调用自定义响应处理器或默认处理器
result = {
"choices": [{"message": {"content": content, "reasoning_content": reasoning_content}}],
"choices": [
{"message": {"content": content, "reasoning_content": reasoning_content}}
],
"usage": usage,
}
return (
@@ -394,7 +419,9 @@ class LLM_request:
# 处理aiohttp抛出的响应错误
if retry < policy["max_retries"] - 1:
wait_time = policy["base_wait"] * (2**retry)
logger.error(f"模型 {self.model_name} HTTP响应错误等待{wait_time}秒后重试... 状态码: {e.status}, 错误: {e.message}")
logger.error(
f"模型 {self.model_name} HTTP响应错误等待{wait_time}秒后重试... 状态码: {e.status}, 错误: {e.message}"
)
try:
if hasattr(e, "response") and e.response and hasattr(e.response, "text"):
error_text = await e.response.text()
@@ -419,13 +446,17 @@ class LLM_request:
else:
logger.error(f"模型 {self.model_name} 服务器错误响应: {error_json}")
except (json.JSONDecodeError, TypeError) as json_err:
logger.warning(f"模型 {self.model_name} 响应不是有效的JSON: {str(json_err)}, 原始内容: {error_text[:200]}")
logger.warning(
f"模型 {self.model_name} 响应不是有效的JSON: {str(json_err)}, 原始内容: {error_text[:200]}"
)
except (AttributeError, TypeError, ValueError) as parse_err:
logger.warning(f"模型 {self.model_name} 无法解析响应错误内容: {str(parse_err)}")
await asyncio.sleep(wait_time)
else:
logger.critical(f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {e.status}, 错误: {e.message}")
logger.critical(
f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {e.status}, 错误: {e.message}"
)
# 安全地检查和记录请求详情
if (
image_base64

View File

@@ -6,6 +6,7 @@ from dataclasses import dataclass
from ..config.config import global_config
from src.common.logger import get_module_logger, LogConfig, MOOD_STYLE_CONFIG
from ..person_info.relationship_manager import relationship_manager
from src.individuality.individuality import Individuality
mood_config = LogConfig(
# 使用海马体专用样式
@@ -17,8 +18,8 @@ logger = get_module_logger("mood_manager", config=mood_config)
@dataclass
class MoodState:
valence: float # 愉悦度 (-1 到 1)
arousal: float # 唤醒度 (0 到 1)
valence: float # 愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面
arousal: float # 唤醒度 (0.0 到 1.0)0表示完全平静1表示极度兴奋
text: str # 心情文本描述
@@ -125,20 +126,48 @@ class MoodManager:
time.sleep(update_interval)
def _apply_decay(self) -> None:
"""应用情绪衰减"""
"""应用情绪衰减,正向和负向情绪分开计算"""
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1
agreeableness_bias = 0
neuroticism_factor = 0.5
# Valence 向中性0回归
valence_target = 0
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-self.decay_rate_valence * time_diff
)
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.5
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.5
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 2
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 2
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if self.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0.5)回归
arousal_target = 0.5
self.current_mood.arousal = arousal_target + (self.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
# 确保值在合理范围内
@@ -250,8 +279,9 @@ class MoodManager:
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(0.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
logger.info(f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}")
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)

View File

@@ -5,7 +5,11 @@ import hashlib
from typing import Any, Callable, Dict
import datetime
import asyncio
import numpy
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path
import pandas as pd
@@ -27,38 +31,39 @@ PersonInfoManager 类方法功能摘要:
logger = get_module_logger("person_info")
person_info_default = {
"person_id" : None,
"platform" : None,
"user_id" : None,
"nickname" : None,
"person_id": None,
"platform": None,
"user_id": None,
"nickname": None,
# "age" : 0,
"relationship_value" : 0,
"relationship_value": 0,
# "saved" : True,
# "impression" : None,
# "gender" : Unkown,
"konw_time" : 0,
"konw_time": 0,
"msg_interval": 3000,
"msg_interval_list": []
"msg_interval_list": [],
} # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项
class PersonInfoManager:
def __init__(self):
if "person_info" not in db.list_collection_names():
db.create_collection("person_info")
db.person_info.create_index("person_id", unique=True)
def get_person_id(self, platform:str, user_id:int):
def get_person_id(self, platform: str, user_id: int):
"""获取唯一id"""
components = [platform, str(user_id)]
key = "_".join(components)
return hashlib.md5(key.encode()).hexdigest()
async def create_person_info(self, person_id:str, data:dict = None):
async def create_person_info(self, person_id: str, data: dict = None):
"""创建一个项"""
if not person_id:
logger.debug("创建失败personid不存在")
return
_person_info_default = copy.deepcopy(person_info_default)
_person_info_default["person_id"] = person_id
@@ -69,19 +74,16 @@ class PersonInfoManager:
db.person_info.insert_one(_person_info_default)
async def update_one_field(self, person_id:str, field_name:str, value, Data:dict = None):
async def update_one_field(self, person_id: str, field_name: str, value, Data: dict = None):
"""更新某一个字段,会补全"""
if field_name not in person_info_default.keys():
logger.debug(f"更新'{field_name}'失败,未定义的字段")
return
document = db.person_info.find_one({"person_id": person_id})
if document:
db.person_info.update_one(
{"person_id": person_id},
{"$set": {field_name: value}}
)
db.person_info.update_one({"person_id": person_id}, {"$set": {field_name: value}})
else:
Data[field_name] = value
logger.debug(f"更新时{person_id}不存在,已新建")
@@ -104,23 +106,20 @@ class PersonInfoManager:
if not person_id:
logger.debug("get_value获取失败person_id不能为空")
return None
if field_name not in person_info_default:
logger.debug(f"get_value获取失败字段'{field_name}'未定义")
return None
document = db.person_info.find_one(
{"person_id": person_id},
{field_name: 1}
)
document = db.person_info.find_one({"person_id": person_id}, {field_name: 1})
if document and field_name in document:
return document[field_name]
else:
default_value = copy.deepcopy(person_info_default[field_name])
logger.debug(f"获取{person_id}{field_name}失败,已返回默认值{default_value}")
return default_value
async def get_values(self, person_id: str, field_names: list) -> dict:
"""获取指定person_id文档的多个字段值若不存在该字段则返回该字段的全局默认值"""
if not person_id:
@@ -136,62 +135,57 @@ class PersonInfoManager:
# 构建查询投影(所有字段都有效才会执行到这里)
projection = {field: 1 for field in field_names}
document = db.person_info.find_one(
{"person_id": person_id},
projection
)
document = db.person_info.find_one({"person_id": person_id}, projection)
result = {}
for field in field_names:
result[field] = copy.deepcopy(
document.get(field, person_info_default[field])
if document else person_info_default[field]
document.get(field, person_info_default[field]) if document else person_info_default[field]
)
return result
async def del_all_undefined_field(self):
"""删除所有项里的未定义字段"""
# 获取所有已定义的字段名
defined_fields = set(person_info_default.keys())
try:
# 遍历集合中的所有文档
for document in db.person_info.find({}):
# 找出文档中未定义的字段
undefined_fields = set(document.keys()) - defined_fields - {'_id'}
undefined_fields = set(document.keys()) - defined_fields - {"_id"}
if undefined_fields:
# 构建更新操作,使用$unset删除未定义字段
update_result = db.person_info.update_one(
{'_id': document['_id']},
{'$unset': {field: 1 for field in undefined_fields}}
{"_id": document["_id"]}, {"$unset": {field: 1 for field in undefined_fields}}
)
if update_result.modified_count > 0:
logger.debug(f"已清理文档 {document['_id']} 的未定义字段: {undefined_fields}")
return
except Exception as e:
logger.error(f"清理未定义字段时出错: {e}")
return
async def get_specific_value_list(
self,
field_name: str,
way: Callable[[Any], bool], # 接受任意类型值
) ->Dict[str, Any]:
self,
field_name: str,
way: Callable[[Any], bool], # 接受任意类型值
) -> Dict[str, Any]:
"""
获取满足条件的字段值字典
Args:
field_name: 目标字段名
way: 判断函数 (value: Any) -> bool
Returns:
{person_id: value} | {}
Example:
# 查找所有nickname包含"admin"的用户
result = manager.specific_value_list(
@@ -205,10 +199,7 @@ class PersonInfoManager:
try:
result = {}
for doc in db.person_info.find(
{field_name: {"$exists": True}},
{"person_id": 1, field_name: 1, "_id": 0}
):
for doc in db.person_info.find({field_name: {"$exists": True}}, {"person_id": 1, field_name: 1, "_id": 0}):
try:
value = doc[field_name]
if way(value):
@@ -222,11 +213,11 @@ class PersonInfoManager:
except Exception as e:
logger.error(f"数据库查询失败: {str(e)}", exc_info=True)
return {}
async def personal_habit_deduction(self):
"""启动个人信息推断,每天根据一定条件推断一次"""
try:
while(1):
while 1:
await asyncio.sleep(60)
current_time = datetime.datetime.now()
logger.info(f"个人信息推断启动: {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
@@ -234,18 +225,18 @@ class PersonInfoManager:
# "msg_interval"推断
msg_interval_map = False
msg_interval_lists = await self.get_specific_value_list(
"msg_interval_list",
lambda x: isinstance(x, list) and len(x) >= 100
"msg_interval_list", lambda x: isinstance(x, list) and len(x) >= 100
)
for person_id, msg_interval_list_ in msg_interval_lists.items():
try:
time_interval = []
for t1, t2 in zip(msg_interval_list_, msg_interval_list_[1:]):
delta = t2 - t1
if delta < 8000 and delta > 0: # 小于8秒
if delta > 0:
time_interval.append(delta)
if len(time_interval) > 30:
time_interval = [t for t in time_interval if 500 <= t <= 8000]
if len(time_interval) >= 30:
time_interval.sort()
# 画图(log)
@@ -253,40 +244,36 @@ class PersonInfoManager:
log_dir = Path("logs/person_info")
log_dir.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(10, 6))
time_series = pd.Series(time_interval)
# 绘制直方图
plt.hist(time_series, bins=50, density=True, alpha=0.4, color='pink', label='Histogram')
# 绘制KDE曲线使用相同的实际数据
time_series.plot(kind='kde', color='mediumpurple', linewidth=1, label='Density')
plt.hist(time_series, bins=50, density=True, alpha=0.4, color="pink", label="Histogram")
time_series.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density")
plt.grid(True, alpha=0.2)
plt.xlim(0, 8000)
plt.title(f"Message Interval Distribution (User: {person_id[:8]}...)")
plt.xlabel("Interval (ms)")
plt.ylabel("Density")
plt.legend(framealpha=0.9, facecolor='white')
plt.legend(framealpha=0.9, facecolor="white")
img_path = log_dir / f"interval_distribution_{person_id[:8]}.png"
plt.savefig(img_path)
plt.close()
# 画图
filtered_intervals = [t for t in time_interval if t >= 500]
if len(filtered_intervals) > 25:
msg_interval = int(round(numpy.percentile(filtered_intervals, 80)))
await self.update_one_field(person_id, "msg_interval", msg_interval)
logger.debug(f"用户{person_id}的msg_interval已经被更新为{msg_interval}")
q25, q75 = np.percentile(time_interval, [25, 75])
iqr = q75 - q25
filtered = [x for x in time_interval if (q25 - 1.5 * iqr) <= x <= (q75 + 1.5 * iqr)]
msg_interval = int(round(np.percentile(filtered, 80)))
await self.update_one_field(person_id, "msg_interval", msg_interval)
logger.debug(f"用户{person_id}的msg_interval已经被更新为{msg_interval}")
except Exception as e:
logger.debug(f"处理用户{person_id}msg_interval推断时出错: {str(e)}")
logger.debug(f"用户{person_id}消息间隔计算失败: {type(e).__name__}: {str(e)}")
continue
# 其他...
if msg_interval_map:
logger.info("已保存分布图到: logs/person_info")
current_time = datetime.datetime.now()
logger.info(f"个人信息推断结束: {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
await asyncio.sleep(86400)
@@ -294,4 +281,5 @@ class PersonInfoManager:
logger.error(f"个人信息推断运行时出错: {str(e)}")
logger.exception("详细错误信息:")
person_info_manager = PersonInfoManager()
person_info_manager = PersonInfoManager()

View File

@@ -12,6 +12,7 @@ relationship_config = LogConfig(
)
logger = get_module_logger("rel_manager", config=relationship_config)
class RelationshipManager:
def __init__(self):
self.positive_feedback_value = 0 # 正反馈系统
@@ -22,6 +23,7 @@ class RelationshipManager:
def mood_manager(self):
if self._mood_manager is None:
from ..moods.moods import MoodManager # 延迟导入
self._mood_manager = MoodManager.get_instance()
return self._mood_manager
@@ -51,27 +53,27 @@ class RelationshipManager:
self.positive_feedback_value -= 1
elif self.positive_feedback_value > 0:
self.positive_feedback_value = 0
if abs(self.positive_feedback_value) > 1:
logger.info(f"触发mood变更增益当前增益系数{self.gain_coefficient[abs(self.positive_feedback_value)]}")
def mood_feedback(self, value):
"""情绪反馈"""
mood_manager = self.mood_manager
mood_gain = (mood_manager.get_current_mood().valence) ** 2 \
* math.copysign(1, value * mood_manager.get_current_mood().valence)
mood_gain = (mood_manager.get_current_mood().valence) ** 2 * math.copysign(
1, value * mood_manager.get_current_mood().valence
)
value += value * mood_gain
logger.info(f"当前relationship增益系数{mood_gain:.3f}")
return value
def feedback_to_mood(self, mood_value):
"""对情绪的反馈"""
coefficient = self.gain_coefficient[abs(self.positive_feedback_value)]
if (mood_value > 0 and self.positive_feedback_value > 0
or mood_value < 0 and self.positive_feedback_value < 0):
return mood_value*coefficient
if mood_value > 0 and self.positive_feedback_value > 0 or mood_value < 0 and self.positive_feedback_value < 0:
return mood_value * coefficient
else:
return mood_value/coefficient
return mood_value / coefficient
async def calculate_update_relationship_value(self, chat_stream: ChatStream, label: str, stance: str) -> None:
"""计算并变更关系值
@@ -88,7 +90,7 @@ class RelationshipManager:
"中立": 1,
"反对": 2,
}
valuedict = {
"开心": 1.5,
"愤怒": -2.0,
@@ -103,10 +105,10 @@ class RelationshipManager:
person_id = person_info_manager.get_person_id(chat_stream.user_info.platform, chat_stream.user_info.user_id)
data = {
"platform" : chat_stream.user_info.platform,
"user_id" : chat_stream.user_info.user_id,
"nickname" : chat_stream.user_info.user_nickname,
"konw_time" : int(time.time())
"platform": chat_stream.user_info.platform,
"user_id": chat_stream.user_info.user_id,
"nickname": chat_stream.user_info.user_nickname,
"konw_time": int(time.time()),
}
old_value = await person_info_manager.get_value(person_id, "relationship_value")
old_value = self.ensure_float(old_value, person_id)
@@ -200,4 +202,5 @@ class RelationshipManager:
logger.warning(f"[关系管理] {person_id}值转换失败(原始值:{value}已重置为0")
return 0.0
relationship_manager = RelationshipManager()

View File

@@ -1,195 +0,0 @@
"""
The definition of artificial personality in this paper follows the dispositional para-digm and adapts a definition of
personality developed for humans [17]:
Personality for a human is the "whole and organisation of relatively stable tendencies and patterns of experience and
behaviour within one person (distinguishing it from other persons)". This definition is modified for artificial
personality:
Artificial personality describes the relatively stable tendencies and patterns of behav-iour of an AI-based machine that
can be designed by developers and designers via different modalities, such as language, creating the impression
of individuality of a humanized social agent when users interact with the machine."""
from typing import Dict, List
import json
import os
from pathlib import Path
from dotenv import load_dotenv
import sys
"""
第一种方案:基于情景评估的人格测定
"""
current_dir = Path(__file__).resolve().parent
project_root = current_dir.parent.parent.parent
env_path = project_root / ".env"
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
sys.path.append(root_path)
from src.plugins.personality.scene import get_scene_by_factor, PERSONALITY_SCENES # noqa: E402
from src.plugins.personality.questionnaire import FACTOR_DESCRIPTIONS # noqa: E402
from src.plugins.personality.offline_llm import LLMModel # noqa: E402
# 加载环境变量
if env_path.exists():
print(f"{env_path} 加载环境变量")
load_dotenv(env_path)
else:
print(f"未找到环境变量文件: {env_path}")
print("将使用默认配置")
class PersonalityEvaluator_direct:
def __init__(self):
self.personality_traits = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0}
self.scenarios = []
# 为每个人格特质获取对应的场景
for trait in PERSONALITY_SCENES:
scenes = get_scene_by_factor(trait)
if not scenes:
continue
# 从每个维度选择3个场景
import random
scene_keys = list(scenes.keys())
selected_scenes = random.sample(scene_keys, min(3, len(scene_keys)))
for scene_key in selected_scenes:
scene = scenes[scene_key]
# 为每个场景添加评估维度
# 主维度是当前特质,次维度随机选择一个其他特质
other_traits = [t for t in PERSONALITY_SCENES if t != trait]
secondary_trait = random.choice(other_traits)
self.scenarios.append(
{"场景": scene["scenario"], "评估维度": [trait, secondary_trait], "场景编号": scene_key}
)
self.llm = LLMModel()
def evaluate_response(self, scenario: str, response: str, dimensions: List[str]) -> Dict[str, float]:
"""
使用 DeepSeek AI 评估用户对特定场景的反应
"""
# 构建维度描述
dimension_descriptions = []
for dim in dimensions:
desc = FACTOR_DESCRIPTIONS.get(dim, "")
if desc:
dimension_descriptions.append(f"- {dim}{desc}")
dimensions_text = "\n".join(dimension_descriptions)
prompt = f"""请根据以下场景和用户描述评估用户在大五人格模型中的相关维度得分1-6分
场景描述:
{scenario}
用户回应:
{response}
需要评估的维度说明:
{dimensions_text}
请按照以下格式输出评估结果仅输出JSON格式
{{
"{dimensions[0]}": 分数,
"{dimensions[1]}": 分数
}}
评分标准:
1 = 非常不符合该维度特征
2 = 比较不符合该维度特征
3 = 有点不符合该维度特征
4 = 有点符合该维度特征
5 = 比较符合该维度特征
6 = 非常符合该维度特征
请根据用户的回应结合场景和维度说明进行评分。确保分数在1-6之间并给出合理的评估。"""
try:
ai_response, _ = self.llm.generate_response(prompt)
# 尝试从AI响应中提取JSON部分
start_idx = ai_response.find("{")
end_idx = ai_response.rfind("}") + 1
if start_idx != -1 and end_idx != 0:
json_str = ai_response[start_idx:end_idx]
scores = json.loads(json_str)
# 确保所有分数在1-6之间
return {k: max(1, min(6, float(v))) for k, v in scores.items()}
else:
print("AI响应格式不正确使用默认评分")
return {dim: 3.5 for dim in dimensions}
except Exception as e:
print(f"评估过程出错:{str(e)}")
return {dim: 3.5 for dim in dimensions}
def main():
print("欢迎使用人格形象创建程序!")
print("接下来您将面对一系列场景共15个。请根据您想要创建的角色形象描述在该场景下可能的反应。")
print("每个场景都会评估不同的人格维度,最终得出完整的人格特征评估。")
print("评分标准1=非常不符合2=比较不符合3=有点不符合4=有点符合5=比较符合6=非常符合")
print("\n准备好了吗?按回车键开始...")
input()
evaluator = PersonalityEvaluator_direct()
final_scores = {"开放性": 0, "严谨性": 0, "外向性": 0, "宜人性": 0, "神经质": 0}
dimension_counts = {trait: 0 for trait in final_scores.keys()}
for i, scenario_data in enumerate(evaluator.scenarios, 1):
print(f"\n场景 {i}/{len(evaluator.scenarios)} - {scenario_data['场景编号']}:")
print("-" * 50)
print(scenario_data["场景"])
print("\n请描述您的角色在这种情况下会如何反应:")
response = input().strip()
if not response:
print("反应描述不能为空!")
continue
print("\n正在评估您的描述...")
scores = evaluator.evaluate_response(scenario_data["场景"], response, scenario_data["评估维度"])
# 更新最终分数
for dimension, score in scores.items():
final_scores[dimension] += score
dimension_counts[dimension] += 1
print("\n当前评估结果:")
print("-" * 30)
for dimension, score in scores.items():
print(f"{dimension}: {score}/6")
if i < len(evaluator.scenarios):
print("\n按回车键继续下一个场景...")
input()
# 计算平均分
for dimension in final_scores:
if dimension_counts[dimension] > 0:
final_scores[dimension] = round(final_scores[dimension] / dimension_counts[dimension], 2)
print("\n最终人格特征评估结果:")
print("-" * 30)
for trait, score in final_scores.items():
print(f"{trait}: {score}/6")
print(f"测试场景数:{dimension_counts[trait]}")
# 保存结果
result = {"final_scores": final_scores, "dimension_counts": dimension_counts, "scenarios": evaluator.scenarios}
# 确保目录存在
os.makedirs("results", exist_ok=True)
# 保存到文件
with open("results/personality_result.json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print("\n结果已保存到 results/personality_result.json")
if __name__ == "__main__":
main()

View File

@@ -1,261 +0,0 @@
from typing import Dict
PERSONALITY_SCENES = {
"外向性": {
"场景1": {
"scenario": """你刚刚搬到一个新的城市工作。今天是你入职的第一天,在公司的电梯里,一位同事微笑着和你打招呼:
同事:「嗨!你是新来的同事吧?我是市场部的小林。」
同事看起来很友善,还主动介绍说:「待会午饭时间,我们部门有几个人准备一起去楼下新开的餐厅,你要一起来吗?可以认识一下其他同事。」""",
"explanation": "这个场景通过职场社交情境,观察个体对于新环境、新社交圈的态度和反应倾向。",
},
"场景2": {
"scenario": """在大学班级群里,班长发起了一个组织班级联谊活动的投票:
班长「大家好下周末我们准备举办一次班级联谊活动地点在学校附近的KTV。想请大家报名参加也欢迎大家邀请其他班级的同学
已经有几个同学在群里积极响应,有人@你问你要不要一起参加。""",
"explanation": "通过班级活动场景,观察个体对群体社交活动的参与意愿。",
},
"场景3": {
"scenario": """你在社交平台上发布了一条动态,收到了很多陌生网友的评论和私信:
网友A「你说的这个观点很有意思想和你多交流一下。」
网友B「我也对这个话题很感兴趣要不要建个群一起讨论""",
"explanation": "通过网络社交场景,观察个体对线上社交的态度。",
},
"场景4": {
"scenario": """你暗恋的对象今天主动来找你:
对方:「那个...我最近在准备一个演讲比赛,听说你口才很好。能不能请你帮我看看演讲稿,顺便给我一些建议?"""
"""如果你有时间的话,可以一起吃个饭聊聊。」""",
"explanation": "通过恋爱情境,观察个体在面对心仪对象时的社交表现。",
},
"场景5": {
"scenario": """在一次线下读书会上,主持人突然点名让你分享读后感:
主持人:「听说你对这本书很有见解,能不能和大家分享一下你的想法?」
现场有二十多个陌生的读书爱好者,都期待地看着你。""",
"explanation": "通过即兴发言场景,观察个体的社交表现欲和公众表达能力。",
},
},
"神经质": {
"场景1": {
"scenario": """你正在准备一个重要的项目演示,这关系到你的晋升机会。"""
"""就在演示前30分钟你收到了主管发来的消息
主管「临时有个变动CEO也会来听你的演示。他对这个项目特别感兴趣。」
正当你准备回复时主管又发来一条「对了能不能把演示时间压缩到15分钟CEO下午还有其他安排。你之前准备的是30分钟的版本对吧""",
"explanation": "这个场景通过突发的压力情境,观察个体在面对计划外变化时的情绪反应和调节能力。",
},
"场景2": {
"scenario": """期末考试前一天晚上,你收到了好朋友发来的消息:
好朋友:「不好意思这么晚打扰你...我看你平时成绩很好,能不能帮我解答几个问题?我真的很担心明天的考试。」
你看了看时间已经是晚上11点而你原本计划的复习还没完成。""",
"explanation": "通过考试压力场景,观察个体在时间紧张时的情绪管理。",
},
"场景3": {
"scenario": """你在社交媒体上发表的一个观点引发了争议,有不少人开始批评你:
网友A「这种观点也好意思说出来真是无知。」
网友B「建议楼主先去补补课再来发言。」
评论区里的负面评论越来越多,还有人开始人身攻击。""",
"explanation": "通过网络争议场景,观察个体面对批评时的心理承受能力。",
},
"场景4": {
"scenario": """你和恋人约好今天一起看电影,但在约定时间前半小时,对方发来消息:
恋人:「对不起,我临时有点事,可能要迟到一会儿。」
二十分钟后,对方又发来消息:「可能要再等等,抱歉!」
电影快要开始了,但对方还是没有出现。""",
"explanation": "通过恋爱情境,观察个体对不确定性的忍耐程度。",
},
"场景5": {
"scenario": """在一次重要的小组展示中,你的组员在演示途中突然卡壳了:
组员小声对你说:「我忘词了,接下来的部分是什么来着...」
台下的老师和同学都在等待,气氛有些尴尬。""",
"explanation": "通过公开场合的突发状况,观察个体的应急反应和压力处理能力。",
},
},
"严谨性": {
"场景1": {
"scenario": """你是团队的项目负责人,刚刚接手了一个为期两个月的重要项目。在第一次团队会议上:
小王:「老大,我觉得两个月时间很充裕,我们先做着看吧,遇到问题再解决。」
小张:「要不要先列个时间表?不过感觉太详细的计划也没必要,点到为止就行。」
小李:「客户那边说如果能提前完成有奖励,我觉得我们可以先做快一点的部分。」""",
"explanation": "这个场景通过项目管理情境,体现个体在工作方法、计划性和责任心方面的特征。",
},
"场景2": {
"scenario": """期末小组作业,组长让大家分工完成一份研究报告。在截止日期前三天:
组员A「我的部分大概写完了感觉还行。」
组员B「我这边可能还要一天才能完成最近太忙了。」
组员C发来一份没有任何引用出处、可能存在抄袭的内容「我写完了你们看看怎么样""",
"explanation": "通过学习场景,观察个体对学术规范和质量要求的重视程度。",
},
"场景3": {
"scenario": """你在一个兴趣小组的群聊中,大家正在讨论举办一次线下活动:
成员A「到时候见面就知道具体怎么玩了
成员B「对啊随意一点挺好的。」
成员C「人来了自然就热闹了。」""",
"explanation": "通过活动组织场景,观察个体对活动计划的态度。",
},
"场景4": {
"scenario": """你和恋人计划一起去旅游,对方说:
恋人:「我们就随心而行吧!订个目的地,其他的到了再说,这样更有意思。」
距离出发还有一周时间,但机票、住宿和具体行程都还没有确定。""",
"explanation": "通过旅行规划场景,观察个体的计划性和对不确定性的接受程度。",
},
"场景5": {
"scenario": """在一个重要的团队项目中,你发现一个同事的工作存在明显错误:
同事:「差不多就行了,反正领导也看不出来。」
这个错误可能不会立即造成问题,但长期来看可能会影响项目质量。""",
"explanation": "通过工作质量场景,观察个体对细节和标准的坚持程度。",
},
},
"开放性": {
"场景1": {
"scenario": """周末下午,你的好友小美兴致勃勃地给你打电话:
小美:「我刚发现一个特别有意思的沉浸式艺术展!不是传统那种挂画的展览,而是把整个空间都变成了艺术品。"""
"""观众要穿特制的服装还要带上VR眼镜好像还有AI实时互动
小美继续说:「虽然票价不便宜,但听说体验很独特。网上评价两极分化,有人说是前所未有的艺术革新,也有人说是哗众取宠。"""
"""要不要周末一起去体验一下?」""",
"explanation": "这个场景通过新型艺术体验,反映个体对创新事物的接受程度和尝试意愿。",
},
"场景2": {
"scenario": """在一节创意写作课上,老师提出了一个特别的作业:
老师「下周的作业是用AI写作工具协助创作一篇小说。你们可以自由探索如何与AI合作打破传统写作方式。」
班上随即展开了激烈讨论,有人认为这是对创作的亵渎,也有人对这种新形式感到兴奋。""",
"explanation": "通过新技术应用场景,观察个体对创新学习方式的态度。",
},
"场景3": {
"scenario": """在社交媒体上,你看到一个朋友分享了一种新的生活方式:
「最近我在尝试'数字游牧'生活,就是一边远程工作一边环游世界。"""
"""没有固定住所,住青旅或短租,认识来自世界各地的朋友。虽然有时会很不稳定,但这种自由的生活方式真的很棒!」
评论区里争论不断,有人向往这种生活,也有人觉得太冒险。""",
"explanation": "通过另类生活方式,观察个体对非传统选择的态度。",
},
"场景4": {
"scenario": """你的恋人突然提出了一个想法:
恋人:「我们要不要尝试一下开放式关系?就是在保持彼此关系的同时,也允许和其他人发展感情。现在国外很多年轻人都这样。」
这个提议让你感到意外,你之前从未考虑过这种可能性。""",
"explanation": "通过感情观念场景,观察个体对非传统关系模式的接受度。",
},
"场景5": {
"scenario": """在一次朋友聚会上,大家正在讨论未来职业规划:
朋友A「我准备辞职去做自媒体专门介绍一些小众的文化和艺术。」
朋友B「我想去学习生物科技准备转行做人造肉研发。」
朋友C「我在考虑加入一个区块链创业项目虽然风险很大。」""",
"explanation": "通过职业选择场景,观察个体对新兴领域的探索意愿。",
},
},
"宜人性": {
"场景1": {
"scenario": """在回家的公交车上,你遇到这样一幕:
一位老奶奶颤颤巍巍地上了车,车上座位已经坐满了。她站在你旁边,看起来很疲惫。这时你听到前排两个年轻人的对话:
年轻人A「那个老太太好像站不稳看起来挺累的。」
年轻人B「现在的老年人真是...我看她包里还有菜,肯定是去菜市场买完菜回来的,这么多人都不知道叫子女开车接送。」
就在这时,老奶奶一个趔趄,差点摔倒。她扶住了扶手,但包里的东西洒了一些出来。""",
"explanation": "这个场景通过公共场合的助人情境,体现个体的同理心和对他人需求的关注程度。",
},
"场景2": {
"scenario": """在班级群里,有同学发起为生病住院的同学捐款:
同学A「大家好小林最近得了重病住院医药费很贵家里负担很重。我们要不要一起帮帮他
同学B「我觉得这是他家里的事我们不方便参与吧。」
同学C「但是都是同学一场帮帮忙也是应该的。」""",
"explanation": "通过同学互助场景,观察个体的助人意愿和同理心。",
},
"场景3": {
"scenario": """在一个网络讨论组里,有人发布了求助信息:
求助者:「最近心情很低落,感觉生活很压抑,不知道该怎么办...」
评论区里已经有一些回复:
「生活本来就是这样,想开点!」
「你这样子太消极了,要积极面对。」
「谁还没点烦心事啊,过段时间就好了。」""",
"explanation": "通过网络互助场景,观察个体的共情能力和安慰方式。",
},
"场景4": {
"scenario": """你的恋人向你倾诉工作压力:
恋人:「最近工作真的好累,感觉快坚持不下去了...」
但今天你也遇到了很多烦心事,心情也不太好。""",
"explanation": "通过感情关系场景,观察个体在自身状态不佳时的关怀能力。",
},
"场景5": {
"scenario": """在一次团队项目中,新来的同事小王因为经验不足,造成了一个严重的错误。在部门会议上:
主管:「这个错误造成了很大的损失,是谁负责的这部分?」
小王看起来很紧张,欲言又止。你知道是他造成的错误,同时你也是这个项目的共同负责人。""",
"explanation": "通过职场情境,观察个体在面对他人过错时的态度和处理方式。",
},
},
}
def get_scene_by_factor(factor: str) -> Dict:
"""
根据人格因子获取对应的情景测试
Args:
factor (str): 人格因子名称
Returns:
Dict: 包含情景描述的字典
"""
return PERSONALITY_SCENES.get(factor, None)
def get_all_scenes() -> Dict:
"""
获取所有情景测试
Returns:
Dict: 所有情景测试的字典
"""
return PERSONALITY_SCENES

View File

@@ -0,0 +1,142 @@
# 人格测试问卷题目
# 王孟成, 戴晓阳, & 姚树桥. (2011).
# 中国大五人格问卷的初步编制Ⅲ:简式版的制定及信效度检验. 中国临床心理学杂志, 19(04), Article 04.
# 王孟成, 戴晓阳, & 姚树桥. (2010).
# 中国大五人格问卷的初步编制Ⅰ:理论框架与信度分析. 中国临床心理学杂志, 18(05), Article 05.
PERSONALITY_QUESTIONS = [
# 神经质维度 (F1)
{"id": 1, "content": "我常担心有什么不好的事情要发生", "factor": "神经质", "reverse_scoring": False},
{"id": 2, "content": "我常感到害怕", "factor": "神经质", "reverse_scoring": False},
{"id": 3, "content": "有时我觉得自己一无是处", "factor": "神经质", "reverse_scoring": False},
{"id": 4, "content": "我很少感到忧郁或沮丧", "factor": "神经质", "reverse_scoring": True},
{"id": 5, "content": "别人一句漫不经心的话,我常会联系在自己身上", "factor": "神经质", "reverse_scoring": False},
{"id": 6, "content": "在面对压力时,我有种快要崩溃的感觉", "factor": "神经质", "reverse_scoring": False},
{"id": 7, "content": "我常担忧一些无关紧要的事情", "factor": "神经质", "reverse_scoring": False},
{"id": 8, "content": "我常常感到内心不踏实", "factor": "神经质", "reverse_scoring": False},
# 严谨性维度 (F2)
{"id": 9, "content": "在工作上,我常只求能应付过去便可", "factor": "严谨性", "reverse_scoring": True},
{"id": 10, "content": "一旦确定了目标,我会坚持努力地实现它", "factor": "严谨性", "reverse_scoring": False},
{"id": 11, "content": "我常常是仔细考虑之后才做出决定", "factor": "严谨性", "reverse_scoring": False},
{"id": 12, "content": "别人认为我是个慎重的人", "factor": "严谨性", "reverse_scoring": False},
{"id": 13, "content": "做事讲究逻辑和条理是我的一个特点", "factor": "严谨性", "reverse_scoring": False},
{"id": 14, "content": "我喜欢一开头就把事情计划好", "factor": "严谨性", "reverse_scoring": False},
{"id": 15, "content": "我工作或学习很勤奋", "factor": "严谨性", "reverse_scoring": False},
{"id": 16, "content": "我是个倾尽全力做事的人", "factor": "严谨性", "reverse_scoring": False},
# 宜人性维度 (F3)
{
"id": 17,
"content": "尽管人类社会存在着一些阴暗的东西(如战争、罪恶、欺诈),我仍然相信人性总的来说是善良的",
"factor": "宜人性",
"reverse_scoring": False,
},
{"id": 18, "content": "我觉得大部分人基本上是心怀善意的", "factor": "宜人性", "reverse_scoring": False},
{"id": 19, "content": "虽然社会上有骗子,但我觉得大部分人还是可信的", "factor": "宜人性", "reverse_scoring": False},
{"id": 20, "content": "我不太关心别人是否受到不公正的待遇", "factor": "宜人性", "reverse_scoring": True},
{"id": 21, "content": "我时常觉得别人的痛苦与我无关", "factor": "宜人性", "reverse_scoring": True},
{"id": 22, "content": "我常为那些遭遇不幸的人感到难过", "factor": "宜人性", "reverse_scoring": False},
{"id": 23, "content": "我是那种只照顾好自己,不替别人担忧的人", "factor": "宜人性", "reverse_scoring": True},
{"id": 24, "content": "当别人向我诉说不幸时,我常感到难过", "factor": "宜人性", "reverse_scoring": False},
# 开放性维度 (F4)
{"id": 25, "content": "我的想象力相当丰富", "factor": "开放性", "reverse_scoring": False},
{"id": 26, "content": "我头脑中经常充满生动的画面", "factor": "开放性", "reverse_scoring": False},
{"id": 27, "content": "我对许多事情有着很强的好奇心", "factor": "开放性", "reverse_scoring": False},
{"id": 28, "content": "我喜欢冒险", "factor": "开放性", "reverse_scoring": False},
{"id": 29, "content": "我是个勇于冒险,突破常规的人", "factor": "开放性", "reverse_scoring": False},
{"id": 30, "content": "我身上具有别人没有的冒险精神", "factor": "开放性", "reverse_scoring": False},
{
"id": 31,
"content": "我渴望学习一些新东西,即使它们与我的日常生活无关",
"factor": "开放性",
"reverse_scoring": False,
},
{
"id": 32,
"content": "我很愿意也很容易接受那些新事物、新观点、新想法",
"factor": "开放性",
"reverse_scoring": False,
},
# 外向性维度 (F5)
{"id": 33, "content": "我喜欢参加社交与娱乐聚会", "factor": "外向性", "reverse_scoring": False},
{"id": 34, "content": "我对人多的聚会感到乏味", "factor": "外向性", "reverse_scoring": True},
{"id": 35, "content": "我尽量避免参加人多的聚会和嘈杂的环境", "factor": "外向性", "reverse_scoring": True},
{"id": 36, "content": "在热闹的聚会上,我常常表现主动并尽情玩耍", "factor": "外向性", "reverse_scoring": False},
{"id": 37, "content": "有我在的场合一般不会冷场", "factor": "外向性", "reverse_scoring": False},
{"id": 38, "content": "我希望成为领导者而不是被领导者", "factor": "外向性", "reverse_scoring": False},
{"id": 39, "content": "在一个团体中,我希望处于领导地位", "factor": "外向性", "reverse_scoring": False},
{"id": 40, "content": "别人多认为我是一个热情和友好的人", "factor": "外向性", "reverse_scoring": False},
]
# 因子维度说明
FACTOR_DESCRIPTIONS = {
"外向性": {
"description": "反映个体神经系统的强弱和动力特征。外向性主要表现为个体在人际交往和社交活动中的倾向性,"
"包括对社交活动的兴趣、"
"对人群的态度、社交互动中的主动程度以及在群体中的影响力。高分者倾向于积极参与社交活动,乐于与人交往,善于表达自我,"
"并往往在群体中发挥领导作用;低分者则倾向于独处,不喜欢热闹的社交场合,表现出内向、安静的特征。",
"trait_words": ["热情", "活力", "社交", "主动"],
"subfactors": {
"合群性": "个体愿意与他人聚在一起,即接近人群的倾向;高分表现乐群、好交际,低分表现封闭、独处",
"热情": "个体对待别人时所表现出的态度;高分表现热情好客,低分表现冷淡",
"支配性": "个体喜欢指使、操纵他人,倾向于领导别人的特点;高分表现好强、发号施令,低分表现顺从、低调",
"活跃": "个体精力充沛,活跃、主动性等特点;高分表现活跃,低分表现安静",
},
},
"神经质": {
"description": "反映个体情绪的状态和体验内心苦恼的倾向性。这个维度主要关注个体在面对压力、"
"挫折和日常生活挑战时的情绪稳定性和适应能力。它包含了对焦虑、抑郁、愤怒等负面情绪的敏感程度,"
"以及个体对这些情绪的调节和控制能力。高分者容易体验负面情绪,对压力较为敏感,情绪波动较大;"
"低分者则表现出较强的情绪稳定性,能够较好地应对压力和挫折。",
"trait_words": ["稳定", "沉着", "从容", "坚韧"],
"subfactors": {
"焦虑": "个体体验焦虑感的个体差异;高分表现坐立不安,低分表现平静",
"抑郁": "个体体验抑郁情感的个体差异;高分表现郁郁寡欢,低分表现平静",
"敏感多疑": "个体常常关注自己的内心活动,行为和过于意识人对自己的看法、评价;高分表现敏感多疑,"
"低分表现淡定、自信",
"脆弱性": "个体在危机或困难面前无力、脆弱的特点;高分表现无能、易受伤、逃避,低分表现坚强",
"愤怒-敌意": "个体准备体验愤怒,及相关情绪的状态;高分表现暴躁易怒,低分表现平静",
},
},
"严谨性": {
"description": "反映个体在目标导向行为上的组织、坚持和动机特征。这个维度体现了个体在工作、"
"学习等目标性活动中的自我约束和行为管理能力。它涉及到个体的责任感、自律性、计划性、条理性以及完成任务的态度。"
"高分者往往表现出强烈的责任心、良好的组织能力、谨慎的决策风格和持续的努力精神;低分者则可能表现出随意性强、"
"缺乏规划、做事马虎或易放弃的特点。",
"trait_words": ["负责", "自律", "条理", "勤奋"],
"subfactors": {
"责任心": "个体对待任务和他人认真负责,以及对自己承诺的信守;高分表现有责任心、负责任,"
"低分表现推卸责任、逃避处罚",
"自我控制": "个体约束自己的能力,及自始至终的坚持性;高分表现自制、有毅力,低分表现冲动、无毅力",
"审慎性": "个体在采取具体行动前的心理状态;高分表现谨慎、小心,低分表现鲁莽、草率",
"条理性": "个体处理事务和工作的秩序,条理和逻辑性;高分表现整洁、有秩序,低分表现混乱、遗漏",
"勤奋": "个体工作和学习的努力程度及为达到目标而表现出的进取精神;高分表现勤奋、刻苦,低分表现懒散",
},
},
"开放性": {
"description": "反映个体对新异事物、新观念和新经验的接受程度,以及在思维和行为方面的创新倾向。"
"这个维度体现了个体在认知和体验方面的广度、深度和灵活性。它包括对艺术的欣赏能力、对知识的求知欲、想象力的丰富程度,"
"以及对冒险和创新的态度。高分者往往具有丰富的想象力、广泛的兴趣、开放的思维方式和创新的倾向;低分者则倾向于保守、"
"传统,喜欢熟悉和常规的事物。",
"trait_words": ["创新", "好奇", "艺术", "冒险"],
"subfactors": {
"幻想": "个体富于幻想和想象的水平;高分表现想象力丰富,低分表现想象力匮乏",
"审美": "个体对于艺术和美的敏感与热爱程度;高分表现富有艺术气息,低分表现一般对艺术不敏感",
"好奇心": "个体对未知事物的态度;高分表现兴趣广泛、好奇心浓,低分表现兴趣少、无好奇心",
"冒险精神": "个体愿意尝试有风险活动的个体差异;高分表现好冒险,低分表现保守",
"价值观念": "个体对新事物、新观念、怪异想法的态度;高分表现开放、坦然接受新事物,低分则相反",
},
},
"宜人性": {
"description": "反映个体在人际关系中的亲和倾向,体现了对他人的关心、同情和合作意愿。"
"这个维度主要关注个体与他人互动时的态度和行为特征,包括对他人的信任程度、同理心水平、"
"助人意愿以及在人际冲突中的处理方式。高分者通常表现出友善、富有同情心、乐于助人的特质,善于与他人建立和谐关系;"
"低分者则可能表现出较少的人际关注,在社交互动中更注重自身利益,较少考虑他人感受。",
"trait_words": ["友善", "同理", "信任", "合作"],
"subfactors": {
"信任": "个体对他人和/或他人言论的相信程度;高分表现信任他人,低分表现怀疑",
"体贴": "个体对别人的兴趣和需要的关注程度;高分表现体贴、温存,低分表现冷漠、不在乎",
"同情": "个体对处于不利地位的人或物的态度;高分表现富有同情心,低分表现冷漠",
},
},
}

View File

@@ -14,7 +14,7 @@ from src.common.logger import get_module_logger, SCHEDULE_STYLE_CONFIG, LogConfi
from src.plugins.models.utils_model import LLM_request # noqa: E402
from src.plugins.config.config import global_config # noqa: E402
TIME_ZONE = tz.gettz(global_config.TIME_ZONE) # 设置时区
TIME_ZONE = tz.gettz(global_config.TIME_ZONE) # 设置时区
schedule_config = LogConfig(
@@ -31,10 +31,16 @@ class ScheduleGenerator:
def __init__(self):
# 使用离线LLM模型
self.llm_scheduler_all = LLM_request(
model=global_config.llm_reasoning, temperature=global_config.SCHEDULE_TEMPERATURE, max_tokens=7000, request_type="schedule"
model=global_config.llm_reasoning,
temperature=global_config.SCHEDULE_TEMPERATURE,
max_tokens=7000,
request_type="schedule",
)
self.llm_scheduler_doing = LLM_request(
model=global_config.llm_normal, temperature=global_config.SCHEDULE_TEMPERATURE, max_tokens=2048, request_type="schedule"
model=global_config.llm_normal,
temperature=global_config.SCHEDULE_TEMPERATURE,
max_tokens=2048,
request_type="schedule",
)
self.today_schedule_text = ""
@@ -62,9 +68,7 @@ class ScheduleGenerator:
self.name = name
self.behavior = behavior
self.schedule_doing_update_interval = interval
for pers in personality:
self.personality += pers + "\n"
self.personality = personality
async def mai_schedule_start(self):
"""启动日程系统每5分钟执行一次move_doing并在日期变化时重新检查日程"""

View File

@@ -2,7 +2,7 @@ import threading
import time
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Dict
from typing import Any, Dict, List
from src.common.logger import get_module_logger
from ...common.database import db
@@ -22,6 +22,7 @@ class LLMStatistics:
self.stats_thread = None
self.console_thread = None
self._init_database()
self.name_dict: Dict[List] = {}
def _init_database(self):
"""初始化数据库集合"""
@@ -137,16 +138,24 @@ class LLMStatistics:
# user_id = str(doc.get("user_info", {}).get("user_id", "unknown"))
chat_info = doc.get("chat_info", {})
user_info = doc.get("user_info", {})
message_time = doc.get("time", 0)
group_info = chat_info.get("group_info") if chat_info else {}
# print(f"group_info: {group_info}")
group_name = None
if group_info:
group_id = f"g{group_info.get('group_id')}"
group_name = group_info.get("group_name", f"{group_info.get('group_id')}")
if user_info and not group_name:
group_id = f"u{user_info['user_id']}"
group_name = user_info["user_nickname"]
if self.name_dict.get(group_id):
if message_time > self.name_dict.get(group_id)[1]:
self.name_dict[group_id] = [group_name, message_time]
else:
self.name_dict[group_id] = [group_name, message_time]
# print(f"group_name: {group_name}")
stats["messages_by_user"][user_id] += 1
stats["messages_by_chat"][group_name] += 1
stats["messages_by_chat"][group_id] += 1
return stats
@@ -187,7 +196,7 @@ class LLMStatistics:
tokens = stats["tokens_by_model"][model_name]
cost = stats["costs_by_model"][model_name]
output.append(
data_fmt.format(model_name[:32] + ".." if len(model_name) > 32 else model_name, count, tokens, cost)
data_fmt.format(model_name[:30] + ".." if len(model_name) > 32 else model_name, count, tokens, cost)
)
output.append("")
@@ -221,8 +230,8 @@ class LLMStatistics:
# 添加聊天统计
output.append("群组统计:")
output.append(("群组名称 消息数量"))
for group_name, count in sorted(stats["messages_by_chat"].items()):
output.append(f"{group_name[:32]:<32} {count:>10}")
for group_id, count in sorted(stats["messages_by_chat"].items()):
output.append(f"{self.name_dict[group_id][0][:32]:<32} {count:>10}")
return "\n".join(output)
@@ -250,7 +259,7 @@ class LLMStatistics:
tokens = stats["tokens_by_model"][model_name]
cost = stats["costs_by_model"][model_name]
output.append(
data_fmt.format(model_name[:32] + ".." if len(model_name) > 32 else model_name, count, tokens, cost)
data_fmt.format(model_name[:30] + ".." if len(model_name) > 32 else model_name, count, tokens, cost)
)
output.append("")
@@ -284,8 +293,8 @@ class LLMStatistics:
# 添加聊天统计
output.append("群组统计:")
output.append(("群组名称 消息数量"))
for group_name, count in sorted(stats["messages_by_chat"].items()):
output.append(f"{group_name[:32]:<32} {count:>10}")
for group_id, count in sorted(stats["messages_by_chat"].items()):
output.append(f"{self.name_dict[group_id][0][:32]:<32} {count:>10}")
return "\n".join(output)

View File

@@ -158,12 +158,12 @@ class WillingManager:
logger.debug(f"被提及, 当前意愿: {current_willing}")
if is_emoji:
current_willing *= 0.1
current_willing = global_config.emoji_response_penalty * 0.1
logger.debug(f"表情包, 当前意愿: {current_willing}")
# 根据话题兴趣度适当调整
if interested_rate > 0.5:
current_willing += (interested_rate - 0.5) * 0.5
current_willing += (interested_rate - 0.5) * 0.5 * global_config.response_interested_rate_amplifier
# 根据当前模式计算回复概率
base_probability = 0.0
@@ -180,7 +180,7 @@ class WillingManager:
base_probability = 0.30 if msg_count >= 15 else 0.03 * min(msg_count, 10)
# 考虑回复意愿的影响
reply_probability = base_probability * current_willing
reply_probability = base_probability * current_willing * global_config.response_willing_amplifier
# 检查群组权限(如果是群聊)
if chat_stream.group_info and config:

View File

@@ -53,18 +53,18 @@ class KnowledgeLibrary:
# 按空行分割内容
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
chunks = []
for para in paragraphs:
para_length = len(para)
# 如果段落长度小于等于最大长度,直接添加
if para_length <= max_length:
chunks.append(para)
else:
# 如果段落超过最大长度,则按最大长度切分
for i in range(0, para_length, max_length):
chunks.append(para[i:i + max_length])
chunks.append(para[i : i + max_length])
return chunks
def get_embedding(self, text: str) -> list:

1249
temp_utils_ui/temp_ui.py Normal file

File diff suppressed because it is too large Load Diff

View File

View File

@@ -1,5 +1,5 @@
[inner]
version = "1.2.4"
version = "1.2.5"
#以下是给开发人员阅读的,一般用户不需要阅读
@@ -33,15 +33,28 @@ talk_allowed = [
talk_frequency_down = [] #降低回复频率的群号码
ban_user_id = [] #禁止回复和读取消息的QQ号
[personality]
prompt_personality = [
"用一句话或几句话描述性格特点和其他特征",
"例如,是一个热爱国家热爱党的新时代好青年",
"例如,曾经是一个学习地质的女大学生,现在学习心理学和脑科学,你会刷贴吧"
]
personality_1_probability = 0.7 # 第一种人格出现概率
personality_2_probability = 0.2 # 第二种人格出现概率可以为0
personality_3_probability = 0.1 # 第三种人格出现概率请确保三个概率相加等于1
[personality] #未完善
personality_core = "用一句话或几句话描述人格的核心特点" # 建议20字以内谁再写3000字小作文敲谁脑袋
personality_sides = [
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
]# 条数任意
[identity] #アイデンティティがない 生まれないらららら
# 兴趣爱好 未完善,有些条目未使用
identity_detail = [
"身份特点",
"身份特点",
]# 条数任意
#外貌特征
height = 170 # 身高 单位厘米
weight = 50 # 体重 单位千克
age = 20 # 年龄 单位岁
gender = "男" # 性别
appearance = "用几句话描述外貌特征" # 外貌特征
[schedule]
enable_schedule_gen = true # 是否启用日程表(尚未完成)
@@ -60,7 +73,7 @@ response_mode = "heart_flow" # 回复策略可选值heart_flow心流
model_r1_probability = 0.7 # 麦麦回答时选择主要回复模型1 模型的概率
model_v3_probability = 0.3 # 麦麦回答时选择次要回复模型2 模型的概率
[heartflow] # 注意可能会消耗大量token请谨慎开启
[heartflow] # 注意可能会消耗大量token请谨慎开启仅会使用v3模型
sub_heart_flow_update_interval = 60 # 子心流更新频率,间隔 单位秒
sub_heart_flow_freeze_time = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
sub_heart_flow_stop_time = 600 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
@@ -86,13 +99,14 @@ ban_msgs_regex = [
[willing]
willing_mode = "classical" # 回复意愿模式 经典模式
# willing_mode = "dynamic" # 动态模式(可能不兼容)
# willing_mode = "dynamic" # 动态模式(不兼容,需要维护)
# willing_mode = "custom" # 自定义模式(可自行调整
response_willing_amplifier = 1 # 麦麦回复意愿放大系数一般为1
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数
down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法
emoji_response_penalty = 0.1 # 表情包回复惩罚系数设为0为不回复单个表情包减少单独回复表情包的概率
mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复
[emoji]
max_emoji_num = 120 # 表情包最大数量
@@ -152,7 +166,7 @@ enable = true
[experimental]
enable_friend_chat = false # 是否启用好友聊天
pfc_chatting = false # 是否启用PFC聊天
pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
#下面的模型若使用硅基流动则不需要更改使用ds官方则改成.env自定义的宏使用自定义模型则选择定位相似的模型自己填写
#推理模型

View File

@@ -0,0 +1 @@
该版本变动了人格相关设置,原有的配置内容可能被自动更新,如果你没有备份,可以在\config\old找回

View File

@@ -0,0 +1,56 @@
@echo off
chcp 65001 > nul
setlocal enabledelayedexpansion
cd /d %~dp0
title 麦麦人格生成
cls
echo ======================================
echo 警告提示
echo ======================================
echo 1.这是一个demo系统,仅供体验,特性可能会在将来移除
echo ======================================
echo.
echo ======================================
echo 请选择Python环境:
echo 1 - venv (推荐)
echo 2 - conda
echo ======================================
choice /c 12 /n /m "请输入数字选择(1或2): "
if errorlevel 2 (
echo ======================================
set "CONDA_ENV="
set /p CONDA_ENV="请输入要激活的 conda 环境名称: "
:: 检查输入是否为空
if "!CONDA_ENV!"=="" (
echo 错误:环境名称不能为空
pause
exit /b 1
)
call conda activate !CONDA_ENV!
if errorlevel 1 (
echo 激活 conda 环境失败
pause
exit /b 1
)
echo Conda 环境 "!CONDA_ENV!" 激活成功
python src/individuality/per_bf_gen.py
) else (
if exist "venv\Scripts\python.exe" (
venv\Scripts\python src/individuality/per_bf_gen.py
) else (
echo ======================================
echo 错误: venv环境不存在请先创建虚拟环境
pause
exit /b 1
)
)
endlocal
pause