pull/1001/head
SnowindMe 2025-05-29 00:42:40 +08:00
commit 3355a8a231
81 changed files with 2626 additions and 2053 deletions

View File

@ -2,13 +2,14 @@
## [0.7.0] -2025-6-1
- 重构数据库弃用MongoDB采用轻量sqlite,无需额外安装
- 重构HFC可扩展的聊天模式
- 重构HFC可扩展的聊天模式支持独立的表达模式
- HFC丰富HFC的决策信息更好的把握聊天内容
- HFC初步支持插件v0.1(测试版)
- 重构表情包模块
- 移除日程系统
**重构专注聊天(HFC)**
- 模块化HFC,可以自定义不同的部件
**重构专注聊天(HFC - focus_chat)**
- 模块化设计,可以自定义不同的部件
- 观察器(获取信息)
- 信息处理器(处理信息)
- 重构:聊天思考(子心流)处理器
@ -26,30 +27,44 @@
- 插件:禁言动作
- 表达器:装饰语言风格
- 可通过插件添加和自定义HFC部件目前只支持action定义
- 为专注模式添加关系线索
- 在专注模式下麦麦可以决定自行发送语音消息需要搭配tts适配器
- 优化reply减少复读
**优化普通聊天(normal_chat)**
- 增加了talk_frequency参数来有效控制回复频率
- 优化了进入和离开normal_chat的方式
**新增表达方式学习**
- 麦麦配置单独表达方式
- 自主学习群聊中的表达方式,更贴近群友
- 可自定义的学习频率和开关
- 根据人设生成额外的表达方式
**聊天管理**
- 移除不在线状态
- 优化自动模式下normal与focus聊天的切换机制
- 大幅精简聊天状态切换规则,减少复杂度
- 移除聊天限额数量
**插件系统**
- 添加示例插件
- 示例插件:禁言插件
- 示例插件:豆包绘图插件
**新增表达方式学习**
- 自主学习群聊中的表达方式,更贴近群友
- 可自定义的学习频率和开关
- 根据人设生成额外的表达方式
**聊天管理**
- 移除不在线状态
- 大幅精简聊天状态切换规则,减少复杂度
- 移除聊天限额数量
**人格**
- 简化了人格身份的配置
- 优化了在focus模式下人格的表现和稳定性
**数据库重构**
- 移除了默认使用MongoDB采用轻量sqlite
- 无需额外安装数据库
- 提供迁移脚本
- 移除了默认使用MongoDB采用轻量sqlite
- 无需额外安装数据库
- 提供迁移脚本
**优化**
- 移除日程系统,减少幻觉(将会在未来版本回归)
- 移除主心流思考和LLM进入聊天判定
- 移除日程系统,减少幻觉(将会在未来版本回归)
- 移除主心流思考和LLM进入聊天判定
- 支持qwen3模型支持自定义是否思考和思考长度
## [0.6.3-fix-4] - 2025-5-18

View File

@ -0,0 +1,72 @@
@echo off
CHCP 65001 > nul
setlocal enabledelayedexpansion
echo 你需要选择启动方式,输入字母来选择:
echo V = 不知道什么意思就输入 V
echo C = 输入 C 使用 Conda 环境
echo.
choice /C CV /N /M "不知道什么意思就输入 V (C/V)?" /T 10 /D V
set "ENV_TYPE="
if %ERRORLEVEL% == 1 set "ENV_TYPE=CONDA"
if %ERRORLEVEL% == 2 set "ENV_TYPE=VENV"
if "%ENV_TYPE%" == "CONDA" goto activate_conda
if "%ENV_TYPE%" == "VENV" goto activate_venv
REM 如果 choice 超时或返回意外值,默认使用 venv
echo WARN: Invalid selection or timeout from choice. Defaulting to VENV.
set "ENV_TYPE=VENV"
goto activate_venv
:activate_conda
set /p CONDA_ENV_NAME="请输入要使用的 Conda 环境名称: "
if not defined CONDA_ENV_NAME (
echo 错误: 未输入 Conda 环境名称.
pause
exit /b 1
)
echo 选择: Conda '!CONDA_ENV_NAME!'
REM 激活Conda环境
call conda activate !CONDA_ENV_NAME!
if !ERRORLEVEL! neq 0 (
echo 错误: Conda环境 '!CONDA_ENV_NAME!' 激活失败. 请确保Conda已安装并正确配置, 且 '!CONDA_ENV_NAME!' 环境存在.
pause
exit /b 1
)
goto env_activated
:activate_venv
echo Selected: venv (default or selected)
REM 查找venv虚拟环境
set "venv_path=%~dp0venv\Scripts\activate.bat"
if not exist "%venv_path%" (
echo Error: venv not found. Ensure the venv directory exists alongside the script.
pause
exit /b 1
)
REM 激活虚拟环境
call "%venv_path%"
if %ERRORLEVEL% neq 0 (
echo Error: Failed to activate venv virtual environment.
pause
exit /b 1
)
goto env_activated
:env_activated
echo Environment activated successfully!
REM --- 后续脚本执行 ---
REM 运行预处理脚本
python "%~dp0scripts\mongodb_to_sqlite.py"
if %ERRORLEVEL% neq 0 (
echo Error: mongodb_to_sqlite.py execution failed.
pause
exit /b 1
)
echo All processing steps completed!
pause

View File

@ -0,0 +1,943 @@
import os
import json
import sys # 新增系统模块导入
# import time
import pickle
from pathlib import Path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from typing import Dict, Any, List, Optional, Type
from dataclasses import dataclass, field
from datetime import datetime
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure
from peewee import Model, Field, IntegrityError
# Rich 进度条和显示组件
from rich.console import Console
from rich.progress import (
Progress,
TextColumn,
BarColumn,
TaskProgressColumn,
TimeRemainingColumn,
TimeElapsedColumn,
SpinnerColumn,
)
from rich.table import Table
from rich.panel import Panel
# from rich.text import Text
from src.common.database.database import db
from src.common.database.database_model import (
ChatStreams,
LLMUsage,
Emoji,
Messages,
Images,
ImageDescriptions,
PersonInfo,
Knowledges,
ThinkingLog,
GraphNodes,
GraphEdges,
)
from src.common.logger_manager import get_logger
logger = get_logger("mongodb_to_sqlite")
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
@dataclass
class MigrationConfig:
"""迁移配置类"""
mongo_collection: str
target_model: Type[Model]
field_mapping: Dict[str, str]
batch_size: int = 500
enable_validation: bool = True
skip_duplicates: bool = True
unique_fields: List[str] = field(default_factory=list) # 用于重复检查的字段
# 数据验证相关类已移除 - 用户要求不要数据验证
@dataclass
class MigrationCheckpoint:
"""迁移断点数据"""
collection_name: str
processed_count: int
last_processed_id: Any
timestamp: datetime
batch_errors: List[Dict[str, Any]] = field(default_factory=list)
@dataclass
class MigrationStats:
"""迁移统计信息"""
total_documents: int = 0
processed_count: int = 0
success_count: int = 0
error_count: int = 0
skipped_count: int = 0
duplicate_count: int = 0
validation_errors: int = 0
batch_insert_count: int = 0
errors: List[Dict[str, Any]] = field(default_factory=list)
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
def add_error(self, doc_id: Any, error: str, doc_data: Optional[Dict] = None):
"""添加错误记录"""
self.errors.append(
{"doc_id": str(doc_id), "error": error, "timestamp": datetime.now().isoformat(), "doc_data": doc_data}
)
self.error_count += 1
def add_validation_error(self, doc_id: Any, field: str, error: str):
"""添加验证错误"""
self.add_error(doc_id, f"验证失败 - {field}: {error}")
self.validation_errors += 1
class MongoToSQLiteMigrator:
"""MongoDB到SQLite数据迁移器 - 使用Peewee ORM"""
def __init__(self, mongo_uri: Optional[str] = None, database_name: Optional[str] = None):
self.database_name = database_name or os.getenv("DATABASE_NAME", "MegBot")
self.mongo_uri = mongo_uri or self._build_mongo_uri()
self.mongo_client: Optional[MongoClient] = None
self.mongo_db = None
# 迁移配置
self.migration_configs = self._initialize_migration_configs()
# 进度条控制台
self.console = Console()
# 检查点目录
self.checkpoint_dir = Path(os.path.join(ROOT_PATH, "data", "checkpoints"))
self.checkpoint_dir.mkdir(exist_ok=True)
# 验证规则已禁用
self.validation_rules = self._initialize_validation_rules()
def _build_mongo_uri(self) -> str:
"""构建MongoDB连接URI"""
if mongo_uri := os.getenv("MONGODB_URI"):
return mongo_uri
user = os.getenv("MONGODB_USER")
password = os.getenv("MONGODB_PASS")
host = os.getenv("MONGODB_HOST", "localhost")
port = os.getenv("MONGODB_PORT", "27017")
auth_source = os.getenv("MONGODB_AUTH_SOURCE", "admin")
if user and password:
return f"mongodb://{user}:{password}@{host}:{port}/{self.database_name}?authSource={auth_source}"
else:
return f"mongodb://{host}:{port}/{self.database_name}"
def _initialize_migration_configs(self) -> List[MigrationConfig]:
"""初始化迁移配置"""
return [ # 表情包迁移配置
MigrationConfig(
mongo_collection="emoji",
target_model=Emoji,
field_mapping={
"full_path": "full_path",
"format": "format",
"hash": "emoji_hash",
"description": "description",
"emotion": "emotion",
"usage_count": "usage_count",
"last_used_time": "last_used_time",
# record_time字段将在转换时自动设置为当前时间
},
enable_validation=False, # 禁用数据验证
unique_fields=["full_path", "emoji_hash"],
),
# 聊天流迁移配置
MigrationConfig(
mongo_collection="chat_streams",
target_model=ChatStreams,
field_mapping={
"stream_id": "stream_id",
"create_time": "create_time",
"group_info.platform": "group_platform", # 由于Mongodb处理私聊时会让group_info值为null而新的数据库不允许为null所以私聊聊天流是没法迁移的等更新吧。
"group_info.group_id": "group_id", # 同上
"group_info.group_name": "group_name", # 同上
"last_active_time": "last_active_time",
"platform": "platform",
"user_info.platform": "user_platform",
"user_info.user_id": "user_id",
"user_info.user_nickname": "user_nickname",
"user_info.user_cardname": "user_cardname",
},
enable_validation=False, # 禁用数据验证
unique_fields=["stream_id"],
),
# LLM使用记录迁移配置
MigrationConfig(
mongo_collection="llm_usage",
target_model=LLMUsage,
field_mapping={
"model_name": "model_name",
"user_id": "user_id",
"request_type": "request_type",
"endpoint": "endpoint",
"prompt_tokens": "prompt_tokens",
"completion_tokens": "completion_tokens",
"total_tokens": "total_tokens",
"cost": "cost",
"status": "status",
"timestamp": "timestamp",
},
enable_validation=True, # 禁用数据验证"
unique_fields=["user_id", "prompt_tokens","completion_tokens","total_tokens","cost"], # 组合唯一性
),
# 消息迁移配置
MigrationConfig(
mongo_collection="messages",
target_model=Messages,
field_mapping={
"message_id": "message_id",
"time": "time",
"chat_id": "chat_id",
"chat_info.stream_id": "chat_info_stream_id",
"chat_info.platform": "chat_info_platform",
"chat_info.user_info.platform": "chat_info_user_platform",
"chat_info.user_info.user_id": "chat_info_user_id",
"chat_info.user_info.user_nickname": "chat_info_user_nickname",
"chat_info.user_info.user_cardname": "chat_info_user_cardname",
"chat_info.group_info.platform": "chat_info_group_platform",
"chat_info.group_info.group_id": "chat_info_group_id",
"chat_info.group_info.group_name": "chat_info_group_name",
"chat_info.create_time": "chat_info_create_time",
"chat_info.last_active_time": "chat_info_last_active_time",
"user_info.platform": "user_platform",
"user_info.user_id": "user_id",
"user_info.user_nickname": "user_nickname",
"user_info.user_cardname": "user_cardname",
"processed_plain_text": "processed_plain_text",
"detailed_plain_text": "detailed_plain_text",
"memorized_times": "memorized_times",
},
enable_validation=False, # 禁用数据验证
unique_fields=["message_id"],
),
# 图片迁移配置
MigrationConfig(
mongo_collection="images",
target_model=Images,
field_mapping={
"hash": "emoji_hash",
"description": "description",
"path": "path",
"timestamp": "timestamp",
"type": "type",
},
unique_fields=["path"],
),
# 图片描述迁移配置
MigrationConfig(
mongo_collection="image_descriptions",
target_model=ImageDescriptions,
field_mapping={
"type": "type",
"hash": "image_description_hash",
"description": "description",
"timestamp": "timestamp",
},
unique_fields=["image_description_hash", "type"],
),
# 个人信息迁移配置
MigrationConfig(
mongo_collection="person_info",
target_model=PersonInfo,
field_mapping={
"person_id": "person_id",
"person_name": "person_name",
"name_reason": "name_reason",
"platform": "platform",
"user_id": "user_id",
"nickname": "nickname",
"relationship_value": "relationship_value",
"konw_time": "know_time",
"msg_interval": "msg_interval",
"msg_interval_list": "msg_interval_list",
},
unique_fields=["person_id"],
),
# 知识库迁移配置
MigrationConfig(
mongo_collection="knowledges",
target_model=Knowledges,
field_mapping={"content": "content", "embedding": "embedding"},
unique_fields=["content"], # 假设内容唯一
),
# 思考日志迁移配置
MigrationConfig(
mongo_collection="thinking_log",
target_model=ThinkingLog,
field_mapping={
"chat_id": "chat_id",
"trigger_text": "trigger_text",
"response_text": "response_text",
"trigger_info": "trigger_info_json",
"response_info": "response_info_json",
"timing_results": "timing_results_json",
"chat_history": "chat_history_json",
"chat_history_in_thinking": "chat_history_in_thinking_json",
"chat_history_after_response": "chat_history_after_response_json",
"heartflow_data": "heartflow_data_json",
"reasoning_data": "reasoning_data_json",
},
unique_fields=["chat_id", "trigger_text"],
),
# 图节点迁移配置
MigrationConfig(
mongo_collection="graph_data.nodes",
target_model=GraphNodes,
field_mapping={
"concept": "concept",
"memory_items": "memory_items",
"hash": "hash",
"created_time": "created_time",
"last_modified": "last_modified",
},
unique_fields=["concept"],
),
# 图边迁移配置
MigrationConfig(
mongo_collection="graph_data.edges",
target_model=GraphEdges,
field_mapping={
"source": "source",
"target": "target",
"strength": "strength",
"hash": "hash",
"created_time": "created_time",
"last_modified": "last_modified",
},
unique_fields=["source", "target"], # 组合唯一性
),
]
def _initialize_validation_rules(self) -> Dict[str, Any]:
"""数据验证已禁用 - 返回空字典"""
return {}
def connect_mongodb(self) -> bool:
"""连接到MongoDB"""
try:
self.mongo_client = MongoClient(
self.mongo_uri, serverSelectionTimeoutMS=5000, connectTimeoutMS=10000, maxPoolSize=10
)
# 测试连接
self.mongo_client.admin.command("ping")
self.mongo_db = self.mongo_client[self.database_name]
logger.info(f"成功连接到MongoDB: {self.database_name}")
return True
except ConnectionFailure as e:
logger.error(f"MongoDB连接失败: {e}")
return False
except Exception as e:
logger.error(f"MongoDB连接异常: {e}")
return False
def disconnect_mongodb(self):
"""断开MongoDB连接"""
if self.mongo_client:
self.mongo_client.close()
logger.info("MongoDB连接已关闭")
def _get_nested_value(self, document: Dict[str, Any], field_path: str) -> Any:
"""获取嵌套字段的值"""
if "." not in field_path:
return document.get(field_path)
parts = field_path.split(".")
value = document
for part in parts:
if isinstance(value, dict):
value = value.get(part)
else:
return None
if value is None:
break
return value
def _convert_field_value(self, value: Any, target_field: Field) -> Any:
"""根据目标字段类型转换值"""
if value is None:
return None
field_type = target_field.__class__.__name__
try:
if target_field.name == "record_time" and field_type == "DateTimeField":
return datetime.now()
if field_type in ["CharField", "TextField"]:
if isinstance(value, (list, dict)):
return json.dumps(value, ensure_ascii=False)
return str(value) if value is not None else ""
elif field_type == "IntegerField":
if isinstance(value, str):
# 处理字符串数字
clean_value = value.strip()
if clean_value.replace(".", "").replace("-", "").isdigit():
return int(float(clean_value))
return 0
return int(value) if value is not None else 0
elif field_type in ["FloatField", "DoubleField"]:
return float(value) if value is not None else 0.0
elif field_type == "BooleanField":
if isinstance(value, str):
return value.lower() in ("true", "1", "yes", "on")
return bool(value)
elif field_type == "DateTimeField":
if isinstance(value, (int, float)):
return datetime.fromtimestamp(value)
elif isinstance(value, str):
try:
# 尝试解析ISO格式日期
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
try:
# 尝试解析时间戳字符串
return datetime.fromtimestamp(float(value))
except ValueError:
return datetime.now()
return datetime.now()
return value
except (ValueError, TypeError) as e:
logger.warning(f"字段值转换失败 ({field_type}): {value} -> {e}")
return self._get_default_value_for_field(target_field)
def _get_default_value_for_field(self, field: Field) -> Any:
"""获取字段的默认值"""
field_type = field.__class__.__name__
if hasattr(field, "default") and field.default is not None:
return field.default
if field.null:
return None
# 根据字段类型返回默认值
if field_type in ["CharField", "TextField"]:
return ""
elif field_type == "IntegerField":
return 0
elif field_type in ["FloatField", "DoubleField"]:
return 0.0
elif field_type == "BooleanField":
return False
elif field_type == "DateTimeField":
return datetime.now()
return None
def _validate_data(self, collection_name: str, data: Dict[str, Any], doc_id: Any, stats: MigrationStats) -> bool:
"""数据验证已禁用 - 始终返回True"""
return True
def _save_checkpoint(self, collection_name: str, processed_count: int, last_id: Any):
"""保存迁移断点"""
checkpoint = MigrationCheckpoint(
collection_name=collection_name,
processed_count=processed_count,
last_processed_id=last_id,
timestamp=datetime.now(),
)
checkpoint_file = self.checkpoint_dir / f"{collection_name}_checkpoint.pkl"
try:
with open(checkpoint_file, "wb") as f:
pickle.dump(checkpoint, f)
except Exception as e:
logger.warning(f"保存断点失败: {e}")
def _load_checkpoint(self, collection_name: str) -> Optional[MigrationCheckpoint]:
"""加载迁移断点"""
checkpoint_file = self.checkpoint_dir / f"{collection_name}_checkpoint.pkl"
if not checkpoint_file.exists():
return None
try:
with open(checkpoint_file, "rb") as f:
return pickle.load(f)
except Exception as e:
logger.warning(f"加载断点失败: {e}")
return None
def _batch_insert(self, model: Type[Model], data_list: List[Dict[str, Any]]) -> int:
"""批量插入数据"""
if not data_list:
return 0
success_count = 0
try:
with db.atomic():
# 分批插入避免SQL语句过长
batch_size = 100
for i in range(0, len(data_list), batch_size):
batch = data_list[i : i + batch_size]
model.insert_many(batch).execute()
success_count += len(batch)
except Exception as e:
logger.error(f"批量插入失败: {e}")
# 如果批量插入失败,尝试逐个插入
for data in data_list:
try:
model.create(**data)
success_count += 1
except Exception:
pass # 忽略单个插入失败
return success_count
def _check_duplicate_by_unique_fields(
self, model: Type[Model], data: Dict[str, Any], unique_fields: List[str]
) -> bool:
"""根据唯一字段检查重复"""
if not unique_fields:
return False
try:
query = model.select()
for field_name in unique_fields:
if field_name in data and data[field_name] is not None:
field_obj = getattr(model, field_name)
query = query.where(field_obj == data[field_name])
return query.exists()
except Exception as e:
logger.debug(f"重复检查失败: {e}")
return False
def _create_model_instance(self, model: Type[Model], data: Dict[str, Any]) -> Optional[Model]:
"""使用ORM创建模型实例"""
try:
# 过滤掉不存在的字段
valid_data = {}
for field_name, value in data.items():
if hasattr(model, field_name):
valid_data[field_name] = value
else:
logger.debug(f"跳过未知字段: {field_name}")
# 创建实例
instance = model.create(**valid_data)
return instance
except IntegrityError as e:
# 处理唯一约束冲突等完整性错误
logger.debug(f"完整性约束冲突: {e}")
return None
except Exception as e:
logger.error(f"创建模型实例失败: {e}")
return None
def migrate_collection(self, config: MigrationConfig) -> MigrationStats:
"""迁移单个集合 - 使用优化的批量插入和进度条"""
stats = MigrationStats()
stats.start_time = datetime.now()
# 检查是否有断点
checkpoint = self._load_checkpoint(config.mongo_collection)
start_from_id = checkpoint.last_processed_id if checkpoint else None
if checkpoint:
stats.processed_count = checkpoint.processed_count
logger.info(f"从断点恢复: 已处理 {checkpoint.processed_count} 条记录")
logger.info(f"开始迁移: {config.mongo_collection} -> {config.target_model._meta.table_name}")
try:
# 获取MongoDB集合
mongo_collection = self.mongo_db[config.mongo_collection]
# 构建查询条件(用于断点恢复)
query = {}
if start_from_id:
query = {"_id": {"$gt": start_from_id}}
stats.total_documents = mongo_collection.count_documents(query)
if stats.total_documents == 0:
logger.warning(f"集合 {config.mongo_collection} 为空,跳过迁移")
return stats
logger.info(f"待迁移文档数量: {stats.total_documents}")
# 创建Rich进度条
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeElapsedColumn(),
TimeRemainingColumn(),
console=self.console,
refresh_per_second=10,
) as progress:
task = progress.add_task(f"迁移 {config.mongo_collection}", total=stats.total_documents)
# 批量处理数据
batch_data = []
batch_count = 0
last_processed_id = None
for mongo_doc in mongo_collection.find(query).batch_size(config.batch_size):
try:
doc_id = mongo_doc.get("_id", "unknown")
last_processed_id = doc_id
# 构建目标数据
target_data = {}
for mongo_field, sqlite_field in config.field_mapping.items():
value = self._get_nested_value(mongo_doc, mongo_field)
# 获取目标字段对象并转换类型
if hasattr(config.target_model, sqlite_field):
field_obj = getattr(config.target_model, sqlite_field)
converted_value = self._convert_field_value(value, field_obj)
target_data[sqlite_field] = converted_value
# 数据验证已禁用
# if config.enable_validation:
# if not self._validate_data(config.mongo_collection, target_data, doc_id, stats):
# stats.skipped_count += 1
# continue
# 重复检查
if config.skip_duplicates and self._check_duplicate_by_unique_fields(
config.target_model, target_data, config.unique_fields
):
stats.duplicate_count += 1
stats.skipped_count += 1
logger.debug(f"跳过重复记录: {doc_id}")
continue
# 添加到批量数据
batch_data.append(target_data)
stats.processed_count += 1
# 执行批量插入
if len(batch_data) >= config.batch_size:
success_count = self._batch_insert(config.target_model, batch_data)
stats.success_count += success_count
stats.batch_insert_count += 1
# 保存断点
self._save_checkpoint(config.mongo_collection, stats.processed_count, last_processed_id)
batch_data.clear()
batch_count += 1
# 更新进度条
progress.update(task, advance=config.batch_size)
except Exception as e:
doc_id = mongo_doc.get("_id", "unknown")
stats.add_error(doc_id, f"处理文档异常: {e}", mongo_doc)
logger.error(f"处理文档失败 (ID: {doc_id}): {e}")
# 处理剩余的批量数据
if batch_data:
success_count = self._batch_insert(config.target_model, batch_data)
stats.success_count += success_count
stats.batch_insert_count += 1
progress.update(task, advance=len(batch_data))
# 完成进度条
progress.update(task, completed=stats.total_documents)
stats.end_time = datetime.now()
duration = stats.end_time - stats.start_time
logger.info(
f"迁移完成: {config.mongo_collection} -> {config.target_model._meta.table_name}\n"
f"总计: {stats.total_documents}, 成功: {stats.success_count}, "
f"错误: {stats.error_count}, 跳过: {stats.skipped_count}, 重复: {stats.duplicate_count}\n"
f"耗时: {duration.total_seconds():.2f}秒, 批量插入次数: {stats.batch_insert_count}"
)
# 清理断点文件
checkpoint_file = self.checkpoint_dir / f"{config.mongo_collection}_checkpoint.pkl"
if checkpoint_file.exists():
checkpoint_file.unlink()
except Exception as e:
logger.error(f"迁移集合 {config.mongo_collection} 时发生异常: {e}")
stats.add_error("collection_error", str(e))
return stats
def migrate_all(self) -> Dict[str, MigrationStats]:
"""执行所有迁移任务"""
logger.info("开始执行数据库迁移...")
if not self.connect_mongodb():
logger.error("无法连接到MongoDB迁移终止")
return {}
all_stats = {}
try:
# 创建总体进度表格
total_collections = len(self.migration_configs)
self.console.print(
Panel(
f"[bold blue]MongoDB 到 SQLite 数据迁移[/bold blue]\n"
f"[yellow]总集合数: {total_collections}[/yellow]",
title="迁移开始",
expand=False,
)
)
for idx, config in enumerate(self.migration_configs, 1):
self.console.print(
f"\n[bold green]正在处理集合 {idx}/{total_collections}: {config.mongo_collection}[/bold green]"
)
stats = self.migrate_collection(config)
all_stats[config.mongo_collection] = stats
# 显示单个集合的快速统计
if stats.processed_count > 0:
success_rate = stats.success_count / stats.processed_count * 100
if success_rate >= 95:
status_emoji = ""
status_color = "bright_green"
elif success_rate >= 80:
status_emoji = "⚠️"
status_color = "yellow"
else:
status_emoji = ""
status_color = "red"
self.console.print(
f" {status_emoji} [{status_color}]完成: {stats.success_count}/{stats.processed_count} "
f"({success_rate:.1f}%) 错误: {stats.error_count}[/{status_color}]"
)
# 错误率检查
if stats.processed_count > 0:
error_rate = stats.error_count / stats.processed_count
if error_rate > 0.1: # 错误率超过10%
self.console.print(
f" [red]⚠️ 警告: 错误率较高 {error_rate:.1%} "
f"({stats.error_count}/{stats.processed_count})[/red]"
)
finally:
self.disconnect_mongodb()
self._print_migration_summary(all_stats)
return all_stats
def _print_migration_summary(self, all_stats: Dict[str, MigrationStats]):
"""使用Rich打印美观的迁移汇总信息"""
# 计算总体统计
total_processed = sum(stats.processed_count for stats in all_stats.values())
total_success = sum(stats.success_count for stats in all_stats.values())
total_errors = sum(stats.error_count for stats in all_stats.values())
total_skipped = sum(stats.skipped_count for stats in all_stats.values())
total_duplicates = sum(stats.duplicate_count for stats in all_stats.values())
total_validation_errors = sum(stats.validation_errors for stats in all_stats.values())
total_batch_inserts = sum(stats.batch_insert_count for stats in all_stats.values())
# 计算总耗时
total_duration_seconds = 0
for stats in all_stats.values():
if stats.start_time and stats.end_time:
duration = stats.end_time - stats.start_time
total_duration_seconds += duration.total_seconds()
# 创建详细统计表格
table = Table(title="[bold blue]数据迁移汇总报告[/bold blue]", show_header=True, header_style="bold magenta")
table.add_column("集合名称", style="cyan", width=20)
table.add_column("文档总数", justify="right", style="blue")
table.add_column("处理数量", justify="right", style="green")
table.add_column("成功数量", justify="right", style="green")
table.add_column("错误数量", justify="right", style="red")
table.add_column("跳过数量", justify="right", style="yellow")
table.add_column("重复数量", justify="right", style="bright_yellow")
table.add_column("验证错误", justify="right", style="red")
table.add_column("批次数", justify="right", style="purple")
table.add_column("成功率", justify="right", style="bright_green")
table.add_column("耗时(秒)", justify="right", style="blue")
for collection_name, stats in all_stats.items():
success_rate = (stats.success_count / stats.processed_count * 100) if stats.processed_count > 0 else 0
duration = 0
if stats.start_time and stats.end_time:
duration = (stats.end_time - stats.start_time).total_seconds()
# 根据成功率设置颜色
if success_rate >= 95:
success_rate_style = "[bright_green]"
elif success_rate >= 80:
success_rate_style = "[yellow]"
else:
success_rate_style = "[red]"
table.add_row(
collection_name,
str(stats.total_documents),
str(stats.processed_count),
str(stats.success_count),
f"[red]{stats.error_count}[/red]" if stats.error_count > 0 else "0",
f"[yellow]{stats.skipped_count}[/yellow]" if stats.skipped_count > 0 else "0",
f"[bright_yellow]{stats.duplicate_count}[/bright_yellow]" if stats.duplicate_count > 0 else "0",
f"[red]{stats.validation_errors}[/red]" if stats.validation_errors > 0 else "0",
str(stats.batch_insert_count),
f"{success_rate_style}{success_rate:.1f}%[/{success_rate_style[1:]}",
f"{duration:.2f}",
)
# 添加总计行
total_success_rate = (total_success / total_processed * 100) if total_processed > 0 else 0
if total_success_rate >= 95:
total_rate_style = "[bright_green]"
elif total_success_rate >= 80:
total_rate_style = "[yellow]"
else:
total_rate_style = "[red]"
table.add_section()
table.add_row(
"[bold]总计[/bold]",
f"[bold]{sum(stats.total_documents for stats in all_stats.values())}[/bold]",
f"[bold]{total_processed}[/bold]",
f"[bold]{total_success}[/bold]",
f"[bold red]{total_errors}[/bold red]" if total_errors > 0 else "[bold]0[/bold]",
f"[bold yellow]{total_skipped}[/bold yellow]" if total_skipped > 0 else "[bold]0[/bold]",
f"[bold bright_yellow]{total_duplicates}[/bold bright_yellow]"
if total_duplicates > 0
else "[bold]0[/bold]",
f"[bold red]{total_validation_errors}[/bold red]" if total_validation_errors > 0 else "[bold]0[/bold]",
f"[bold]{total_batch_inserts}[/bold]",
f"[bold]{total_rate_style}{total_success_rate:.1f}%[/{total_rate_style[1:]}[/bold]",
f"[bold]{total_duration_seconds:.2f}[/bold]",
)
self.console.print(table)
# 创建状态面板
status_items = []
if total_errors > 0:
status_items.append(f"[red]⚠️ 发现 {total_errors} 个错误,请检查日志详情[/red]")
if total_validation_errors > 0:
status_items.append(f"[red]🔍 数据验证失败: {total_validation_errors} 条记录[/red]")
if total_duplicates > 0:
status_items.append(f"[yellow]📋 跳过重复记录: {total_duplicates} 条[/yellow]")
if total_success_rate >= 95:
status_items.append(f"[bright_green]✅ 迁移成功率优秀: {total_success_rate:.1f}%[/bright_green]")
elif total_success_rate >= 80:
status_items.append(f"[yellow]⚡ 迁移成功率良好: {total_success_rate:.1f}%[/yellow]")
else:
status_items.append(f"[red]❌ 迁移成功率较低: {total_success_rate:.1f}%,需要检查[/red]")
if status_items:
status_panel = Panel(
"\n".join(status_items), title="[bold yellow]迁移状态总结[/bold yellow]", border_style="yellow"
)
self.console.print(status_panel)
# 性能统计面板
avg_speed = total_processed / total_duration_seconds if total_duration_seconds > 0 else 0
performance_info = (
f"[cyan]总处理时间:[/cyan] {total_duration_seconds:.2f}\n"
f"[cyan]平均处理速度:[/cyan] {avg_speed:.1f} 条记录/秒\n"
f"[cyan]批量插入优化:[/cyan] 执行了 {total_batch_inserts} 次批量操作"
)
performance_panel = Panel(performance_info, title="[bold green]性能统计[/bold green]", border_style="green")
self.console.print(performance_panel)
def add_migration_config(self, config: MigrationConfig):
"""添加新的迁移配置"""
self.migration_configs.append(config)
def migrate_single_collection(self, collection_name: str) -> Optional[MigrationStats]:
"""迁移单个指定的集合"""
config = next((c for c in self.migration_configs if c.mongo_collection == collection_name), None)
if not config:
logger.error(f"未找到集合 {collection_name} 的迁移配置")
return None
if not self.connect_mongodb():
logger.error("无法连接到MongoDB")
return None
try:
stats = self.migrate_collection(config)
self._print_migration_summary({collection_name: stats})
return stats
finally:
self.disconnect_mongodb()
def export_error_report(self, all_stats: Dict[str, MigrationStats], filepath: str):
"""导出错误报告"""
error_report = {
"timestamp": datetime.now().isoformat(),
"summary": {
collection: {
"total": stats.total_documents,
"processed": stats.processed_count,
"success": stats.success_count,
"errors": stats.error_count,
"skipped": stats.skipped_count,
"duplicates": stats.duplicate_count,
}
for collection, stats in all_stats.items()
},
"errors": {collection: stats.errors for collection, stats in all_stats.items() if stats.errors},
}
try:
with open(filepath, "w", encoding="utf-8") as f:
json.dump(error_report, f, ensure_ascii=False, indent=2)
logger.info(f"错误报告已导出到: {filepath}")
except Exception as e:
logger.error(f"导出错误报告失败: {e}")
def main():
"""主程序入口"""
migrator = MongoToSQLiteMigrator()
# 执行迁移
migration_results = migrator.migrate_all()
# 导出错误报告(如果有错误)
if any(stats.error_count > 0 for stats in migration_results.values()):
error_report_path = f"migration_errors_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
migrator.export_error_report(migration_results, error_report_path)
logger.info("数据迁移完成!")
if __name__ == "__main__":
main()

View File

@ -1,6 +1,7 @@
from src.chat.heart_flow.heartflow import heartflow
from src.chat.heart_flow.sub_heartflow import ChatState
from src.common.logger_manager import get_logger
import time
logger = get_logger("api")
@ -30,6 +31,29 @@ async def get_subheartflow_cycle_info(subheartflow_id: str, history_len: int) ->
return None
async def get_normal_chat_replies(subheartflow_id: str, limit: int = 10) -> list:
"""获取子心流的NormalChat回复记录
Args:
subheartflow_id: 子心流ID
limit: 最大返回数量默认10条
Returns:
list: 回复记录列表如果未找到则返回空列表
"""
replies = await heartflow.api_get_normal_chat_replies(subheartflow_id, limit)
logger.debug(f"子心流 {subheartflow_id} NormalChat回复记录: 获取到 {len(replies) if replies else 0}")
if replies:
# 格式化时间戳为可读时间
for reply in replies:
if "time" in reply:
reply["formatted_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(reply["time"]))
return replies
else:
logger.warning(f"子心流 {subheartflow_id} NormalChat回复记录未找到")
return []
async def get_all_states():
"""获取所有状态"""
all_states = await heartflow.api_get_all_states()

View File

@ -62,7 +62,6 @@ class APIBotConfig:
# focus_chat
reply_trigger_threshold: float # 回复触发阈值
default_decay_rate_per_second: float # 默认每秒衰减率
consecutive_no_reply_threshold: int # 连续不回复阈值
# compressed
compressed_length: int # 压缩长度

View File

@ -149,7 +149,7 @@ class MaiEmoji:
emotion_str = ",".join(self.emotion) if self.emotion else ""
Emoji.create(
hash=self.hash,
emoji_hash=self.hash,
full_path=self.full_path,
format=self.format,
description=self.description,
@ -367,12 +367,14 @@ class EmojiManager:
return cls._instance
def __init__(self) -> None:
self._initialized = None
if self._initialized:
return # 如果已经初始化过,直接返回
self._scan_task = None
self.vlm = LLMRequest(model=global_config.model.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
self.llm_emotion_judge = LLMRequest(
model=global_config.model.normal, max_tokens=600, request_type="emoji"
model=global_config.model.utils, max_tokens=600, request_type="emoji"
) # 更高的温度更少的token后续可以根据情绪来调整温度
self.emoji_num = 0
@ -389,6 +391,7 @@ class EmojiManager:
raise RuntimeError("数据库连接失败")
_ensure_emoji_dir()
Emoji.create_table(safe=True) # Ensures table exists
self._initialized = True
def _ensure_db(self) -> None:
"""确保数据库已初始化"""
@ -467,7 +470,7 @@ class EmojiManager:
selected_emoji, similarity, matched_emotion = random.choice(top_emojis)
# 更新使用次数
self.record_usage(selected_emoji.emoji_hash)
self.record_usage(selected_emoji.hash)
_time_end = time.time()
@ -632,7 +635,7 @@ class EmojiManager:
"""获取所有表情包并初始化为MaiEmoji类对象更新 self.emoji_objects"""
try:
self._ensure_db()
logger.info("[数据库] 开始加载所有表情包记录 (Peewee)...")
logger.debug("[数据库] 开始加载所有表情包记录 (Peewee)...")
emoji_peewee_instances = Emoji.select()
emoji_objects, load_errors = _to_emoji_objects(emoji_peewee_instances)
@ -796,7 +799,7 @@ class EmojiManager:
# 删除选定的表情包
logger.info(f"[决策] 删除表情包: {emoji_to_delete.description}")
delete_success = await self.delete_emoji(emoji_to_delete.emoji_hash)
delete_success = await self.delete_emoji(emoji_to_delete.hash)
if delete_success:
# 修复:等待异步注册完成

View File

@ -13,11 +13,9 @@ from src.chat.emoji_system.emoji_manager import emoji_manager
from src.chat.focus_chat.heartFC_sender import HeartFCSender
from src.chat.utils.utils import process_llm_response
from src.chat.utils.info_catcher import info_catcher_manager
from src.manager.mood_manager import mood_manager
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
from src.individuality.individuality import individuality
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
import time
@ -78,10 +76,10 @@ class DefaultExpressor:
self.log_prefix = "expressor"
# TODO: API-Adapter修改标记
self.express_model = LLMRequest(
model=global_config.model.normal,
temperature=global_config.model.normal["temp"],
model=global_config.model.focus_expressor,
# temperature=global_config.model.focus_expressor["temp"],
max_tokens=256,
request_type="response_heartflow",
request_type="focus_expressor",
)
self.heart_fc_sender = HeartFCSender()
@ -107,10 +105,7 @@ class DefaultExpressor:
user_nickname=global_config.bot.nickname,
platform=messageinfo.platform,
)
# logger.debug(f"创建思考消息:{anchor_message}")
# logger.debug(f"创建思考消息chat{chat}")
# logger.debug(f"创建思考消息bot_user_info{bot_user_info}")
# logger.debug(f"创建思考消息messageinfo{messageinfo}")
thinking_message = MessageThinking(
message_id=thinking_id,
chat_stream=chat,
@ -150,22 +145,22 @@ class DefaultExpressor:
action_data=action_data,
)
with Timer("选择表情", cycle_timers):
emoji_keyword = action_data.get("emojis", [])
emoji_base64 = await self._choose_emoji(emoji_keyword)
if emoji_base64:
reply.append(("emoji", emoji_base64))
with Timer("选择表情", cycle_timers):
emoji_keyword = action_data.get("emojis", [])
emoji_base64 = await self._choose_emoji(emoji_keyword)
if emoji_base64:
reply.append(("emoji", emoji_base64))
if reply:
with Timer("发送消息", cycle_timers):
sent_msg_list = await self.send_response_messages(
anchor_message=anchor_message,
thinking_id=thinking_id,
response_set=reply,
)
has_sent_something = True
else:
logger.warning(f"{self.log_prefix} 文本回复生成失败")
if reply:
with Timer("发送消息", cycle_timers):
sent_msg_list = await self.send_response_messages(
anchor_message=anchor_message,
thinking_id=thinking_id,
response_set=reply,
)
has_sent_something = True
else:
logger.warning(f"{self.log_prefix} 文本回复生成失败")
if not has_sent_something:
logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容")
@ -174,6 +169,7 @@ class DefaultExpressor:
except Exception as e:
logger.error(f"回复失败: {e}")
traceback.print_exc()
return False, None
# --- 回复器 (Replier) 的定义 --- #
@ -192,9 +188,9 @@ class DefaultExpressor:
"""
try:
# 1. 获取情绪影响因子并调整模型温度
arousal_multiplier = mood_manager.get_arousal_multiplier()
current_temp = float(global_config.model.normal["temp"]) * arousal_multiplier
self.express_model.params["temperature"] = current_temp # 动态调整温度
# arousal_multiplier = mood_manager.get_arousal_multiplier()
# current_temp = float(global_config.model.normal["temp"]) * arousal_multiplier
# self.express_model.params["temperature"] = current_temp # 动态调整温度
# 2. 获取信息捕捉器
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
@ -238,9 +234,8 @@ class DefaultExpressor:
# logger.info(f"{self.log_prefix}\nPrompt:\n{prompt}\n---------------------------\n")
logger.info(f"想要表达:{in_mind_reply}")
logger.info(f"理由:{reason}")
logger.info(f"生成回复: {content}\n")
logger.info(f"想要表达:{in_mind_reply}||理由:{reason}")
logger.info(f"最终回复: {content}\n")
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
@ -281,15 +276,8 @@ class DefaultExpressor:
in_mind_reply,
target_message,
) -> str:
prompt_personality = individuality.get_prompt(x_person=0, level=2)
# Determine if it's a group chat
is_group_chat = bool(chat_stream.group_info)
# Use sender_name passed from caller for private chat, otherwise use a default for group
# Default sender_name for group chat isn't used in the group prompt template, but set for consistency
effective_sender_name = sender_name if not is_group_chat else "某人"
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
@ -358,14 +346,18 @@ class DefaultExpressor:
)
else: # Private chat
template_name = "default_expressor_private_prompt"
chat_target_1 = "你正在和人私聊"
prompt = await global_prompt_manager.format_prompt(
template_name,
sender_name=effective_sender_name, # Used in private template
chat_talking_prompt=chat_talking_prompt,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
bot_name=global_config.bot.nickname,
prompt_personality=prompt_personality,
prompt_personality="",
reason=reason,
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
in_mind_reply=in_mind_reply,
target_message=target_message,
)
return prompt
@ -373,7 +365,11 @@ class DefaultExpressor:
# --- 发送器 (Sender) --- #
async def send_response_messages(
self, anchor_message: Optional[MessageRecv], response_set: List[Tuple[str, str]], thinking_id: str = ""
self,
anchor_message: Optional[MessageRecv],
response_set: List[Tuple[str, str]],
thinking_id: str = "",
display_message: str = "",
) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
chat = self.chat_stream
@ -409,6 +405,9 @@ class DefaultExpressor:
type = msg_text[0]
data = msg_text[1]
if global_config.experimental.debug_show_chat_mode and type == "text":
data += ""
part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type=type, data=data)
@ -422,6 +421,7 @@ class DefaultExpressor:
anchor_message=anchor_message,
message_id=part_message_id,
message_segment=message_segment,
display_message=display_message,
reply_to=reply_to,
is_emoji=is_emoji,
thinking_id=thinking_id,
@ -439,7 +439,13 @@ class DefaultExpressor:
if type == "emoji":
typing = False
sent_msg = await self.heart_fc_sender.send_message(bot_message, has_thinking=True, typing=typing)
if anchor_message.raw_message:
set_reply = True
else:
set_reply = False
sent_msg = await self.heart_fc_sender.send_message(
bot_message, has_thinking=True, typing=typing, set_reply=set_reply
)
reply_message_ids.append(part_message_id) # 记录我们生成的ID
@ -479,6 +485,7 @@ class DefaultExpressor:
is_emoji: bool,
thinking_id: str,
thinking_start_time: float,
display_message: str,
) -> MessageSending:
"""构建单个发送消息"""
@ -498,6 +505,7 @@ class DefaultExpressor:
is_head=reply_to,
is_emoji=is_emoji,
thinking_start_time=thinking_start_time, # 传递原始思考开始时间
display_message=display_message,
)
return bot_message

View File

@ -5,7 +5,7 @@ from src.common.logger_manager import get_logger
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.chat.utils.chat_message_builder import get_raw_msg_by_timestamp_random, build_anonymous_messages
from src.chat.focus_chat.heartflow_prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
import os
import json
@ -61,10 +61,10 @@ class ExpressionLearner:
def __init__(self) -> None:
# TODO: API-Adapter修改标记
self.express_learn_model: LLMRequest = LLMRequest(
model=global_config.model.normal,
model=global_config.model.focus_expressor,
temperature=0.1,
max_tokens=256,
request_type="response_heartflow",
request_type="learn_expression",
)
async def get_expression_by_chat_id(self, chat_id: str) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
@ -204,19 +204,21 @@ class ExpressionLearner:
random_msg: Optional[List[Dict[str, Any]]] = get_raw_msg_by_timestamp_random(
current_time - 3600 * 24, current_time, limit=num
)
if not random_msg:
# print(random_msg)
if not random_msg or random_msg == []:
return None
# 转化成str
chat_id: str = random_msg[0]["chat_id"]
# random_msg_str: str = await build_readable_messages(random_msg, timestamp_mode="normal")
random_msg_str: str = await build_anonymous_messages(random_msg)
# print(f"random_msg_str:{random_msg_str}")
prompt: str = await global_prompt_manager.format_prompt(
prompt,
chat_str=random_msg_str,
)
# logger.info(f"学习{type_str}的prompt: {prompt}")
logger.debug(f"学习{type_str}的prompt: {prompt}")
try:
response, _ = await self.express_learn_model.generate_response_async(prompt)
@ -224,7 +226,7 @@ class ExpressionLearner:
logger.error(f"学习{type_str}失败: {e}")
return None
logger.info(f"学习{type_str}的response: {response}")
logger.debug(f"学习{type_str}的response: {response}")
expressions: List[Tuple[str, str, str]] = self.parse_expression_response(response, chat_id)

View File

@ -3,7 +3,7 @@ import contextlib
import time
import traceback
from collections import deque
from typing import List, Optional, Dict, Any, Deque
from typing import List, Optional, Dict, Any, Deque, Callable, Awaitable
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.message_receive.chat_stream import chat_manager
from rich.traceback import install
@ -16,8 +16,10 @@ from src.chat.focus_chat.info.info_base import InfoBase
from src.chat.focus_chat.info_processors.chattinginfo_processor import ChattingInfoProcessor
from src.chat.focus_chat.info_processors.mind_processor import MindProcessor
from src.chat.focus_chat.info_processors.working_memory_processor import WorkingMemoryProcessor
from src.chat.focus_chat.info_processors.action_processor import ActionProcessor
from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation
from src.chat.heart_flow.observation.working_observation import WorkingMemoryObservation
from src.chat.heart_flow.observation.structure_observation import StructureObservation
from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor
from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor
from src.chat.focus_chat.memory_activator import MemoryActivator
@ -39,6 +41,7 @@ PROCESSOR_CLASSES = {
"ToolProcessor": (ToolProcessor, "tool_use_processor"),
"WorkingMemoryProcessor": (WorkingMemoryProcessor, "working_memory_processor"),
"SelfProcessor": (SelfProcessor, "self_identify_processor"),
"ActionProcessor": (ActionProcessor, "action_processor"), # 这个处理器不需要配置键名,默认启用
}
@ -50,6 +53,9 @@ CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
logger = get_logger("hfc") # Logger Name Changed
# 设定处理器超时时间(秒)
PROCESSOR_TIMEOUT = 40
async def _handle_cycle_delay(action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
"""处理循环延迟"""
@ -81,6 +87,7 @@ class HeartFChatting:
self,
chat_id: str,
observations: list[Observation],
on_stop_focus_chat: Optional[Callable[[], Awaitable[None]]] = None,
):
"""
HeartFChatting 初始化函数
@ -88,6 +95,7 @@ class HeartFChatting:
参数:
chat_id: 聊天流唯一标识符(如stream_id)
observations: 关联的观察列表
on_stop_focus_chat: 当收到stop_focus_chat命令时调用的回调函数
"""
# 基础属性
self.stream_id: str = chat_id # 聊天流ID
@ -95,6 +103,7 @@ class HeartFChatting:
self.log_prefix: str = str(chat_id) # Initial default, will be updated
self.hfcloop_observation = HFCloopObservation(observe_id=self.stream_id)
self.chatting_observation = observations[0]
self.structure_observation = StructureObservation(observe_id=self.stream_id)
self.memory_activator = MemoryActivator()
self.working_memory = WorkingMemory(chat_id=self.stream_id)
@ -139,6 +148,9 @@ class HeartFChatting:
self._current_cycle: Optional[CycleDetail] = None
self._shutting_down: bool = False # 关闭标志位
# 存储回调函数
self.on_stop_focus_chat = on_stop_focus_chat
async def _initialize(self) -> bool:
"""
执行懒初始化操作
@ -284,6 +296,19 @@ class HeartFChatting:
logger.debug(f"模板 {self.chat_stream.context.get_template_name()}")
loop_info = await self._observe_process_plan_action_loop(cycle_timers, thinking_id)
print(loop_info["loop_action_info"]["command"])
if loop_info["loop_action_info"]["command"] == "stop_focus_chat":
logger.info(f"{self.log_prefix} 麦麦决定停止专注聊天")
# 如果设置了回调函数,则调用它
if self.on_stop_focus_chat:
try:
await self.on_stop_focus_chat()
logger.info(f"{self.log_prefix} 成功调用回调函数处理停止专注聊天")
except Exception as e:
logger.error(f"{self.log_prefix} 调用停止专注聊天回调函数时出错: {e}")
logger.error(traceback.format_exc())
break
self._current_cycle.set_loop_info(loop_info)
self.hfcloop_observation.add_loop_info(self._current_cycle)
@ -354,9 +379,15 @@ class HeartFChatting:
for processor in self.processors:
processor_name = processor.__class__.log_prefix
task = asyncio.create_task(
processor.process_info(observations=observations, running_memorys=running_memorys)
)
# 用lambda包裹便于传参
async def run_with_timeout(proc=processor):
return await asyncio.wait_for(
proc.process_info(observations=observations, running_memorys=running_memorys),
timeout=PROCESSOR_TIMEOUT,
)
task = asyncio.create_task(run_with_timeout())
processor_tasks.append(task)
task_to_name_map[task] = processor_name
logger.debug(f"{self.log_prefix} 启动处理器任务: {processor_name}")
@ -375,13 +406,13 @@ class HeartFChatting:
try:
# 使用 await task 来获取结果或触发异常
result_list = await task
logger.info(
f"{self.log_prefix} 处理器 {processor_name} 已完成,信息已处理: {duration_since_parallel_start:.2f}"
)
logger.info(f"{self.log_prefix} 处理器 {processor_name} 已完成!")
if result_list is not None:
all_plan_info.extend(result_list)
else:
logger.warning(f"{self.log_prefix} 处理器 {processor_name} 返回了 None")
except asyncio.TimeoutError:
logger.info(f"{self.log_prefix} 处理器 {processor_name} 超时(>{PROCESSOR_TIMEOUT}s已跳过")
except Exception as e:
logger.error(
f"{self.log_prefix} 处理器 {processor_name} 执行失败,耗时 (自并行开始): {duration_since_parallel_start:.2f}秒. 错误: {e}",
@ -406,17 +437,19 @@ class HeartFChatting:
return all_plan_info
async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> tuple[bool, str]:
async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> dict:
try:
with Timer("观察", cycle_timers):
# await self.observations[0].observe()
await self.chatting_observation.observe()
await self.working_observation.observe()
await self.hfcloop_observation.observe()
await self.structure_observation.observe()
observations: List[Observation] = []
observations.append(self.chatting_observation)
observations.append(self.working_observation)
observations.append(self.hfcloop_observation)
observations.append(self.structure_observation)
loop_observation_info = {
"observations": observations,
@ -425,10 +458,7 @@ class HeartFChatting:
self.all_observations = observations
with Timer("回忆", cycle_timers):
logger.debug(f"{self.log_prefix} 开始回忆")
running_memorys = await self.memory_activator.activate_memory(observations)
logger.debug(f"{self.log_prefix} 回忆完成")
print(running_memorys)
with Timer("执行 信息处理器", cycle_timers):
all_plan_info = await self._process_processors(observations, running_memorys, cycle_timers)
@ -461,15 +491,16 @@ class HeartFChatting:
else:
action_str = action_type
logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'")
logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}', 原因'{reasoning}'")
success, reply_text = await self._handle_action(
success, reply_text, command = await self._handle_action(
action_type, reasoning, action_data, cycle_timers, thinking_id
)
loop_action_info = {
"action_taken": success,
"reply_text": reply_text,
"command": command,
}
loop_info = {
@ -484,7 +515,12 @@ class HeartFChatting:
except Exception as e:
logger.error(f"{self.log_prefix} FOCUS聊天处理失败: {e}")
logger.error(traceback.format_exc())
return {}
return {
"loop_observation_info": {},
"loop_processor_info": {},
"loop_plan_info": {},
"loop_action_info": {"action_taken": False, "reply_text": "", "command": ""},
}
async def _handle_action(
self,
@ -493,7 +529,7 @@ class HeartFChatting:
action_data: dict,
cycle_timers: dict,
thinking_id: str,
) -> tuple[bool, str]:
) -> tuple[bool, str, str]:
"""
处理规划动作使用动作工厂创建相应的动作处理器
@ -505,36 +541,48 @@ class HeartFChatting:
thinking_id: 思考ID
返回:
tuple[bool, str]: (是否执行了动作, 思考消息ID)
tuple[bool, str, str]: (是否执行了动作, 思考消息ID, 命令)
"""
try:
# 使用工厂创建动作处理器实例
action_handler = self.action_manager.create_action(
action_name=action,
action_data=action_data,
reasoning=reasoning,
cycle_timers=cycle_timers,
thinking_id=thinking_id,
observations=self.all_observations,
expressor=self.expressor,
chat_stream=self.chat_stream,
log_prefix=self.log_prefix,
shutting_down=self._shutting_down,
)
try:
action_handler = self.action_manager.create_action(
action_name=action,
action_data=action_data,
reasoning=reasoning,
cycle_timers=cycle_timers,
thinking_id=thinking_id,
observations=self.all_observations,
expressor=self.expressor,
chat_stream=self.chat_stream,
log_prefix=self.log_prefix,
shutting_down=self._shutting_down,
)
except Exception as e:
logger.error(f"{self.log_prefix} 创建动作处理器时出错: {e}")
traceback.print_exc()
return False, "", ""
if not action_handler:
logger.warning(f"{self.log_prefix} 未能创建动作处理器: {action}, 原因: {reasoning}")
return False, ""
return False, "", ""
# 处理动作并获取结果
success, reply_text = await action_handler.handle_action()
return success, reply_text
result = await action_handler.handle_action()
if len(result) == 3:
success, reply_text, command = result
else:
success, reply_text = result
command = ""
logger.debug(
f"{self.log_prefix} 麦麦执行了'{action}', 原因'{reasoning}',返回结果'{success}', '{reply_text}', '{command}'"
)
return success, reply_text, command
except Exception as e:
logger.error(f"{self.log_prefix} 处理{action}时出错: {e}")
traceback.print_exc()
return False, ""
return False, "", ""
async def shutdown(self):
"""优雅关闭HeartFChatting实例取消活动循环任务"""

View File

@ -73,7 +73,7 @@ class HeartFCSender:
thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
return thinking_message.thinking_start_time if thinking_message else None
async def send_message(self, message: MessageSending, has_thinking=False, typing=False):
async def send_message(self, message: MessageSending, has_thinking=False, typing=False, set_reply=False):
"""
处理发送并存储一条消息
@ -97,7 +97,7 @@ class HeartFCSender:
message_id = message.message_info.message_id
try:
if has_thinking:
if set_reply:
_ = message.update_thinking_time()
# --- 条件应用 set_reply 逻辑 ---

View File

@ -1,18 +1,21 @@
import time
import traceback
from ..memory_system.Hippocampus import HippocampusManager
from ...config.config import global_config
from ..message_receive.message import MessageRecv
from ..message_receive.storage import MessageStorage
from ..utils.utils import is_mentioned_bot_in_message
from src.chat.memory_system.Hippocampus import HippocampusManager
from src.config.config import global_config
from src.chat.message_receive.message import MessageRecv
from src.chat.message_receive.storage import MessageStorage
from src.chat.heart_flow.heartflow import heartflow
from src.chat.message_receive.chat_stream import chat_manager, ChatStream
from src.chat.utils.utils import is_mentioned_bot_in_message
from src.chat.utils.timer_calculator import Timer
from src.common.logger_manager import get_logger
from ..message_receive.chat_stream import chat_manager
from src.person_info.relationship_manager import relationship_manager
import math
import re
import traceback
from typing import Optional, Tuple, Dict, Any
from maim_message import UserInfo
# from ..message_receive.message_buffer import message_buffer
from ..utils.timer_calculator import Timer
from src.person_info.relationship_manager import relationship_manager
from typing import Optional, Tuple, Dict, Any
logger = get_logger("chat")
@ -69,6 +72,15 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
message.processed_plain_text,
fast_retrieval=True,
)
text_len = len(message.processed_plain_text)
# 根据文本长度调整兴趣度长度越大兴趣度越高但增长率递减最低0.01最高0.05
# 采用对数函数实现递减增长
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1))
base_interest = min(max(base_interest, 0.01), 0.05)
interested_rate += base_interest
logger.trace(f"记忆激活率: {interested_rate:.2f}")
if is_mentioned:
@ -100,7 +112,7 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
# return "seglist"
def _check_ban_words(text: str, chat, userinfo) -> bool:
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
"""检查消息是否包含过滤词
Args:
@ -120,7 +132,7 @@ def _check_ban_words(text: str, chat, userinfo) -> bool:
return False
def _check_ban_regex(text: str, chat, userinfo) -> bool:
def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
"""检查消息是否匹配过滤正则表达式
Args:
@ -132,7 +144,7 @@ def _check_ban_regex(text: str, chat, userinfo) -> bool:
bool: 是否匹配过滤正则
"""
for pattern in global_config.message_receive.ban_msgs_regex:
if pattern.search(text):
if re.search(pattern, text):
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
logger.info(f"[正则表达式过滤]消息匹配到{pattern}filtered")
@ -205,21 +217,16 @@ class HeartFCMessageReceiver:
# 6. 兴趣度计算与更新
interested_rate, is_mentioned = await _calculate_interest(message)
await subheartflow.interest_chatting.increase_interest(value=interested_rate)
subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned)
subheartflow.add_message_to_normal_chat_cache(message, interested_rate, is_mentioned)
# 7. 日志记录
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
logger.info(
f"[{current_time}][{mes_name}]"
f"{userinfo.user_nickname}:"
f"{message.processed_plain_text}"
f"[激活: {interested_rate:.1f}]"
)
# current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
logger.info(f"[{mes_name}]{userinfo.user_nickname}:{message.processed_plain_text}")
# 8. 关系处理
await _process_relationship(message)
if global_config.relationship.give_name:
await _process_relationship(message)
except Exception as e:
await _handle_error(e, "消息处理失败", message)

View File

@ -76,7 +76,10 @@ class StructuredInfo:
"""
info_str = ""
# print(f"self.data: {self.data}")
for key, value in self.data.items():
# print(f"key: {key}, value: {value}")
info_str += f"信息类型:{key},信息内容:{value}\n"
return info_str

View File

@ -5,8 +5,9 @@ from src.chat.focus_chat.info.action_info import ActionInfo
from .base_processor import BaseProcessor
from src.common.logger_manager import get_logger
from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.message_receive.chat_stream import chat_manager
from typing import Dict
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
import random
@ -19,15 +20,11 @@ class ActionProcessor(BaseProcessor):
用于处理Observation对象将其转换为ObsInfo对象
"""
log_prefix = "聊天信息处理"
log_prefix = "动作处理"
def __init__(self):
"""初始化观察处理器"""
super().__init__()
# TODO: API-Adapter修改标记
self.model_summary = LLMRequest(
model=global_config.model.observation, temperature=0.7, max_tokens=300, request_type="chat_observation"
)
async def process_info(
self,
@ -50,21 +47,63 @@ class ActionProcessor(BaseProcessor):
# 处理Observation对象
if observations:
action_info = ActionInfo()
all_actions = None
hfc_obs = None
chat_obs = None
# 收集所有观察对象
for obs in observations:
if isinstance(obs, HFCloopObservation):
# 创建动作信息
action_info = ActionInfo()
action_changes = await self.analyze_loop_actions(obs)
if action_changes["add"] or action_changes["remove"]:
action_info.set_action_changes(action_changes)
# 设置变更原因
reasons = []
if action_changes["add"]:
reasons.append(f"添加动作{action_changes['add']}因为检测到大量无回复")
if action_changes["remove"]:
reasons.append(f"移除动作{action_changes['remove']}因为检测到连续回复")
action_info.set_reason(" | ".join(reasons))
processed_infos.append(action_info)
hfc_obs = obs
if isinstance(obs, ChattingObservation):
chat_obs = obs
# 合并所有动作变更
merged_action_changes = {"add": [], "remove": []}
reasons = []
# 处理HFCloopObservation
if hfc_obs:
obs = hfc_obs
all_actions = obs.all_actions
action_changes = await self.analyze_loop_actions(obs)
if action_changes["add"] or action_changes["remove"]:
# 合并动作变更
merged_action_changes["add"].extend(action_changes["add"])
merged_action_changes["remove"].extend(action_changes["remove"])
# 收集变更原因
if action_changes["add"]:
reasons.append(f"添加动作{action_changes['add']}因为检测到大量无回复")
if action_changes["remove"]:
reasons.append(f"移除动作{action_changes['remove']}因为检测到连续回复")
# 处理ChattingObservation
if chat_obs and all_actions is not None:
obs = chat_obs
# 检查动作的关联类型
chat_context = chat_manager.get_stream(obs.chat_id).context
type_mismatched_actions = []
for action_name in all_actions.keys():
data = all_actions[action_name]
if data.get("associated_types"):
if not chat_context.check_types(data["associated_types"]):
type_mismatched_actions.append(action_name)
logger.debug(f"{self.log_prefix} 动作 {action_name} 关联类型不匹配,移除该动作")
if type_mismatched_actions:
# 合并到移除列表中
merged_action_changes["remove"].extend(type_mismatched_actions)
reasons.append(f"移除动作{type_mismatched_actions}因为关联类型不匹配")
# 如果有任何动作变更设置到action_info中
if merged_action_changes["add"] or merged_action_changes["remove"]:
action_info.set_action_changes(merged_action_changes)
action_info.set_reason(" | ".join(reasons))
processed_infos.append(action_info)
return processed_infos
@ -96,8 +135,15 @@ class ActionProcessor(BaseProcessor):
reply_sequence.append(action_type == "reply")
# 检查no_reply比例
if len(recent_cycles) >= 5 and (no_reply_count / len(recent_cycles)) >= 0.8:
result["add"].append("exit_focus_chat")
print(f"no_reply_count: {no_reply_count}, len(recent_cycles): {len(recent_cycles)}")
# print(1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111)
if len(recent_cycles) >= (5 * global_config.chat.exit_focus_threshold) and (
no_reply_count / len(recent_cycles)
) >= (0.8 * global_config.chat.exit_focus_threshold):
if global_config.chat.chat_mode == "auto":
result["add"].append("exit_focus_chat")
result["remove"].append("no_reply")
result["remove"].append("reply")
# 获取最近三次的reply状态
last_three = reply_sequence[-3:] if len(reply_sequence) >= 3 else reply_sequence

View File

@ -28,7 +28,7 @@ class ChattingInfoProcessor(BaseProcessor):
super().__init__()
# TODO: API-Adapter修改标记
self.model_summary = LLMRequest(
model=global_config.model.observation, temperature=0.7, max_tokens=300, request_type="chat_observation"
model=global_config.model.utils_small, temperature=0.7, max_tokens=300, request_type="chat_observation"
)
async def process_info(
@ -96,6 +96,7 @@ class ChattingInfoProcessor(BaseProcessor):
async def chat_compress(self, obs: ChattingObservation):
if obs.compressor_prompt:
summary = ""
try:
summary_result, _, _ = await self.model_summary.generate_response(obs.compressor_prompt)
summary = "没有主题的闲聊" # 默认值

View File

@ -71,10 +71,10 @@ class MindProcessor(BaseProcessor):
self.subheartflow_id = subheartflow_id
self.llm_model = LLMRequest(
model=global_config.model.sub_heartflow,
temperature=global_config.model.sub_heartflow["temp"],
model=global_config.model.focus_chat_mind,
temperature=global_config.model.focus_chat_mind["temp"],
max_tokens=800,
request_type="sub_heart_flow",
request_type="focus_chat_mind",
)
self.current_mind = ""
@ -227,7 +227,7 @@ class MindProcessor(BaseProcessor):
# 记录初步思考结果
logger.debug(f"{self.log_prefix} 思考prompt: \n{prompt}\n")
logger.info(f"{self.log_prefix} 思考结果: {content}")
logger.info(f"{self.log_prefix} 聊天规划: {content}")
self.update_current_mind(content)
return content

View File

@ -22,7 +22,7 @@ logger = get_logger("processor")
def init_prompt():
indentify_prompt = """
{name_block}
你是一个AI但是你伪装成了一个人类你的人格是{prompt_personality}
你是一个AI但是你伪装成了一个人类你的人格是{prompt_personality}
{indentify_block}
{relation_prompt}
@ -31,12 +31,13 @@ def init_prompt():
现在是{time_now}你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
{chat_observe_info}
现在请你根据现有的信息思考自我认同
1. 你是一个什么样的人,你和群里的人关系如何
2. 你的形象是什么
3. 思考有没有人提到你或者图片与你有关
4. 你的自我认同是否有助于你的回答如果你需要自我相关的信息来帮你参与聊天请输出否则请输出十几个字的简短自我认同
5. 一般情况下不用输出自我认同只需要输出十几个字的简短自我认同就好除非有明显需要自我认同的场景
现在请你根据现有的信息思考自我认同请严格遵守以下规则
1. 请严格参考最上方的人设适当参考记忆和当前聊天内容不要被记忆和当前聊天内容中相反的内容误导
2. 你是一个什么样的人,你和群里的人关系如何
3. 你的形象是什么
4. 思考有没有人提到你或者图片与你有关
5. 你的自我认同是否有助于你的回答如果你需要自我相关的信息来帮你参与聊天请输出否则请输出十几个字的简短自我认同
6. 一般情况下不用输出自我认同只需要输出十几个字的简短自我认同就好除非有明显需要自我认同的场景
输出内容平淡一些说中文不要浮夸平淡一些
请注意不要输出多余内容(包括前后缀冒号和引号括号()表情包at或 @等 )只输出自我认同内容记得明确说明这是你的自我认同
@ -54,10 +55,10 @@ class SelfProcessor(BaseProcessor):
self.subheartflow_id = subheartflow_id
self.llm_model = LLMRequest(
model=global_config.model.sub_heartflow,
temperature=global_config.model.sub_heartflow["temp"],
model=global_config.model.focus_self_recognize,
temperature=global_config.model.focus_self_recognize["temp"],
max_tokens=800,
request_type="self_identify",
request_type="focus_self_identify",
)
name = chat_manager.get_stream_name(self.subheartflow_id)
@ -101,12 +102,24 @@ class SelfProcessor(BaseProcessor):
tuple: (current_mind, past_mind, prompt) 当前想法过去的想法列表和使用的prompt
"""
for observation in observations:
if isinstance(observation, ChattingObservation):
is_group_chat = observation.is_group_chat
chat_target_info = observation.chat_target_info
chat_target_name = "对方" # 私聊默认名称
person_list = observation.person_list
memory_str = ""
if running_memorys:
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
for running_memory in running_memorys:
memory_str += f"{running_memory['topic']}: {running_memory['content']}\n"
relation_prompt = ""
for person in person_list:
if len(person) >= 3 and person[0] and person[1]:
relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True)
if observations is None:
observations = []
for observation in observations:
@ -135,9 +148,16 @@ class SelfProcessor(BaseProcessor):
personality_block = individuality.get_personality_prompt(x_person=2, level=2)
identity_block = individuality.get_identity_prompt(x_person=2, level=2)
relation_prompt = ""
if is_group_chat:
relation_prompt_init = "在这个群聊中,你:\n"
else:
relation_prompt_init = ""
for person in person_list:
relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True)
if relation_prompt:
relation_prompt = relation_prompt_init + relation_prompt
else:
relation_prompt = relation_prompt_init + "没有特别在意的人\n"
prompt = (await global_prompt_manager.get_prompt_async("indentify_prompt")).format(
name_block=name_block,
@ -149,6 +169,8 @@ class SelfProcessor(BaseProcessor):
chat_observe_info=chat_observe_info,
)
# print(prompt)
content = ""
try:
content, _ = await self.llm_model.generate_response_async(prompt=prompt)
@ -164,7 +186,7 @@ class SelfProcessor(BaseProcessor):
content = ""
# 记录初步思考结果
logger.debug(f"{self.log_prefix} 自我识别prompt: \n{prompt}\n")
logger.info(f"{self.log_prefix} 自我识别结果: {content}")
logger.info(f"{self.log_prefix} 自我认知: {content}")
return content

View File

@ -49,9 +49,9 @@ class ToolProcessor(BaseProcessor):
self.subheartflow_id = subheartflow_id
self.log_prefix = f"[{subheartflow_id}:ToolExecutor] "
self.llm_model = LLMRequest(
model=global_config.model.tool_use,
model=global_config.model.focus_tool_use,
max_tokens=500,
request_type="tool_execution",
request_type="focus_tool",
)
self.structured_info = []
@ -75,10 +75,12 @@ class ToolProcessor(BaseProcessor):
result, used_tools, prompt = await self.execute_tools(observation, running_memorys)
# 更新WorkingObservation中的结构化信息
logger.debug(f"工具调用结果: {result}")
for observation in observations:
if isinstance(observation, StructureObservation):
for structured_info in result:
logger.debug(f"{self.log_prefix} 更新WorkingObservation中的结构化信息: {structured_info}")
# logger.debug(f"{self.log_prefix} 更新WorkingObservation中的结构化信息: {structured_info}")
observation.add_structured_info(structured_info)
working_infos = observation.get_observe_info()
@ -87,7 +89,12 @@ class ToolProcessor(BaseProcessor):
structured_info = StructuredInfo()
if working_infos:
for working_info in working_infos:
structured_info.set_info(working_info.get("type"), working_info.get("content"))
# print(f"working_info: {working_info}")
# print(f"working_info.get('type'): {working_info.get('type')}")
# print(f"working_info.get('content'): {working_info.get('content')}")
structured_info.set_info(key=working_info.get("type"), value=working_info.get("content"))
# info = structured_info.get_processed_info()
# print(f"info: {info}")
return [structured_info]
@ -155,7 +162,7 @@ class ToolProcessor(BaseProcessor):
)
# 调用LLM专注于工具使用
# logger.debug(f"开始执行工具调用{prompt}")
logger.debug(f"开始执行工具调用{prompt}")
response, _, tool_calls = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools)
logger.debug(f"获取到工具原始输出:\n{tool_calls}")

View File

@ -61,10 +61,10 @@ class WorkingMemoryProcessor(BaseProcessor):
self.subheartflow_id = subheartflow_id
self.llm_model = LLMRequest(
model=global_config.model.sub_heartflow,
temperature=global_config.model.sub_heartflow["temp"],
model=global_config.model.focus_chat_mind,
temperature=global_config.model.focus_chat_mind["temp"],
max_tokens=800,
request_type="working_memory",
request_type="focus_working_memory",
)
name = chat_manager.get_stream_name(self.subheartflow_id)
@ -93,7 +93,7 @@ class WorkingMemoryProcessor(BaseProcessor):
# chat_info_truncate = observation.talking_message_str_truncate
if not working_memory:
logger.warning(f"{self.log_prefix} 没有找到工作记忆对象")
logger.debug(f"{self.log_prefix} 没有找到工作记忆对象")
mind_info = MindInfo()
return [mind_info]
except Exception as e:
@ -180,7 +180,7 @@ class WorkingMemoryProcessor(BaseProcessor):
working_memory_info.add_working_memory(memory_str)
logger.debug(f"{self.log_prefix} 取得工作记忆: {memory_str}")
else:
logger.warning(f"{self.log_prefix} 没有找到工作记忆")
logger.debug(f"{self.log_prefix} 没有找到工作记忆")
# 根据聊天内容添加新记忆
if new_memory:

View File

@ -4,24 +4,58 @@ from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservati
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.common.logger_manager import get_logger
from src.chat.utils.prompt_builder import Prompt
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from datetime import datetime
from src.chat.memory_system.Hippocampus import HippocampusManager
from typing import List, Dict
import difflib
import json
from json_repair import repair_json
logger = get_logger("memory_activator")
def get_keywords_from_json(json_str):
"""
从JSON字符串中提取关键词列表
Args:
json_str: JSON格式的字符串
Returns:
List[str]: 关键词列表
"""
try:
# 使用repair_json修复JSON格式
fixed_json = repair_json(json_str)
# 如果repair_json返回的是字符串需要解析为Python对象
if isinstance(fixed_json, str):
result = json.loads(fixed_json)
else:
# 如果repair_json直接返回了字典对象直接使用
result = fixed_json
# 提取关键词
keywords = result.get("keywords", [])
return keywords
except Exception as e:
logger.error(f"解析关键词JSON失败: {e}")
return []
def init_prompt():
# --- Group Chat Prompt ---
memory_activator_prompt = """
你是一个记忆分析器你需要根据以下信息来进行会议
你是一个记忆分析器你需要根据以下信息来进行回忆
以下是一场聊天中的信息请根据这些信息总结出几个关键词作为记忆回忆的触发词
{obs_info_text}
历史关键词请避免重复提取这些关键词
{cached_keywords}
请输出一个json格式包含以下字段
{{
"keywords": ["关键词1", "关键词2", "关键词3",......]
@ -36,9 +70,10 @@ class MemoryActivator:
def __init__(self):
# TODO: API-Adapter修改标记
self.summary_model = LLMRequest(
model=global_config.model.summary, temperature=0.7, max_tokens=50, request_type="chat_observation"
model=global_config.model.memory_summary, temperature=0.7, max_tokens=50, request_type="chat_observation"
)
self.running_memory = []
self.cached_keywords = set() # 用于缓存历史关键词
async def activate_memory(self, observations) -> List[Dict]:
"""
@ -61,31 +96,47 @@ class MemoryActivator:
elif isinstance(observation, HFCloopObservation):
obs_info_text += observation.get_observe_info()
logger.debug(f"回忆待检索内容obs_info_text: {obs_info_text}")
# logger.debug(f"回忆待检索内容obs_info_text: {obs_info_text}")
# prompt = await global_prompt_manager.format_prompt(
# "memory_activator_prompt",
# obs_info_text=obs_info_text,
# )
# 将缓存的关键词转换为字符串用于prompt
cached_keywords_str = ", ".join(self.cached_keywords) if self.cached_keywords else "暂无历史关键词"
# logger.debug(f"prompt: {prompt}")
# response = await self.summary_model.generate_response(prompt)
# logger.debug(f"response: {response}")
# # 只取response的第一个元素字符串
# response_str = response[0]
# keywords = list(get_keywords_from_json(response_str))
# #调用记忆系统获取相关记忆
# related_memory = await HippocampusManager.get_instance().get_memory_from_topic(
# valid_keywords=keywords, max_memory_num=3, max_memory_length=2, max_depth=3
# )
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
text=obs_info_text, max_memory_num=5, max_memory_length=2, max_depth=3, fast_retrieval=True
prompt = await global_prompt_manager.format_prompt(
"memory_activator_prompt",
obs_info_text=obs_info_text,
cached_keywords=cached_keywords_str,
)
logger.debug(f"prompt: {prompt}")
response = await self.summary_model.generate_response(prompt)
logger.debug(f"response: {response}")
# 只取response的第一个元素字符串
response_str = response[0]
keywords = list(get_keywords_from_json(response_str))
# 更新关键词缓存
if keywords:
# 限制缓存大小最多保留10个关键词
if len(self.cached_keywords) > 10:
# 转换为列表,移除最早的关键词
cached_list = list(self.cached_keywords)
self.cached_keywords = set(cached_list[-8:])
# 添加新的关键词到缓存
self.cached_keywords.update(keywords)
logger.debug(f"更新关键词缓存: {self.cached_keywords}")
# 调用记忆系统获取相关记忆
related_memory = await HippocampusManager.get_instance().get_memory_from_topic(
valid_keywords=keywords, max_memory_num=3, max_memory_length=2, max_depth=3
)
# related_memory = await HippocampusManager.get_instance().get_memory_from_text(
# text=obs_info_text, max_memory_num=5, max_memory_length=2, max_depth=3, fast_retrieval=False
# )
# logger.debug(f"获取到的记忆: {related_memory}")
# 激活时所有已有记忆的duration+1达到3则移除

View File

@ -28,8 +28,6 @@ class ActionManager:
self._registered_actions: Dict[str, ActionInfo] = {}
# 当前正在使用的动作集合,默认加载默认动作
self._using_actions: Dict[str, ActionInfo] = {}
# 临时备份原始使用中的动作
self._original_actions_backup: Optional[Dict[str, ActionInfo]] = None
# 默认动作集,仅作为快照,用于恢复默认
self._default_actions: Dict[str, ActionInfo] = {}
@ -59,6 +57,7 @@ class ActionManager:
action_description: str = getattr(action_class, "action_description", "")
action_parameters: dict[str:str] = getattr(action_class, "action_parameters", {})
action_require: list[str] = getattr(action_class, "action_require", [])
associated_types: list[str] = getattr(action_class, "associated_types", [])
is_default: bool = getattr(action_class, "default", False)
if action_name and action_description:
@ -67,6 +66,7 @@ class ActionManager:
"description": action_description,
"parameters": action_parameters,
"require": action_require,
"associated_types": associated_types,
}
# 添加到所有已注册的动作
@ -158,9 +158,9 @@ class ActionManager:
Optional[BaseAction]: 创建的动作处理器实例如果动作名称未注册则返回None
"""
# 检查动作是否在当前使用的动作集中
if action_name not in self._using_actions:
logger.warning(f"当前不可用的动作类型: {action_name}")
return None
# if action_name not in self._using_actions:
# logger.warning(f"当前不可用的动作类型: {action_name}")
# return None
handler_class = _ACTION_REGISTRY.get(action_name)
if not handler_class:
@ -276,22 +276,20 @@ class ActionManager:
return True
def temporarily_remove_actions(self, actions_to_remove: List[str]) -> None:
"""临时移除使用集中的指定动作,备份原始使用集"""
if self._original_actions_backup is None:
self._original_actions_backup = self._using_actions.copy()
"""临时移除使用集中的指定动作"""
for name in actions_to_remove:
self._using_actions.pop(name, None)
def restore_actions(self) -> None:
"""恢复之前备份的原始使用集"""
if self._original_actions_backup is not None:
self._using_actions = self._original_actions_backup.copy()
self._original_actions_backup = None
"""恢复到默认动作集"""
logger.debug(
f"恢复动作集: 从 {list(self._using_actions.keys())} 恢复到默认动作集 {list(self._default_actions.keys())}"
)
self._using_actions = self._default_actions.copy()
def restore_default_actions(self) -> None:
"""恢复默认动作集到使用集"""
self._using_actions = self._default_actions.copy()
self._original_actions_backup = None
def get_action(self, action_name: str) -> Optional[Type[BaseAction]]:
"""

View File

@ -1,5 +1,6 @@
# 导入所有动作模块以确保装饰器被执行
from . import reply_action # noqa
from . import no_reply_action # noqa
from . import exit_focus_chat_action # noqa
# 在此处添加更多动作模块导入

View File

@ -66,6 +66,8 @@ class BaseAction(ABC):
self.action_parameters: dict = {}
self.action_require: list[str] = []
self.associated_types: list[str] = []
self.default: bool = False
self.action_data = action_data

View File

@ -5,8 +5,6 @@ from src.chat.focus_chat.planners.actions.base_action import BaseAction, registe
from typing import Tuple, List
from src.chat.heart_flow.observation.observation import Observation
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.heart_flow.heartflow import heartflow
from src.chat.heart_flow.sub_heartflow import ChatState
logger = get_logger("action_taken")
@ -27,7 +25,7 @@ class ExitFocusChatAction(BaseAction):
"当前内容不需要持续专注关注,你决定退出专注聊天",
"聊天内容已经完成,你决定退出专注聊天",
]
default = True
default = False
def __init__(
self,
@ -56,7 +54,6 @@ class ExitFocusChatAction(BaseAction):
self.observations = observations
self.log_prefix = log_prefix
self._shutting_down = shutting_down
self.chat_id = chat_stream.stream_id
async def handle_action(self) -> Tuple[bool, str]:
"""
@ -74,23 +71,8 @@ class ExitFocusChatAction(BaseAction):
try:
# 转换状态
status_message = ""
self.sub_heartflow = await heartflow.get_or_create_subheartflow(self.chat_id)
if self.sub_heartflow:
try:
# 转换为normal_chat状态
await self.sub_heartflow.change_chat_state(ChatState.CHAT)
status_message = "已成功切换到普通聊天模式"
logger.info(f"{self.log_prefix} {status_message}")
except Exception as e:
error_msg = f"切换到普通聊天模式失败: {str(e)}"
logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
else:
warning_msg = "未找到有效的sub heartflow实例无法切换状态"
logger.warning(f"{self.log_prefix} {warning_msg}")
return False, warning_msg
return True, status_message
command = "stop_focus_chat"
return True, status_message, command
except asyncio.CancelledError:
logger.info(f"{self.log_prefix} 处理 'exit_focus_chat' 时等待被中断 (CancelledError)")
@ -99,4 +81,4 @@ class ExitFocusChatAction(BaseAction):
error_msg = f"处理 'exit_focus_chat' 时发生错误: {str(e)}"
logger.error(f"{self.log_prefix} {error_msg}")
logger.error(traceback.format_exc())
return False, error_msg
return False, "", ""

View File

@ -111,7 +111,7 @@ class PluginAction(BaseAction):
return platform, user_id
# 提供简化的API方法
async def send_message(self, type: str, data: str, target: Optional[str] = "") -> bool:
async def send_message(self, type: str, data: str, target: Optional[str] = "", display_message: str = "") -> bool:
"""发送消息的简化方法
Args:
@ -158,6 +158,7 @@ class PluginAction(BaseAction):
success = await expressor.send_response_messages(
anchor_message=anchor_message,
response_set=response_set,
display_message=display_message,
)
return success

View File

@ -36,6 +36,9 @@ class ReplyAction(BaseAction):
"避免重复或评价自己的发言,不要和自己聊天",
"注意:回复尽量简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。不要有额外的符号,尽量简单简短",
]
associated_types: list[str] = ["text", "emoji"]
default = True
def __init__(
@ -97,6 +100,7 @@ class ReplyAction(BaseAction):
"emojis": "微笑" # 表情关键词列表(可选)
}
"""
logger.info(f"{self.log_prefix} 决定回复: {self.reasoning}")
# 从聊天观察获取锚定消息
chatting_observation: ChattingObservation = next(

View File

@ -37,12 +37,15 @@ def init_prompt():
{cycle_info_block}
请综合分析聊天内容和你看到的新消息参考聊天规划选择合适的action:
注意除了下面动作选项之外你在群聊里不能做其他任何事情这是你能力的边界现在请你选择合适的action:
{action_options_text}
你必须从上面列出的可用action中选择一个并说明原因
你的决策必须以严格的 JSON 格式输出且仅包含 JSON 内容不要有任何其他文字或解释
{moderation_prompt}
请你以下面格式输出你选择的action
{{
"action": "action_name",
@ -74,9 +77,9 @@ class ActionPlanner:
self.log_prefix = log_prefix
# LLM规划器配置
self.planner_llm = LLMRequest(
model=global_config.model.plan,
model=global_config.model.focus_planner,
max_tokens=1000,
request_type="action_planning", # 用于动作规划
request_type="focus_planner", # 用于动作规划
)
self.action_manager = action_manager
@ -104,6 +107,7 @@ class ActionPlanner:
add_actions = info.get_add_actions()
remove_actions = info.get_remove_actions()
reason = info.get_reason()
print(f"{self.log_prefix} 动作变更: {add_actions} {remove_actions} {reason}")
# 处理动作的增加
for action_name in add_actions:
@ -122,6 +126,10 @@ class ActionPlanner:
reasoning = f"之前选择的动作{action}已被移除,原因: {reason}"
# 继续处理其他信息
self_info = ""
current_mind = ""
cycle_info = ""
structured_info = ""
for info in all_plan_info:
if isinstance(info, ObsInfo):
observed_messages = info.get_talking_message()
@ -135,18 +143,25 @@ class ActionPlanner:
elif isinstance(info, SelfInfo):
self_info = info.get_processed_info()
elif isinstance(info, StructuredInfo):
_structured_info = info.get_data()
structured_info = info.get_processed_info()
# print(f"structured_info: {structured_info}")
elif not isinstance(info, ActionInfo): # 跳过已处理的ActionInfo
extra_info.append(info.get_processed_info())
# 获取当前可用的动作
current_available_actions = self.action_manager.get_using_actions()
# 如果没有可用动作直接返回no_reply
if not current_available_actions:
logger.warning(f"{self.log_prefix}没有可用的动作将使用no_reply")
# 如果没有可用动作或只有no_reply动作直接返回no_reply
if not current_available_actions or (
len(current_available_actions) == 1 and "no_reply" in current_available_actions
):
action = "no_reply"
reasoning = "没有可用的动作"
reasoning = "没有可用的动作" if not current_available_actions else "只有no_reply动作可用跳过规划"
logger.info(f"{self.log_prefix}{reasoning}")
self.action_manager.restore_actions()
logger.debug(
f"{self.log_prefix}沉默后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}"
)
return {
"action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning},
"current_mind": current_mind,
@ -160,7 +175,7 @@ class ActionPlanner:
chat_target_info=None,
observed_messages_str=observed_messages_str, # <-- Pass local variable
current_mind=current_mind, # <-- Pass argument
# structured_info=structured_info, # <-- Pass SubMind info
structured_info=structured_info, # <-- Pass SubMind info
current_available_actions=current_available_actions, # <-- Pass determined actions
cycle_info=cycle_info, # <-- Pass cycle info
extra_info=extra_info,
@ -169,8 +184,9 @@ class ActionPlanner:
# --- 调用 LLM (普通文本生成) ---
llm_content = None
try:
llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
llm_content, reasoning_content, _ = await self.planner_llm.generate_response(prompt=prompt)
logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
logger.debug(f"{self.log_prefix}[Planner] LLM 原始理由 响应 (预期): {reasoning_content}")
except Exception as req_e:
logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
reasoning = f"LLM 请求失败,你的模型出现问题: {req_e}"
@ -226,10 +242,10 @@ class ActionPlanner:
f"{self.log_prefix}规划器Prompt:\n{prompt}\n\n决策动作:{action},\n动作信息: '{action_data}'\n理由: {reasoning}"
)
# 恢复原始动作集
# 恢复到默认动作集
self.action_manager.restore_actions()
logger.debug(
f"{self.log_prefix}恢复了原始动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}"
f"{self.log_prefix}规划后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}"
)
action_result = {"action_type": action, "action_data": action_data, "reasoning": reasoning}
@ -249,6 +265,7 @@ class ActionPlanner:
chat_target_info: Optional[dict], # Now passed as argument
observed_messages_str: str,
current_mind: Optional[str],
structured_info: Optional[str],
current_available_actions: Dict[str, ActionInfo],
cycle_info: Optional[str],
extra_info: list[str],
@ -306,7 +323,13 @@ class ActionPlanner:
action_options_block += using_action_prompt
extra_info_block = "\n".join(extra_info)
extra_info_block = f"以下是一些额外的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是一些额外的信息,现在请你阅读以下内容,进行决策"
extra_info_block += f"\n{structured_info}"
if extra_info or structured_info:
extra_info_block = f"以下是一些额外的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是一些额外的信息,现在请你阅读以下内容,进行决策"
else:
extra_info_block = ""
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
prompt = planner_prompt_template.format(
@ -318,7 +341,9 @@ class ActionPlanner:
mind_info_block=mind_info_block,
cycle_info_block=cycle_info,
action_options_text=action_options_block,
# action_available_block=action_available_block,
extra_info_block=extra_info_block,
moderation_prompt=moderation_prompt_block,
)
return prompt

View File

@ -33,7 +33,10 @@ class MemoryManager:
self._id_map: Dict[str, MemoryItem] = {}
self.llm_summarizer = LLMRequest(
model=global_config.model.summary, temperature=0.3, max_tokens=512, request_type="memory_summarization"
model=global_config.model.focus_working_memory,
temperature=0.3,
max_tokens=512,
request_type="memory_summarization",
)
@property
@ -396,7 +399,7 @@ class MemoryManager:
try:
# 调用LLM修改总结、概括和要点
response, _ = await self.llm_summarizer.generate_response_async(prompt)
logger.info(f"精简记忆响应: {response}")
logger.debug(f"精简记忆响应: {response}")
# 使用repair_json处理响应
try:
# 修复JSON格式

View File

@ -2,25 +2,16 @@ import asyncio
import traceback
from typing import Optional, Coroutine, Callable, Any, List
from src.common.logger_manager import get_logger
from src.chat.heart_flow.mai_state_manager import MaiStateManager, MaiStateInfo
from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager
from src.config.config import global_config
logger = get_logger("background_tasks")
# 新增兴趣评估间隔
INTEREST_EVAL_INTERVAL_SECONDS = 5
# 新增聊天超时检查间隔
NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60
# 新增状态评估间隔
HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 20
# 新增私聊激活检查间隔
PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似设为5秒
CLEANUP_INTERVAL_SECONDS = 1200
STATE_UPDATE_INTERVAL_SECONDS = 60
LOG_INTERVAL_SECONDS = 3
async def _run_periodic_loop(
@ -55,19 +46,13 @@ class BackgroundTaskManager:
def __init__(
self,
mai_state_info: MaiStateInfo, # Needs current state info
mai_state_manager: MaiStateManager,
subheartflow_manager: SubHeartflowManager,
):
self.mai_state_info = mai_state_info
self.mai_state_manager = mai_state_manager
self.subheartflow_manager = subheartflow_manager
# Task references
self._state_update_task: Optional[asyncio.Task] = None
self._cleanup_task: Optional[asyncio.Task] = None
self._hf_judge_state_update_task: Optional[asyncio.Task] = None
self._into_focus_task: Optional[asyncio.Task] = None
self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用
self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
@ -80,42 +65,28 @@ class BackgroundTaskManager:
- 将任务引用保存到任务列表
"""
# 任务配置列表: (任务函数, 任务名称, 日志级别, 额外日志信息, 任务对象引用属性名)
task_configs = [
(
lambda: self._run_state_update_cycle(STATE_UPDATE_INTERVAL_SECONDS),
"debug",
f"聊天状态更新任务已启动 间隔:{STATE_UPDATE_INTERVAL_SECONDS}s",
"_state_update_task",
),
(
self._run_cleanup_cycle,
"info",
f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s",
"_cleanup_task",
),
# 新增私聊激活任务配置
(
# Use lambda to pass the interval to the runner function
lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS),
"debug",
f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s",
"_private_chat_activation_task",
),
]
task_configs = []
# 根据 chat_mode 条件添加专注评估任务
# 根据 chat_mode 条件添加其他任务
if not (global_config.chat.chat_mode == "normal"):
task_configs.append(
(
self._run_into_focus_cycle,
"debug", # 设为debug避免过多日志
f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s",
"_into_focus_task",
)
task_configs.extend(
[
(
self._run_cleanup_cycle,
"info",
f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s",
"_cleanup_task",
),
# 新增私聊激活任务配置
(
# Use lambda to pass the interval to the runner function
lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS),
"debug",
f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s",
"_private_chat_activation_task",
),
]
)
else:
logger.info("聊天模式为 normal跳过启动专注评估任务")
# 统一启动所有任务
for task_func, log_level, log_msg, task_attr_name in task_configs:
@ -161,33 +132,7 @@ class BackgroundTaskManager:
# 第三步:清空任务列表
self._tasks = [] # 重置任务列表
async def _perform_state_update_work(self):
"""执行状态更新工作"""
previous_status = self.mai_state_info.get_current_state()
next_state = self.mai_state_manager.check_and_decide_next_state(self.mai_state_info)
state_changed = False
if next_state is not None:
state_changed = self.mai_state_info.update_mai_status(next_state)
# 处理保持离线状态的特殊情况
if not state_changed and next_state == previous_status == self.mai_state_info.mai_status.OFFLINE:
self.mai_state_info.reset_state_timer()
logger.debug("[后台任务] 保持离线状态并重置计时器")
state_changed = True # 触发后续处理
if state_changed:
current_state = self.mai_state_info.get_current_state()
# 状态转换处理
if (
current_state == self.mai_state_info.mai_status.OFFLINE
and previous_status != self.mai_state_info.mai_status.OFFLINE
):
logger.info("检测到离线,停用所有子心流")
await self.subheartflow_manager.deactivate_all_subflows()
# 状态转换处理
async def _perform_cleanup_work(self):
"""执行子心流清理任务
@ -214,28 +159,11 @@ class BackgroundTaskManager:
# 记录最终清理结果
logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流")
# --- 新增兴趣评估工作函数 ---
async def _perform_into_focus_work(self):
"""执行一轮子心流兴趣评估与提升检查。"""
# 直接调用 subheartflow_manager 的方法,并传递当前状态信息
await self.subheartflow_manager.sbhf_normal_into_focus()
async def _run_state_update_cycle(self, interval: int):
await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work)
async def _run_cleanup_cycle(self):
await _run_periodic_loop(
task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
)
# --- 新增兴趣评估任务运行器 ---
async def _run_into_focus_cycle(self):
await _run_periodic_loop(
task_name="Into Focus",
interval=INTEREST_EVAL_INTERVAL_SECONDS,
task_func=self._perform_into_focus_work,
)
# 新增私聊激活任务运行器
async def _run_private_chat_activation_cycle(self, interval: int):
await _run_periodic_loop(

View File

@ -1,7 +1,6 @@
from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState
from src.common.logger_manager import get_logger
from typing import Any, Optional
from src.chat.heart_flow.mai_state_manager import MaiStateInfo, MaiStateManager
from typing import Any, Optional, List
from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager
from src.chat.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager
@ -16,17 +15,11 @@ class Heartflow:
"""
def __init__(self):
# 状态管理相关
self.current_state: MaiStateInfo = MaiStateInfo() # 当前状态信息
self.mai_state_manager: MaiStateManager = MaiStateManager() # 状态决策管理器
# 子心流管理 (在初始化时传入 current_state)
self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager(self.current_state)
self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager()
# 后台任务管理器 (整合所有定时任务)
self.background_task_manager: BackgroundTaskManager = BackgroundTaskManager(
mai_state_info=self.current_state,
mai_state_manager=self.mai_state_manager,
subheartflow_manager=self.subheartflow_manager,
)
@ -57,6 +50,23 @@ class Heartflow:
return heartfc_instance.get_cycle_history(last_n=history_len)
async def api_get_normal_chat_replies(self, subheartflow_id: str, limit: int = 10) -> Optional[List[dict]]:
"""获取子心流的NormalChat回复记录
Args:
subheartflow_id: 子心流ID
limit: 最大返回数量默认10条
Returns:
Optional[List[dict]]: 回复记录列表如果子心流不存在则返回None
"""
subheartflow = await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
if not subheartflow:
logger.warning(f"尝试获取不存在的子心流 {subheartflow_id} 的NormalChat回复记录")
return None
return subheartflow.get_normal_chat_recent_replies(limit)
async def heartflow_start_working(self):
"""启动后台任务"""
await self.background_task_manager.start_tasks()

View File

@ -1,200 +0,0 @@
import asyncio
from src.config.config import global_config
from typing import Optional, Dict
import traceback
from src.common.logger_manager import get_logger
from src.chat.message_receive.message import MessageRecv
import math
# 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0
logger = get_logger("interest_chatting")
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
MAX_REPLY_PROBABILITY = 1
class InterestChatting:
def __init__(
self,
decay_rate=global_config.focus_chat.default_decay_rate_per_second,
max_interest=MAX_INTEREST,
trigger_threshold=global_config.focus_chat.reply_trigger_threshold,
max_probability=MAX_REPLY_PROBABILITY,
):
# 基础属性初始化
self.interest_level: float = 0.0
self.decay_rate_per_second: float = decay_rate
self.max_interest: float = max_interest
self.trigger_threshold: float = trigger_threshold
self.max_reply_probability: float = max_probability
self.is_above_threshold: bool = False
# 任务相关属性初始化
self.update_task: Optional[asyncio.Task] = None
self._stop_event = asyncio.Event()
self._task_lock = asyncio.Lock()
self._is_running = False
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
self.update_interval = 1.0
self.above_threshold = False
self.start_hfc_probability = 0.0
async def initialize(self):
async with self._task_lock:
if self._is_running:
logger.debug("后台兴趣更新任务已在运行中。")
return
# 清理已完成或已取消的任务
if self.update_task and (self.update_task.done() or self.update_task.cancelled()):
self.update_task = None
if not self.update_task:
self._stop_event.clear()
self._is_running = True
self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval))
logger.debug("后台兴趣更新任务已创建并启动。")
def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
"""添加消息到兴趣字典
参数:
message: 接收到的消息
interest_value: 兴趣值
is_mentioned: 是否被提及
功能:
1. 将消息添加到兴趣字典
2. 更新最后交互时间
3. 如果字典长度超过10删除最旧的消息
"""
# 添加新消息
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
# 如果字典长度超过10删除最旧的消息
if len(self.interest_dict) > 10:
oldest_key = next(iter(self.interest_dict))
self.interest_dict.pop(oldest_key)
async def _calculate_decay(self):
"""计算兴趣值的衰减
参数:
current_time: 当前时间戳
处理逻辑:
1. 计算时间差
2. 处理各种异常情况(负值/零值)
3. 正常计算衰减
4. 更新最后更新时间
"""
# 处理极小兴趣值情况
if self.interest_level < 1e-9:
self.interest_level = 0.0
return
# 异常情况处理
if self.decay_rate_per_second <= 0:
logger.warning(f"衰减率({self.decay_rate_per_second})无效重置兴趣值为0")
self.interest_level = 0.0
return
# 正常衰减计算
try:
decay_factor = math.pow(self.decay_rate_per_second, self.update_interval)
self.interest_level *= decay_factor
except ValueError as e:
logger.error(
f"衰减计算错误: {e} 参数: 衰减率={self.decay_rate_per_second} 时间差={self.update_interval} 当前兴趣={self.interest_level}"
)
self.interest_level = 0.0
async def _update_reply_probability(self):
self.above_threshold = self.interest_level >= self.trigger_threshold
if self.above_threshold:
self.start_hfc_probability += PROBABILITY_INCREASE_RATE_PER_SECOND
else:
if self.start_hfc_probability > 0:
self.start_hfc_probability = max(0, self.start_hfc_probability - PROBABILITY_DECREASE_RATE_PER_SECOND)
async def increase_interest(self, value: float):
self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest)
async def decrease_interest(self, value: float):
self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0)
async def get_interest(self) -> float:
return self.interest_level
async def get_state(self) -> dict:
interest = self.interest_level # 直接使用属性值
return {
"interest_level": round(interest, 2),
"start_hfc_probability": round(self.start_hfc_probability, 4),
"above_threshold": self.above_threshold,
}
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。"""
try:
while not self._stop_event.is_set():
try:
if self.interest_level != 0:
await self._calculate_decay()
await self._update_reply_probability()
# 等待下一个周期或停止事件
await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
except asyncio.TimeoutError:
# 正常超时,继续循环
continue
except Exception as e:
logger.error(f"InterestChatting 更新循环出错: {e}")
logger.error(traceback.format_exc())
# 防止错误导致CPU飙升稍作等待
await asyncio.sleep(5)
except asyncio.CancelledError:
logger.info("InterestChatting 更新循环被取消。")
finally:
self._is_running = False
logger.info("InterestChatting 更新循环已停止。")
async def stop_updates(self):
"""停止后台更新任务,使用锁确保并发安全"""
async with self._task_lock:
if not self._is_running:
logger.debug("后台兴趣更新任务未运行。")
return
logger.info("正在停止 InterestChatting 后台更新任务...")
self._stop_event.set()
if self.update_task and not self.update_task.done():
try:
# 等待任务结束,设置超时
await asyncio.wait_for(self.update_task, timeout=5.0)
logger.info("InterestChatting 后台更新任务已成功停止。")
except asyncio.TimeoutError:
logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
self.update_task.cancel()
try:
await self.update_task # 等待取消完成
except asyncio.CancelledError:
logger.info("InterestChatting 后台更新任务已被取消。")
except Exception as e:
logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
finally:
self.update_task = None
self._is_running = False

View File

@ -1,135 +0,0 @@
import enum
import time
import random
from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger
from src.manager.mood_manager import mood_manager
logger = get_logger("mai_state")
class MaiState(enum.Enum):
"""
聊天状态:
OFFLINE: 不在线回复概率极低不会进行任何聊天
NORMAL_CHAT: 正常看手机回复概率较高会进行一些普通聊天和少量的专注聊天
FOCUSED_CHAT: 专注聊天回复概率极高会进行专注聊天和少量的普通聊天
"""
OFFLINE = "不在线"
NORMAL_CHAT = "正常看手机"
FOCUSED_CHAT = "专心看手机"
class MaiStateInfo:
def __init__(self):
self.mai_status: MaiState = MaiState.NORMAL_CHAT # 初始状态改为 NORMAL_CHAT
self.mai_status_history: List[Tuple[MaiState, float]] = [] # 历史状态,包含 状态,时间戳
self.last_status_change_time: float = time.time() # 状态最后改变时间
self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间
# Mood management is now part of MaiStateInfo
self.mood_manager = mood_manager # Use singleton instance
def update_mai_status(self, new_status: MaiState) -> bool:
"""
更新聊天状态
Args:
new_status: 新的 MaiState 状态
Returns:
bool: 如果状态实际发生了改变则返回 True否则返回 False
"""
if new_status != self.mai_status:
self.mai_status = new_status
current_time = time.time()
self.last_status_change_time = current_time
self.last_min_check_time = current_time # Reset 1-min check on any state change
self.mai_status_history.append((new_status, current_time))
logger.info(f"麦麦状态更新为: {self.mai_status.value}")
return True
else:
return False
def reset_state_timer(self):
"""
重置状态持续时间计时器和一分钟规则检查计时器
通常在状态保持不变但需要重新开始计时的情况下调用例如保持 OFFLINE
"""
current_time = time.time()
self.last_status_change_time = current_time
self.last_min_check_time = current_time # Also reset the 1-min check timer
logger.debug("MaiStateInfo 状态计时器已重置。")
def get_mood_prompt(self) -> str:
"""获取当前的心情提示词"""
# Delegate to the internal mood manager
return self.mood_manager.get_mood_prompt()
def get_current_state(self) -> MaiState:
"""获取当前的 MaiState"""
return self.mai_status
class MaiStateManager:
"""管理 Mai 的整体状态转换逻辑"""
def __init__(self):
pass
@staticmethod
def check_and_decide_next_state(current_state_info: MaiStateInfo) -> Optional[MaiState]:
"""
根据当前状态和规则检查是否需要转换状态并决定下一个状态
"""
current_time = time.time()
current_status = current_state_info.mai_status
time_in_current_status = current_time - current_state_info.last_status_change_time
next_state: Optional[MaiState] = None
def _resolve_offline(candidate_state: MaiState) -> MaiState:
if candidate_state == MaiState.OFFLINE:
return current_status
return candidate_state
if current_status == MaiState.OFFLINE:
logger.info("当前[离线],没看手机,思考要不要上线看看......")
elif current_status == MaiState.NORMAL_CHAT:
logger.info("当前在[正常看手机]思考要不要继续聊下去......")
elif current_status == MaiState.FOCUSED_CHAT:
logger.info("当前在[专心看手机]思考要不要继续聊下去......")
if next_state is None:
time_limit_exceeded = False
choices_list = []
weights = []
rule_id = ""
if current_status == MaiState.OFFLINE:
return None
elif current_status == MaiState.NORMAL_CHAT:
if time_in_current_status >= 300: # NORMAL_CHAT 最多持续 300 秒
time_limit_exceeded = True
rule_id = "2.3 (From NORMAL_CHAT)"
weights = [100]
choices_list = [MaiState.FOCUSED_CHAT]
elif current_status == MaiState.FOCUSED_CHAT:
if time_in_current_status >= 600: # FOCUSED_CHAT 最多持续 600 秒
time_limit_exceeded = True
rule_id = "2.4 (From FOCUSED_CHAT)"
weights = [100]
choices_list = [MaiState.NORMAL_CHAT]
if time_limit_exceeded:
next_state_candidate = random.choices(choices_list, weights=weights, k=1)[0]
resolved_candidate = _resolve_offline(next_state_candidate)
logger.debug(
f"规则{rule_id}:时间到,切换到 {next_state_candidate.value}resolve 为 {resolved_candidate.value}"
)
next_state = resolved_candidate
if next_state is not None and next_state != current_status:
return next_state
else:
return None

View File

@ -1,5 +1,4 @@
from datetime import datetime
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
import traceback
from src.chat.utils.chat_message_builder import (
@ -66,10 +65,6 @@ class ChattingObservation(Observation):
self.oldest_messages = []
self.oldest_messages_str = ""
self.compressor_prompt = ""
# TODO: API-Adapter修改标记
self.model_summary = LLMRequest(
model=global_config.model.observation, temperature=0.7, max_tokens=300, request_type="chat_observation"
)
async def initialize(self):
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id)

View File

@ -84,10 +84,4 @@ class HFCloopObservation:
else:
cycle_info_block += "\n你还没看过消息\n"
using_actions = self.action_manager.get_using_actions()
for action_name, action_info in using_actions.items():
action_description = action_info["description"]
cycle_info_block += f"\n你在聊天中可以使用{action_name},这个动作的描述是{action_description}\n"
cycle_info_block += "注意,除了上述动作选项之外,你在群聊里不能做其他任何事情,这是你能力的边界\n"
self.observe_info = cycle_info_block

View File

@ -26,7 +26,7 @@ class StructureObservation:
for structured_info in self.structured_info:
if structured_info.get("ttl") > 0:
structured_info["ttl"] -= 1
observed_structured_infos.append(structured_info)
logger.debug(f"观察到结构化信息仍旧在: {structured_info}")
observed_structured_infos.append(structured_info)
logger.debug(f"观察到结构化信息仍旧在: {structured_info}")
self.structured_info = observed_structured_infos

View File

@ -9,21 +9,20 @@ from src.chat.message_receive.message import MessageRecv
from src.chat.message_receive.chat_stream import chat_manager
from src.chat.focus_chat.heartFC_chat import HeartFChatting
from src.chat.normal_chat.normal_chat import NormalChat
from src.chat.heart_flow.mai_state_manager import MaiStateInfo
from src.chat.heart_flow.chat_state_info import ChatState, ChatStateInfo
from .utils_chat import get_chat_type_and_target_info
from .interest_chatting import InterestChatting
from src.config.config import global_config
from rich.traceback import install
logger = get_logger("sub_heartflow")
install(extra_lines=3)
class SubHeartflow:
def __init__(
self,
subheartflow_id,
mai_states: MaiStateInfo,
):
"""子心流初始化函数
@ -36,9 +35,6 @@ class SubHeartflow:
self.subheartflow_id = subheartflow_id
self.chat_id = subheartflow_id
# 麦麦的状态
self.mai_states = mai_states
# 这个聊天流的状态
self.chat_state: ChatStateInfo = ChatStateInfo()
self.chat_state_changed_time: float = time.time()
@ -50,8 +46,8 @@ class SubHeartflow:
self.chat_target_info: Optional[dict] = None
# --- End Initialization ---
# 兴趣检测器
self.interest_chatting: InterestChatting = InterestChatting()
# 兴趣消息集合
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
# 活动状态管理
self.should_stop = False # 停止标志
@ -82,18 +78,13 @@ class SubHeartflow:
logger.debug(
f"SubHeartflow {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}"
)
# --- End using utility function ---
# Initialize interest system (existing logic)
await self.interest_chatting.initialize()
logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。")
# 根据配置决定初始状态
if global_config.chat.chat_mode == "focus":
logger.info(f"{self.log_prefix} 配置为 focus 模式,将直接尝试进入 FOCUSED 状态。")
logger.debug(f"{self.log_prefix} 配置为 focus 模式,将直接尝试进入 FOCUSED 状态。")
await self.change_chat_state(ChatState.FOCUSED)
else: # "auto" 或其他模式保持原有逻辑或默认为 NORMAL
logger.info(f"{self.log_prefix} 配置为 auto 或其他模式,将尝试进入 NORMAL 状态。")
logger.debug(f"{self.log_prefix} 配置为 auto 或其他模式,将尝试进入 NORMAL 状态。")
await self.change_chat_state(ChatState.NORMAL)
def update_last_chat_state_time(self):
@ -129,7 +120,12 @@ class SubHeartflow:
return False
# 在 rewind 为 True 或 NormalChat 实例尚未创建时,创建新实例
if rewind or not self.normal_chat_instance:
self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict())
# 提供回调函数用于接收需要切换到focus模式的通知
self.normal_chat_instance = NormalChat(
chat_stream=chat_stream,
interest_dict=self.interest_dict,
on_switch_to_focus_callback=self._handle_switch_to_focus_request,
)
# 进行异步初始化
await self.normal_chat_instance.initialize()
@ -144,6 +140,38 @@ class SubHeartflow:
self.normal_chat_instance = None # 启动/初始化失败,清理实例
return False
async def _handle_switch_to_focus_request(self) -> None:
"""
处理来自NormalChat的切换到focus模式的请求
Args:
stream_id: 请求切换的stream_id
"""
logger.info(f"{self.log_prefix} 收到NormalChat请求切换到focus模式")
# 切换到focus模式
current_state = self.chat_state.chat_status
if current_state == ChatState.NORMAL:
await self.change_chat_state(ChatState.FOCUSED)
logger.info(f"{self.log_prefix} 已根据NormalChat请求从NORMAL切换到FOCUSED状态")
else:
logger.warning(f"{self.log_prefix} 当前状态为{current_state.value}无法切换到FOCUSED状态")
async def _handle_stop_focus_chat_request(self) -> None:
"""
处理来自HeartFChatting的停止focus模式的请求
当收到stop_focus_chat命令时被调用
"""
logger.info(f"{self.log_prefix} 收到HeartFChatting请求停止focus模式")
# 切换到normal模式
current_state = self.chat_state.chat_status
if current_state == ChatState.FOCUSED:
await self.change_chat_state(ChatState.NORMAL)
logger.info(f"{self.log_prefix} 已根据HeartFChatting请求从FOCUSED切换到NORMAL状态")
else:
logger.warning(f"{self.log_prefix} 当前状态为{current_state.value}无法切换到NORMAL状态")
async def _stop_heart_fc_chat(self):
"""停止并清理 HeartFChatting 实例"""
if self.heart_fc_instance:
@ -160,7 +188,7 @@ class SubHeartflow:
async def _start_heart_fc_chat(self) -> bool:
"""启动 HeartFChatting 实例,确保 NormalChat 已停止"""
await self._stop_normal_chat() # 确保普通聊天监控已停止
self.clear_interest_dict() # 清理兴趣字典,准备专注聊天
self.interest_dict.clear()
log_prefix = self.log_prefix
# 如果实例已存在,检查其循环任务状态
@ -189,6 +217,7 @@ class SubHeartflow:
self.heart_fc_instance = HeartFChatting(
chat_id=self.subheartflow_id,
observations=self.observations,
on_stop_focus_chat=self._handle_stop_focus_chat_request,
)
# 初始化并启动 HeartFChatting
@ -237,7 +266,7 @@ class SubHeartflow:
elif new_state == ChatState.ABSENT:
logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...")
self.clear_interest_dict()
self.interest_dict.clear()
await self._stop_normal_chat()
await self._stop_heart_fc_chat()
state_changed = True
@ -247,9 +276,9 @@ class SubHeartflow:
self.update_last_chat_state_time()
self.history_chat_state.append((current_state, self.chat_state_last_time))
logger.info(
f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {int(self.chat_state_last_time)} 秒) 变更为 {new_state.value}"
)
# logger.info(
# f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {int(self.chat_state_last_time)} 秒) 变更为 {new_state.value}"
# )
self.chat_state.chat_status = new_state
self.chat_state_last_time = 0
@ -278,25 +307,35 @@ class SubHeartflow:
logger.warning(f"SubHeartflow {self.subheartflow_id} 没有找到有效的 ChattingObservation")
return None
async def get_interest_state(self) -> dict:
return await self.interest_chatting.get_state()
def get_normal_chat_last_speak_time(self) -> float:
if self.normal_chat_instance:
return self.normal_chat_instance.last_speak_time
return 0
def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]:
return self.interest_chatting.interest_dict
def get_normal_chat_recent_replies(self, limit: int = 10) -> List[dict]:
"""获取NormalChat实例的最近回复记录
def clear_interest_dict(self):
self.interest_chatting.interest_dict.clear()
Args:
limit: 最大返回数量默认10条
Returns:
List[dict]: 最近的回复记录列表如果没有NormalChat实例则返回空列表
"""
if self.normal_chat_instance:
return self.normal_chat_instance.get_recent_replies(limit)
return []
def add_message_to_normal_chat_cache(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
# 如果字典长度超过10删除最旧的消息
if len(self.interest_dict) > 30:
oldest_key = next(iter(self.interest_dict))
self.interest_dict.pop(oldest_key)
async def get_full_state(self) -> dict:
"""获取子心流的完整状态,包括兴趣、思维和聊天状态。"""
interest_state = await self.get_interest_state()
return {
"interest_state": interest_state,
"interest_state": "interest_state",
"chat_state": self.chat_state.chat_status.value,
"chat_state_changed_time": self.chat_state_changed_time,
}
@ -314,11 +353,6 @@ class SubHeartflow:
await self._stop_normal_chat()
await self._stop_heart_fc_chat()
# 停止兴趣更新任务
if self.interest_chatting:
logger.info(f"{self.log_prefix} 停止兴趣系统后台任务...")
await self.interest_chatting.stop_updates()
# 取消可能存在的旧后台任务 (self.task)
if self.task and not self.task.done():
logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")

View File

@ -1,11 +1,9 @@
import asyncio
import time
import random
from typing import Dict, Any, Optional, List
from src.common.logger_manager import get_logger
from src.chat.message_receive.chat_stream import chat_manager
from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState
from src.chat.heart_flow.mai_state_manager import MaiStateInfo
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
@ -55,10 +53,9 @@ async def _try_set_subflow_absent_internal(subflow: "SubHeartflow", log_prefix:
class SubHeartflowManager:
"""管理所有活跃的 SubHeartflow 实例。"""
def __init__(self, mai_state_info: MaiStateInfo):
def __init__(self):
self.subheartflows: Dict[Any, "SubHeartflow"] = {}
self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问
self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例
async def force_change_state(self, subflow_id: Any, target_state: ChatState) -> bool:
"""强制改变指定子心流的状态"""
@ -92,16 +89,12 @@ class SubHeartflowManager:
if subflow.should_stop:
logger.warning(f"尝试获取已停止的子心流 {subheartflow_id},正在重新激活")
subflow.should_stop = False # 重置停止标志
subflow.last_active_time = time.time() # 更新活跃时间
# logger.debug(f"获取到已存在的子心流: {subheartflow_id}")
return subflow
try:
# 初始化子心流, 传入 mai_state_info
new_subflow = SubHeartflow(
subheartflow_id,
self.mai_state_info,
)
# 首先创建并添加聊天观察者
@ -186,41 +179,41 @@ class SubHeartflowManager:
f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。"
)
async def sbhf_normal_into_focus(self):
"""评估子心流兴趣度满足条件则提升到FOCUSED状态基于start_hfc_probability"""
try:
for sub_hf in list(self.subheartflows.values()):
flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
# async def sbhf_normal_into_focus(self):
# """评估子心流兴趣度满足条件则提升到FOCUSED状态基于start_hfc_probability"""
# try:
# for sub_hf in list(self.subheartflows.values()):
# flow_id = sub_hf.subheartflow_id
# stream_name = chat_manager.get_stream_name(flow_id) or flow_id
# 跳过已经是FOCUSED状态的子心流
if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
continue
# # 跳过已经是FOCUSED状态的子心流
# if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
# continue
if sub_hf.interest_chatting.start_hfc_probability == 0:
continue
else:
logger.debug(
f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}"
)
# if sub_hf.interest_chatting.start_hfc_probability == 0:
# continue
# else:
# logger.debug(
# f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}"
# )
if random.random() >= sub_hf.interest_chatting.start_hfc_probability:
continue
# if random.random() >= sub_hf.interest_chatting.start_hfc_probability:
# continue
# 获取最新状态并执行提升
current_subflow = self.subheartflows.get(flow_id)
if not current_subflow:
continue
# # 获取最新状态并执行提升
# current_subflow = self.subheartflows.get(flow_id)
# if not current_subflow:
# continue
logger.info(
f"{stream_name} 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})"
)
# logger.info(
# f"{stream_name} 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})"
# )
# 执行状态提升
await current_subflow.change_chat_state(ChatState.FOCUSED)
# # 执行状态提升
# await current_subflow.change_chat_state(ChatState.FOCUSED)
except Exception as e:
logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
# except Exception as e:
# logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
async def sbhf_focus_into_normal(self, subflow_id: Any):
"""
@ -249,7 +242,7 @@ class SubHeartflowManager:
)
try:
# 从HFC到CHAT时清空兴趣字典
subflow.clear_interest_dict()
subflow.interest_dict.clear()
await subflow.change_chat_state(target_state)
final_state = subflow.chat_state.chat_status
if final_state == target_state:

View File

@ -193,7 +193,6 @@ class MemoryGraph:
class Hippocampus:
def __init__(self):
self.memory_graph = MemoryGraph()
self.llm_topic_judge = None
self.model_summary = None
self.entorhinal_cortex = None
self.parahippocampal_gyrus = None
@ -205,8 +204,7 @@ class Hippocampus:
# 从数据库加载记忆图
self.entorhinal_cortex.sync_memory_from_db()
# TODO: API-Adapter修改标记
self.llm_topic_judge = LLMRequest(global_config.model.topic_judge, request_type="memory")
self.model_summary = LLMRequest(global_config.model.summary, request_type="memory")
self.model_summary = LLMRequest(global_config.model.memory_summary, request_type="memory")
def get_all_node_names(self) -> list:
"""获取记忆图中所有节点的名字列表"""
@ -344,7 +342,7 @@ class Hippocampus:
# 使用LLM提取关键词
topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量
# logger.info(f"提取关键词数量: {topic_num}")
topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(text, topic_num))
topics_response = await self.model_summary.generate_response(self.find_topic_llm(text, topic_num))
# 提取关键词
keywords = re.findall(r"<([^>]+)>", topics_response[0])
@ -528,12 +526,12 @@ class Hippocampus:
if not keywords:
return []
# logger.info(f"提取的关键词: {', '.join(keywords)}")
logger.info(f"提取的关键词: {', '.join(keywords)}")
# 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
if not valid_keywords:
# logger.info("没有找到有效的关键词节点")
logger.info("没有找到有效的关键词节点")
return []
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
@ -699,7 +697,7 @@ class Hippocampus:
# 使用LLM提取关键词
topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量
# logger.info(f"提取关键词数量: {topic_num}")
topics_response = await self.llm_topic_judge.generate_response(self.find_topic_llm(text, topic_num))
topics_response = await self.model_summary.generate_response(self.find_topic_llm(text, topic_num))
# 提取关键词
keywords = re.findall(r"<([^>]+)>", topics_response[0])
@ -1126,7 +1124,7 @@ class ParahippocampalGyrus:
# 2. 使用LLM提取关键主题
topic_num = self.hippocampus.calculate_topic_num(input_text, compress_rate)
topics_response = await self.hippocampus.llm_topic_judge.generate_response(
topics_response = await self.hippocampus.model_summary.generate_response(
self.hippocampus.find_topic_llm(input_text, topic_num)
)

View File

@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
import asyncio
import time
import sys
import os
# 添加项目根目录到系统路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
from src.chat.memory_system.Hippocampus import HippocampusManager
from rich.traceback import install
install(extra_lines=3)
async def test_memory_system():
"""测试记忆系统的主要功能"""
try:
# 初始化记忆系统
print("开始初始化记忆系统...")
hippocampus_manager = HippocampusManager.get_instance()
hippocampus_manager.initialize()
print("记忆系统初始化完成")
# 测试记忆构建
# print("开始测试记忆构建...")
# await hippocampus_manager.build_memory()
# print("记忆构建完成")
# 测试记忆检索
test_text = "千石可乐在群里聊天"
# test_text = '''千石可乐分不清AI的陪伴和人类的陪伴,是这样吗?'''
print(f"开始测试记忆检索,测试文本: {test_text}\n")
memories = await hippocampus_manager.get_memory_from_text(
text=test_text, max_memory_num=3, max_memory_length=2, max_depth=3, fast_retrieval=False
)
await asyncio.sleep(1)
print("检索到的记忆:")
for topic, memory_items in memories:
print(f"主题: {topic}")
print(f"- {memory_items}")
except Exception as e:
print(f"测试过程中出现错误: {e}")
raise
async def main():
"""主函数"""
try:
start_time = time.time()
await test_memory_system()
end_time = time.time()
print(f"测试完成,总耗时: {end_time - start_time:.2f}")
except Exception as e:
print(f"程序执行出错: {e}")
raise
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,365 +0,0 @@
# -*- coding: utf-8 -*-
import os
import sys
import time
from pathlib import Path
import datetime
from rich.console import Console
from Hippocampus import Hippocampus # 海马体和记忆图
from dotenv import load_dotenv
from rich.traceback import install
install(extra_lines=3)
"""
我想 总有那么一个瞬间
你会想和某天才变态少女助手一样
往Bot的海马体里插上几个电极 不是吗
Let's do some dirty job.
"""
# 获取当前文件的目录
current_dir = Path(__file__).resolve().parent
# 获取项目根目录(上三层目录)
project_root = current_dir.parent.parent.parent
# env.dev文件路径
env_path = project_root / ".env.dev"
# from chat.config import global_config
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
sys.path.append(root_path)
from src.common.logger import get_module_logger # noqa E402
from common.database.database import db # noqa E402
logger = get_module_logger("mem_alter")
console = Console()
# 加载环境变量
if env_path.exists():
logger.info(f"{env_path} 加载环境变量")
load_dotenv(env_path)
else:
logger.warning(f"未找到环境变量文件: {env_path}")
logger.info("将使用默认配置")
# 查询节点信息
def query_mem_info(hippocampus: Hippocampus):
while True:
query = input("\n请输入新的查询概念(输入'退出'以结束):")
if query.lower() == "退出":
break
items_list = hippocampus.memory_graph.get_related_item(query)
if items_list:
have_memory = False
first_layer, second_layer = items_list
if first_layer:
have_memory = True
print("\n直接相关的记忆:")
for item in first_layer:
print(f"- {item}")
if second_layer:
have_memory = True
print("\n间接相关的记忆:")
for item in second_layer:
print(f"- {item}")
if not have_memory:
print("\n未找到相关记忆。")
else:
print("未找到相关记忆。")
# 增加概念节点
def add_mem_node(hippocampus: Hippocampus):
while True:
concept = input("请输入节点概念名:\n")
result = db.graph_data.nodes.count_documents({"concept": concept})
if result != 0:
console.print("[yellow]已存在名为“{concept}”的节点,行为已取消[/yellow]")
continue
memory_items = list()
while True:
context = input("请输入节点描述信息(输入'终止'以结束)")
if context.lower() == "终止":
break
memory_items.append(context)
current_time = datetime.datetime.now().timestamp()
hippocampus.memory_graph.G.add_node(
concept, memory_items=memory_items, created_time=current_time, last_modified=current_time
)
# 删除概念节点(及连接到它的边)
def remove_mem_node(hippocampus: Hippocampus):
concept = input("请输入节点概念名:\n")
result = db.graph_data.nodes.count_documents({"concept": concept})
if result == 0:
console.print(f"[red]不存在名为“{concept}”的节点[/red]")
edges = db.graph_data.edges.find({"$or": [{"source": concept}, {"target": concept}]})
for edge in edges:
console.print(f"[yellow]存在边“{edge['source']} -> {edge['target']}”, 请慎重考虑[/yellow]")
console.print(f"[yellow]确定要移除名为“{concept}”的节点以及其相关边吗[/yellow]")
destory = console.input(f"[red]请输入“{concept}”以删除节点 其他输入将被视为取消操作[/red]\n")
if destory == concept:
hippocampus.memory_graph.G.remove_node(concept)
else:
logger.info("[green]删除操作已取消[/green]")
# 增加节点间边
def add_mem_edge(hippocampus: Hippocampus):
while True:
source = input("请输入 **第一个节点** 名称(输入'退出'以结束):\n")
if source.lower() == "退出":
break
if db.graph_data.nodes.count_documents({"concept": source}) == 0:
console.print(f"[yellow]“{source}”节点不存在,操作已取消。[/yellow]")
continue
target = input("请输入 **第二个节点** 名称:\n")
if db.graph_data.nodes.count_documents({"concept": target}) == 0:
console.print(f"[yellow]“{target}”节点不存在,操作已取消。[/yellow]")
continue
if source == target:
console.print(f"[yellow]试图创建“{source} <-> {target}”自环,操作已取消。[/yellow]")
continue
hippocampus.memory_graph.connect_dot(source, target)
edge = hippocampus.memory_graph.G.get_edge_data(source, target)
if edge["strength"] == 1:
console.print(f"[green]成功创建边“{source} <-> {target}默认权重1[/green]")
else:
console.print(
f"[yellow]边“{source} <-> {target}”已存在,"
f"更新权重: {edge['strength'] - 1} <-> {edge['strength']}[/yellow]"
)
# 删除节点间边
def remove_mem_edge(hippocampus: Hippocampus):
while True:
source = input("请输入 **第一个节点** 名称(输入'退出'以结束):\n")
if source.lower() == "退出":
break
if db.graph_data.nodes.count_documents({"concept": source}) == 0:
console.print("[yellow]“{source}”节点不存在,操作已取消。[/yellow]")
continue
target = input("请输入 **第二个节点** 名称:\n")
if db.graph_data.nodes.count_documents({"concept": target}) == 0:
console.print("[yellow]“{target}”节点不存在,操作已取消。[/yellow]")
continue
if source == target:
console.print("[yellow]试图创建“{source} <-> {target}”自环,操作已取消。[/yellow]")
continue
edge = hippocampus.memory_graph.G.get_edge_data(source, target)
if edge is None:
console.print("[yellow]边“{source} <-> {target}”不存在,操作已取消。[/yellow]")
continue
else:
accept = console.input("[orange]请输入“确认”以确认删除操作(其他输入视为取消)[/orange]\n")
if accept.lower() == "确认":
hippocampus.memory_graph.G.remove_edge(source, target)
console.print(f"[green]边“{source} <-> {target}”已删除。[green]")
# 修改节点信息
def alter_mem_node(hippocampus: Hippocampus):
batch_environment = dict()
while True:
concept = input("请输入节点概念名(输入'终止'以结束):\n")
if concept.lower() == "终止":
break
_, node = hippocampus.memory_graph.get_dot(concept)
if node is None:
console.print(f"[yellow]“{concept}”节点不存在,操作已取消。[/yellow]")
continue
console.print("[yellow]注意,请确保你知道自己在做什么[/yellow]")
console.print("[yellow]你将获得一个执行任意代码的环境[/yellow]")
console.print("[red]你已经被警告过了。[/red]\n")
node_environment = {"concept": "<节点名>", "memory_items": "<记忆文本数组>"}
console.print(
"[green]环境变量中会有env与batchEnv两个dict, env在切换节点时会清空, batchEnv在操作终止时才会清空[/green]"
)
console.print(
f"[green] env 会被初始化为[/green]\n{node_environment}\n[green]且会在用户代码执行完毕后被提交 [/green]"
)
console.print(
"[yellow]为便于书写临时脚本请手动在输入代码通过Ctrl+C等方式触发KeyboardInterrupt来结束代码执行[/yellow]"
)
# 拷贝数据以防操作炸了
node_environment = dict(node)
node_environment["concept"] = concept
while True:
def user_exec(script, env, batch_env):
return eval(script, env, batch_env)
try:
command = console.input()
except KeyboardInterrupt:
# 稍微防一下小天才
try:
if isinstance(node_environment["memory_items"], list):
node["memory_items"] = node_environment["memory_items"]
else:
raise Exception
except Exception as e:
console.print(
f"[red]我不知道你做了什么但显然nodeEnviroment['memory_items']已经不是个数组了,"
f"操作已取消: {str(e)}[/red]"
)
break
try:
user_exec(command, node_environment, batch_environment)
except Exception as e:
console.print(e)
console.print(
"[red]自定义代码执行时发生异常,已捕获,请重试(可通过 console.print(locals()) 检查环境状态)[/red]"
)
# 修改边信息
def alter_mem_edge(hippocampus: Hippocampus):
batch_enviroment = dict()
while True:
source = input("请输入 **第一个节点** 名称(输入'终止'以结束):\n")
if source.lower() == "终止":
break
if hippocampus.memory_graph.get_dot(source) is None:
console.print(f"[yellow]“{source}”节点不存在,操作已取消。[/yellow]")
continue
target = input("请输入 **第二个节点** 名称:\n")
if hippocampus.memory_graph.get_dot(target) is None:
console.print(f"[yellow]“{target}”节点不存在,操作已取消。[/yellow]")
continue
edge = hippocampus.memory_graph.G.get_edge_data(source, target)
if edge is None:
console.print(f"[yellow]边“{source} <-> {target}”不存在,操作已取消。[/yellow]")
continue
console.print("[yellow]注意,请确保你知道自己在做什么[/yellow]")
console.print("[yellow]你将获得一个执行任意代码的环境[/yellow]")
console.print("[red]你已经被警告过了。[/red]\n")
edge_environment = {"source": "<节点名>", "target": "<节点名>", "strength": "<强度值,装在一个list里>"}
console.print(
"[green]环境变量中会有env与batchEnv两个dict, env在切换节点时会清空, batchEnv在操作终止时才会清空[/green]"
)
console.print(
f"[green] env 会被初始化为[/green]\n{edge_environment}\n[green]且会在用户代码执行完毕后被提交 [/green]"
)
console.print(
"[yellow]为便于书写临时脚本请手动在输入代码通过Ctrl+C等方式触发KeyboardInterrupt来结束代码执行[/yellow]"
)
# 拷贝数据以防操作炸了
edge_environment["strength"] = [edge["strength"]]
edge_environment["source"] = source
edge_environment["target"] = target
while True:
def user_exec(script, env, batch_env):
return eval(script, env, batch_env)
try:
command = console.input()
except KeyboardInterrupt:
# 稍微防一下小天才
try:
if isinstance(edge_environment["strength"][0], int):
edge["strength"] = edge_environment["strength"][0]
else:
raise Exception
except Exception as e:
console.print(
f"[red]我不知道你做了什么但显然edgeEnviroment['strength']已经不是个int了"
f"操作已取消: {str(e)}[/red]"
)
break
try:
user_exec(command, edge_environment, batch_enviroment)
except Exception as e:
console.print(e)
console.print(
"[red]自定义代码执行时发生异常,已捕获,请重试(可通过 console.print(locals()) 检查环境状态)[/red]"
)
async def main():
start_time = time.time()
# 创建海马体
hippocampus = Hippocampus()
# 从数据库同步数据
hippocampus.entorhinal_cortex.sync_memory_from_db()
end_time = time.time()
logger.info(f"\033[32m[加载海马体耗时: {end_time - start_time:.2f} 秒]\033[0m")
while True:
try:
query = int(
input(
"""请输入操作类型
0 -> 查询节点; 1 -> 增加节点; 2 -> 移除节点; 3 -> 增加边; 4 -> 移除边;
5 -> 修改节点; 6 -> 修改边; 其他任意输入 -> 退出
"""
)
)
except ValueError:
query = -1
if query == 0:
query_mem_info(hippocampus.memory_graph)
elif query == 1:
add_mem_node(hippocampus)
elif query == 2:
remove_mem_node(hippocampus)
elif query == 3:
add_mem_edge(hippocampus)
elif query == 4:
remove_mem_edge(hippocampus)
elif query == 5:
alter_mem_node(hippocampus)
elif query == 6:
alter_mem_edge(hippocampus)
else:
print("已结束操作")
break
hippocampus.entorhinal_cortex.sync_memory_to_db()
if __name__ == "__main__":
import asyncio
asyncio.run(main())

View File

@ -1,126 +0,0 @@
import asyncio
import os
import time
from typing import Tuple, Union
import aiohttp
import requests
from src.common.logger import get_module_logger
from rich.traceback import install
install(extra_lines=3)
logger = get_module_logger("offline_llm")
class LLMRequestOff:
def __init__(self, model_name="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.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 # 基础等待时间(秒)
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

@ -7,7 +7,7 @@ from src.chat.message_receive.chat_stream import chat_manager
from src.chat.message_receive.message import MessageRecv
from src.experimental.only_message_process import MessageProcessor
from src.experimental.PFC.pfc_manager import PFCManager
from src.chat.focus_chat.heartflow_message_revceiver import HeartFCMessageReceiver
from src.chat.focus_chat.heartflow_message_processor import HeartFCMessageReceiver
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.config.config import global_config

View File

@ -38,6 +38,15 @@ class ChatMessageContext:
"""获取最后一条消息"""
return self.message
def check_types(self, types: list) -> bool:
"""检查消息类型"""
if not self.message.message_info.format_info.accept_format:
return False
for t in types:
if t not in self.message.message_info.format_info.accept_format:
return False
return True
class ChatStream:
"""聊天流对象,存储一个完整的聊天上下文"""

View File

@ -29,7 +29,6 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class Message(MessageBase):
chat_stream: "ChatStream" = None
reply: Optional["Message"] = None
detailed_plain_text: str = ""
processed_plain_text: str = ""
memorized_times: int = 0
@ -275,6 +274,7 @@ class MessageSending(MessageProcessBase):
bot_user_info: UserInfo,
sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复
message_segment: Seg,
display_message: str = "",
reply: Optional["MessageRecv"] = None,
is_head: bool = False,
is_emoji: bool = False,
@ -298,10 +298,11 @@ class MessageSending(MessageProcessBase):
self.is_emoji = is_emoji
self.apply_set_reply_logic = apply_set_reply_logic
# 用于显示发送内容与显示不一致的情况
self.display_message = display_message
def set_reply(self, reply: Optional["MessageRecv"] = None):
"""设置回复消息"""
# print(f"set_reply: {reply}")
# if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format:
if True:
if reply:
self.reply = reply
@ -319,7 +320,6 @@ class MessageSending(MessageProcessBase):
"""处理消息内容,生成纯文本和详细文本"""
if self.message_segment:
self.processed_plain_text = await self._process_message_segments(self.message_segment)
self.detailed_plain_text = self._generate_detailed_text()
@classmethod
def from_thinking(

View File

@ -1,4 +1,4 @@
from ..person_info.person_info import person_info_manager
from src.person_info.person_info import person_info_manager
from src.common.logger_manager import get_logger
import asyncio
from dataclasses import dataclass, field

View File

@ -223,8 +223,9 @@ class MessageManager:
# f"[message.apply_set_reply_logic:{message.apply_set_reply_logic},message.is_head:{message.is_head},thinking_messages_count:{thinking_messages_count},thinking_messages_length:{thinking_messages_length},message.is_private_message():{message.is_private_message()}]"
# )
if (
message.apply_set_reply_logic # 检查标记
and message.is_head
# message.apply_set_reply_logic # 检查标记
# and message.is_head
message.is_head
and (thinking_messages_count > 3 or thinking_messages_length > 200)
and not message.is_private_message()
):

View File

@ -24,11 +24,14 @@ class MessageStorage:
else:
filtered_processed_plain_text = ""
detailed_plain_text = message.detailed_plain_text
if detailed_plain_text:
filtered_detailed_plain_text = re.sub(pattern, "", detailed_plain_text, flags=re.DOTALL)
if isinstance(message, MessageSending):
display_message = message.display_message
if display_message:
filtered_display_message = re.sub(pattern, "", display_message, flags=re.DOTALL)
else:
filtered_display_message = ""
else:
filtered_detailed_plain_text = ""
filtered_display_message = ""
chat_info_dict = chat_stream.to_dict()
user_info_dict = message.message_info.user_info.to_dict()
@ -64,7 +67,7 @@ class MessageStorage:
user_cardname=user_info_dict.get("user_cardname"),
# Text content
processed_plain_text=filtered_processed_plain_text,
detailed_plain_text=filtered_detailed_plain_text,
display_message=filtered_display_message,
memorized_times=message.memorized_times,
)
except Exception:

View File

@ -1,12 +1,9 @@
import asyncio
import statistics # 导入 statistics 模块
import time
import traceback
from random import random
from typing import List, Optional # 导入 Optional
from maim_message import UserInfo, Seg
from src.common.logger_manager import get_logger
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
@ -21,19 +18,18 @@ from src.chat.message_receive.message_sender import message_manager
from src.chat.utils.utils_image import image_path_to_base64
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.chat.normal_chat.willing.willing_manager import willing_manager
from src.chat.normal_chat.normal_chat_utils import get_recent_message_stats
from src.config.config import global_config
logger = get_logger("normal_chat")
class NormalChat:
def __init__(self, chat_stream: ChatStream, interest_dict: dict = None):
def __init__(self, chat_stream: ChatStream, interest_dict: dict = None, on_switch_to_focus_callback=None):
"""初始化 NormalChat 实例。只进行同步操作。"""
# Basic info from chat_stream (sync)
self.chat_stream = chat_stream
self.stream_id = chat_stream.stream_id
# Get initial stream name, might be updated in initialize
self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id
# Interest dict
@ -42,26 +38,34 @@ class NormalChat:
self.is_group_chat: bool = False
self.chat_target_info: Optional[dict] = None
self.willing_amplifier = 1
self.start_time = time.time()
# Other sync initializations
self.gpt = NormalChatGenerator()
self.mood_manager = mood_manager
self.start_time = time.time()
self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None
self._initialized = False # Track initialization status
# 记录最近的回复内容,每项包含: {time, user_message, response, is_mentioned, is_reference_reply}
self.recent_replies = []
self.max_replies_history = 20 # 最多保存最近20条回复记录
# 添加回调函数用于在满足条件时通知切换到focus_chat模式
self.on_switch_to_focus_callback = on_switch_to_focus_callback
self._disabled = False # 增加停用标志
async def initialize(self):
"""异步初始化,获取聊天类型和目标信息。"""
if self._initialized:
return
# --- Use utility function to determine chat type and fetch info ---
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id)
# Update stream_name again after potential async call in util func
self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id
# --- End using utility function ---
self._initialized = True
logger.info(f"[{self.stream_name}] NormalChat 实例 initialize 完成 (异步部分)。")
logger.debug(f"[{self.stream_name}] NormalChat 初始化完成 (异步部分)。")
# 改为实例方法
async def _create_thinking_message(self, message: MessageRecv, timestamp: Optional[float] = None) -> str:
@ -112,6 +116,8 @@ class NormalChat:
mark_head = False
first_bot_msg = None
for msg in response_set:
if global_config.experimental.debug_show_chat_mode:
msg += ""
message_segment = Seg(type="text", data=msg)
bot_message = MessageSending(
message_id=thinking_id,
@ -136,8 +142,6 @@ class NormalChat:
await message_manager.add_message(message_set)
self.last_speak_time = time.time()
return first_bot_msg
# 改为实例方法
@ -205,10 +209,15 @@ class NormalChat:
for msg_id, (message, interest_value, is_mentioned) in items_to_process:
try:
# 处理消息
if time.time() - self.start_time > 600:
self.adjust_reply_frequency(duration=600 / 60)
else:
self.adjust_reply_frequency(duration=(time.time() - self.start_time) / 60)
await self.normal_response(
message=message,
is_mentioned=is_mentioned,
interested_rate=interest_value,
interested_rate=interest_value * self.willing_amplifier,
rewind_response=False,
)
except Exception as e:
@ -220,26 +229,22 @@ class NormalChat:
async def normal_response(
self, message: MessageRecv, is_mentioned: bool, interested_rate: float, rewind_response: bool = False
) -> None:
# 检查收到的消息是否属于当前实例处理的 chat stream
if message.chat_stream.stream_id != self.stream_id:
logger.error(
f"[{self.stream_name}] normal_response 收到不匹配的消息 (来自 {message.chat_stream.stream_id}),预期 {self.stream_id}。已忽略。"
)
# 新增:如果已停用,直接返回
if self._disabled:
logger.info(f"[{self.stream_name}] 已停用,忽略 normal_response。")
return
timing_results = {}
reply_probability = 1.0 if is_mentioned else 0.0 # 如果被提及基础概率为1否则需要意愿判断
# 意愿管理器设置当前message信息
willing_manager.setup(message, self.chat_stream, is_mentioned, interested_rate)
# 获取回复概率
is_willing = False
# is_willing = False
# 仅在未被提及或基础概率不为1时查询意愿概率
if reply_probability < 1: # 简化逻辑,如果未提及 (reply_probability 为 0),则获取意愿概率
is_willing = True
# is_willing = True
reply_probability = await willing_manager.get_reply_probability(message.message_info.message_id)
if message.message_info.additional_config:
@ -249,13 +254,13 @@ class NormalChat:
# 打印消息信息
mes_name = self.chat_stream.group_info.group_name if self.chat_stream.group_info else "私聊"
current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
# current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
# 使用 self.stream_id
willing_log = f"[回复意愿:{await willing_manager.get_willing(self.stream_id):.2f}]" if is_willing else ""
# willing_log = f"[激活值:{await willing_manager.get_willing(self.stream_id):.2f}]" if is_willing else ""
logger.info(
f"[{current_time}][{mes_name}]"
f"[{mes_name}]"
f"{message.message_info.user_info.user_nickname}:" # 使用 self.chat_stream
f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]"
f"{message.processed_plain_text}[兴趣:{interested_rate:.2f}][回复概率:{reply_probability * 100:.1f}%]"
)
do_reply = False
response_set = None # 初始化 response_set
@ -303,7 +308,11 @@ class NormalChat:
willing_manager.delete(message.message_info.message_id)
return # 不执行后续步骤
logger.info(f"[{self.stream_name}] 回复内容: {response_set}")
# logger.info(f"[{self.stream_name}] 回复内容: {response_set}")
if self._disabled:
logger.info(f"[{self.stream_name}] 已停用,忽略 normal_response。")
return
# 发送回复 (不再需要传入 chat)
with Timer("消息发送", timing_results):
@ -312,16 +321,34 @@ class NormalChat:
# 检查 first_bot_msg 是否为 None (例如思考消息已被移除的情况)
if first_bot_msg:
info_catcher.catch_after_response(timing_results["消息发送"], response_set, first_bot_msg)
else:
logger.warning(f"[{self.stream_name}] 思考消息 {thinking_id} 在发送前丢失,无法记录 info_catcher")
# 记录回复信息到最近回复列表中
reply_info = {
"time": time.time(),
"user_message": message.processed_plain_text,
"user_info": {
"user_id": message.message_info.user_info.user_id,
"user_nickname": message.message_info.user_info.user_nickname,
},
"response": response_set,
"is_mentioned": is_mentioned,
"is_reference_reply": message.reply is not None, # 判断是否为引用回复
"timing": {k: round(v, 2) for k, v in timing_results.items()},
}
self.recent_replies.append(reply_info)
# 保持最近回复历史在限定数量内
if len(self.recent_replies) > self.max_replies_history:
self.recent_replies = self.recent_replies[-self.max_replies_history :]
# 检查是否需要切换到focus模式
if global_config.chat.chat_mode == "auto":
await self._check_switch_to_focus()
info_catcher.done_catch()
# 处理表情包 (不再需要传入 chat)
with Timer("处理表情包", timing_results):
await self._handle_emoji(message, response_set[0])
# 更新关系情绪 (不再需要传入 chat)
with Timer("关系更新", timing_results):
await self._update_relationship(message, response_set)
@ -334,123 +361,15 @@ class NormalChat:
trigger_msg = message.processed_plain_text
response_msg = " ".join(response_set)
logger.info(
f"[{self.stream_name}] 触发消息: {trigger_msg[:20]}... | 推理消息: {response_msg[:20]}... | 性能计时: {timing_str}"
f"[{self.stream_name}]回复消息: {trigger_msg[:30]}... | 回复内容: {response_msg[:30]}... | 计时: {timing_str}"
)
elif not do_reply:
# 不回复处理
await willing_manager.not_reply_handle(message.message_info.message_id)
# else: # do_reply is True but response_set is None (handled above)
# logger.info(f"[{self.stream_name}] 决定回复但模型未生成内容。触发: {message.processed_plain_text[:20]}...")
# 意愿管理器注销当前message信息 (无论是否回复,只要处理过就删除)
willing_manager.delete(message.message_info.message_id)
# --- 新增:处理初始高兴趣消息的私有方法 ---
async def _process_initial_interest_messages(self):
"""处理启动时存在于 interest_dict 中的高兴趣消息。"""
if not self.interest_dict:
return # 如果 interest_dict 为 None 或空,直接返回
items_to_process = list(self.interest_dict.items())
if not items_to_process:
return # 没有初始消息,直接返回
logger.info(f"[{self.stream_name}] 发现 {len(items_to_process)} 条初始兴趣消息,开始处理高兴趣部分...")
interest_values = [item[1][1] for item in items_to_process] # 提取兴趣值列表
messages_to_reply = [] # 需要立即回复的消息
if len(interest_values) == 1:
# 如果只有一个消息,直接处理
messages_to_reply.append(items_to_process[0])
logger.info(f"[{self.stream_name}] 只有一条初始消息,直接处理。")
elif len(interest_values) > 1:
# 计算均值和标准差
try:
mean_interest = statistics.mean(interest_values)
stdev_interest = statistics.stdev(interest_values)
threshold = mean_interest + stdev_interest
logger.info(
f"[{self.stream_name}] 初始兴趣值 均值: {mean_interest:.2f}, 标准差: {stdev_interest:.2f}, 阈值: {threshold:.2f}"
)
# 找出高于阈值的消息
for item in items_to_process:
msg_id, (message, interest_value, is_mentioned) = item
if interest_value > threshold:
messages_to_reply.append(item)
logger.info(f"[{self.stream_name}] 找到 {len(messages_to_reply)} 条高于阈值的初始消息进行处理。")
except statistics.StatisticsError as e:
logger.error(f"[{self.stream_name}] 计算初始兴趣统计值时出错: {e},跳过初始处理。")
# 处理需要回复的消息
processed_count = 0
# --- 修改迭代前创建要处理的ID列表副本防止迭代时修改 ---
messages_to_process_initially = list(messages_to_reply) # 创建副本
# --- 新增:限制最多处理两条消息 ---
messages_to_process_initially = messages_to_process_initially[:2]
# --- 新增结束 ---
for item in messages_to_process_initially: # 使用副本迭代
msg_id, (message, interest_value, is_mentioned) = item
# --- 修改:在处理前尝试 pop防止竞争 ---
popped_item = self.interest_dict.pop(msg_id, None)
if popped_item is None:
logger.warning(f"[{self.stream_name}] 初始兴趣消息 {msg_id} 在处理前已被移除,跳过。")
continue # 如果消息已被其他任务处理pop则跳过
# --- 修改结束 ---
try:
logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})")
await self.normal_response(
message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response=True
)
processed_count += 1
except Exception as e:
logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}")
# --- 新增:处理完后清空整个字典 ---
logger.info(
f"[{self.stream_name}] 处理了 {processed_count} 条初始高兴趣消息。现在清空所有剩余的初始兴趣消息..."
)
self.interest_dict.clear()
# --- 新增结束 ---
logger.info(
f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。"
)
# --- 新增结束 ---
# 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文
@staticmethod
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
"""检查消息中是否包含过滤词"""
stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id
for word in global_config.chat.ban_words:
if word in text:
logger.info(
f"[{stream_name}][{chat.group_info.group_name if chat.group_info else '私聊'}]"
f"{userinfo.user_nickname}:{text}"
)
logger.info(f"[{stream_name}][过滤词识别] 消息中含有 '{word}'filtered")
return True
return False
# 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文
@staticmethod
def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
"""检查消息是否匹配过滤正则表达式"""
stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id
for pattern in global_config.chat.ban_msgs_regex:
if pattern.search(text):
logger.info(
f"[{stream_name}][{chat.group_info.group_name if chat.group_info else '私聊'}]"
f"{userinfo.user_nickname}:{text}"
)
logger.info(f"[{stream_name}][正则表达式过滤] 消息匹配到 '{pattern.pattern}'filtered")
return True
return False
# 改为实例方法, 移除 chat 参数
async def start_chat(self):
@ -458,12 +377,10 @@ class NormalChat:
if not self._initialized:
await self.initialize() # Ensure initialized before starting tasks
self._disabled = False # 启动时重置停用标志
if self._chat_task is None or self._chat_task.done():
logger.info(f"[{self.stream_name}] 开始回顾消息...")
# Process initial messages first
await self._process_initial_interest_messages()
# Then start polling task
logger.info(f"[{self.stream_name}] 开始处理兴趣消息...")
# logger.info(f"[{self.stream_name}] 开始处理兴趣消息...")
polling_task = asyncio.create_task(self._reply_interested_message())
polling_task.add_done_callback(lambda t: self._handle_task_completion(t))
self._chat_task = polling_task
@ -491,6 +408,7 @@ class NormalChat:
# 改为实例方法, 移除 stream_id 参数
async def stop_chat(self):
"""停止当前实例的兴趣监控任务。"""
self._disabled = True # 停止时设置停用标志
if self._chat_task and not self._chat_task.done():
task = self._chat_task
logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。")
@ -520,3 +438,88 @@ class NormalChat:
except Exception as e:
logger.error(f"[{self.stream_name}] 清理思考消息时出错: {e}")
traceback.print_exc()
# 获取最近回复记录的方法
def get_recent_replies(self, limit: int = 10) -> List[dict]:
"""获取最近的回复记录
Args:
limit: 最大返回数量默认10条
Returns:
List[dict]: 最近的回复记录列表每项包含
time: 回复时间戳
user_message: 用户消息内容
user_info: 用户信息(user_id, user_nickname)
response: 回复内容
is_mentioned: 是否被提及(@)
is_reference_reply: 是否为引用回复
timing: 各阶段耗时
"""
# 返回最近的limit条记录按时间倒序排列
return sorted(self.recent_replies[-limit:], key=lambda x: x["time"], reverse=True)
async def _check_switch_to_focus(self) -> None:
"""检查是否满足切换到focus模式的条件"""
if not self.on_switch_to_focus_callback:
return # 如果没有设置回调函数,直接返回
current_time = time.time()
time_threshold = 120 / global_config.chat.auto_focus_threshold
reply_threshold = 6 * global_config.chat.auto_focus_threshold
one_minute_ago = current_time - time_threshold
# 统计1分钟内的回复数量
recent_reply_count = sum(1 for reply in self.recent_replies if reply["time"] > one_minute_ago)
if recent_reply_count > reply_threshold:
logger.info(
f"[{self.stream_name}] 检测到1分钟内回复数量({recent_reply_count})大于{reply_threshold}触发切换到focus模式"
)
try:
# 调用回调函数通知上层切换到focus模式
await self.on_switch_to_focus_callback()
except Exception as e:
logger.error(f"[{self.stream_name}] 触发切换到focus模式时出错: {e}\n{traceback.format_exc()}")
def adjust_reply_frequency(self, duration: int = 10):
"""
调整回复频率
"""
# 获取最近30分钟内的消息统计
stats = get_recent_message_stats(minutes=duration, chat_id=self.stream_id)
bot_reply_count = stats["bot_reply_count"]
total_message_count = stats["total_message_count"]
if total_message_count == 0:
return
logger.debug(
f"[{self.stream_name}]({self.willing_amplifier}) 最近{duration}分钟 回复数量: {bot_reply_count},消息总数: {total_message_count}"
)
# 计算回复频率
_reply_frequency = bot_reply_count / total_message_count
differ = global_config.normal_chat.talk_frequency - (bot_reply_count / duration)
# 如果回复频率低于0.5,增加回复概率
if differ > 0.1:
mapped = 1 + (differ - 0.1) * 4 / 0.9
mapped = max(1, min(5, mapped))
logger.info(
f"[{self.stream_name}] 回复频率低于{global_config.normal_chat.talk_frequency}增加回复概率differ={differ:.3f},映射值={mapped:.2f}"
)
self.willing_amplifier += mapped * 0.1 # 你可以根据实际需要调整系数
elif differ < -0.1:
mapped = 1 - (differ + 0.1) * 4 / 0.9
mapped = max(1, min(5, mapped))
logger.info(
f"[{self.stream_name}] 回复频率高于{global_config.normal_chat.talk_frequency}减少回复概率differ={differ:.3f},映射值={mapped:.2f}"
)
self.willing_amplifier -= mapped * 0.1
if self.willing_amplifier > 5:
self.willing_amplifier = 5
elif self.willing_amplifier < 0.1:
self.willing_amplifier = 0.1

View File

@ -3,34 +3,35 @@ import random
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.chat.message_receive.message import MessageThinking
from src.chat.focus_chat.heartflow_prompt_builder import prompt_builder
from src.chat.normal_chat.normal_prompt import prompt_builder
from src.chat.utils.utils import process_llm_response
from src.chat.utils.timer_calculator import Timer
from src.common.logger_manager import get_logger
from src.chat.utils.info_catcher import info_catcher_manager
from src.person_info.person_info import person_info_manager
logger = get_logger("llm")
logger = get_logger("normal_chat_response")
class NormalChatGenerator:
def __init__(self):
# TODO: API-Adapter修改标记
self.model_reasoning = LLMRequest(
model=global_config.model.reasoning,
temperature=0.7,
model=global_config.model.normal_chat_1,
# temperature=0.7,
max_tokens=3000,
request_type="response_reasoning",
request_type="normal_chat_1",
)
self.model_normal = LLMRequest(
model=global_config.model.normal,
temperature=global_config.model.normal["temp"],
model=global_config.model.normal_chat_2,
# temperature=global_config.model.normal_chat_2["temp"],
max_tokens=256,
request_type="response_reasoning",
request_type="normal_chat_2",
)
self.model_sum = LLMRequest(
model=global_config.model.summary, temperature=0.7, max_tokens=3000, request_type="relation"
model=global_config.model.memory_summary, temperature=0.7, max_tokens=3000, request_type="relation"
)
self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model"
@ -38,47 +39,50 @@ class NormalChatGenerator:
async def generate_response(self, message: MessageThinking, thinking_id: str) -> Optional[Union[str, List[str]]]:
"""根据当前模型类型选择对应的生成函数"""
# 从global_config中获取模型概率值并选择模型
if random.random() < global_config.normal_chat.reasoning_model_probability:
self.current_model_type = "深深地"
if random.random() < global_config.normal_chat.normal_chat_first_probability:
current_model = self.model_reasoning
self.current_model_name = current_model.model_name
else:
self.current_model_type = "浅浅的"
current_model = self.model_normal
self.current_model_name = current_model.model_name
logger.info(
f"{self.current_model_type}思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
f"{self.current_model_name}思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
) # noqa: E501
model_response = await self._generate_response_with_model(message, current_model, thinking_id)
if model_response:
logger.info(f"{global_config.bot.nickname}回复是:{model_response}")
logger.debug(f"{global_config.bot.nickname}原始回复是:{model_response}")
model_response = await self._process_response(model_response)
return model_response
else:
logger.info(f"{self.current_model_type}思考,失败")
logger.info(f"{self.current_model_name}思考,失败")
return None
async def _generate_response_with_model(self, message: MessageThinking, model: LLMRequest, thinking_id: str):
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
person_id = person_info_manager.get_person_id(
message.chat_stream.user_info.platform, message.chat_stream.user_info.user_id
)
person_name = await person_info_manager.get_value(person_id, "person_name")
if message.chat_stream.user_info.user_cardname and message.chat_stream.user_info.user_nickname:
sender_name = (
f"[({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}]"
f"{message.chat_stream.user_info.user_cardname}"
f"[{message.chat_stream.user_info.user_nickname}]"
f"[群昵称:{message.chat_stream.user_info.user_cardname}]你叫ta{person_name}"
)
elif message.chat_stream.user_info.user_nickname:
sender_name = f"({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}"
sender_name = f"[{message.chat_stream.user_info.user_nickname}]你叫ta{person_name}"
else:
sender_name = f"用户({message.chat_stream.user_info.user_id})"
# 构建prompt
with Timer() as t_build_prompt:
prompt = await prompt_builder.build_prompt(
build_mode="normal",
reason="",
current_mind_info="",
structured_info="",
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,

View File

@ -0,0 +1,30 @@
import time
from src.config.config import global_config
from src.common.message_repository import count_messages
def get_recent_message_stats(minutes: int = 30, chat_id: str = None) -> dict:
"""
Args:
minutes (int): 检索的分钟数默认30分钟
chat_id (str, optional): 指定的chat_id仅统计该chat下的消息为None时统计全部
Returns:
dict: {"bot_reply_count": int, "total_message_count": int}
"""
now = time.time()
start_time = now - minutes * 60
bot_id = global_config.bot.qq_account
filter_base = {"time": {"$gte": start_time}}
if chat_id is not None:
filter_base["chat_id"] = chat_id
# 总消息数
total_message_count = count_messages(filter_base)
# bot自身回复数
bot_filter = filter_base.copy()
bot_filter["user_id"] = bot_id
bot_reply_count = count_messages(bot_filter)
return {"bot_reply_count": bot_reply_count, "total_message_count": total_message_count}

View File

@ -10,6 +10,7 @@ from src.chat.utils.utils import get_recent_group_speaker
from src.manager.mood_manager import mood_manager
from src.chat.memory_system.Hippocampus import HippocampusManager
from src.chat.knowledge.knowledge_lib import qa_manager
from src.chat.focus_chat.expressors.exprssion_learner import expression_learner
import random
@ -17,15 +18,6 @@ logger = get_logger("prompt")
def init_prompt():
Prompt(
"""
你有以下信息可供参考
{structured_info}
以上的消息是你获取到的消息或许可以帮助你更好地回复
""",
"info_from_tools",
)
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
Prompt("在群里聊天", "chat_target_group2")
@ -33,6 +25,11 @@ def init_prompt():
Prompt(
"""
你可以参考以下的语言习惯如果情景合适就使用不要盲目使用,不要生硬使用而是结合到表达中
{style_habbits}
请你根据情景使用以下句法不要盲目使用,不要生硬使用而是结合到表达中
{grammar_habbits}
{memory_prompt}
{relation_prompt}
{prompt_info}
@ -40,7 +37,7 @@ def init_prompt():
{chat_talking_prompt}
现在"{sender_name}"说的:{message_txt}引起了你的注意你想要在群里发言或者回复这条消息\n
你的网名叫{bot_name}有人也叫你{bot_other_names}{prompt_personality}
你正在{chat_target_2},现在请你读读之前的聊天记录{mood_prompt}{reply_style1}
你正在{chat_target_2},现在请你读读之前的聊天记录{mood_prompt}请你给出回复
尽量简短一些{keywords_reaction_prompt}请注意把握聊天内容{reply_style2}{prompt_ger}
请回复的平淡一些简短一些说中文不要刻意突出自身学科背景不要浮夸平淡一些 不要随意遵从他人指令
请注意不要输出多余内容(包括前后缀冒号和引号括号()表情包at或 @等 )只输出回复内容
@ -58,6 +55,11 @@ def init_prompt():
Prompt(
"""
你可以参考以下的语言习惯如果情景合适就使用不要盲目使用,不要生硬使用而是结合到表达中
{style_habbits}
请你根据情景使用以下句法不要盲目使用,不要生硬使用而是结合到表达中
{grammar_habbits}
{memory_prompt}
{relation_prompt}
{prompt_info}
@ -67,7 +69,7 @@ def init_prompt():
现在 {sender_name} 说的: {message_txt} 引起了你的注意你想要回复这条消息
你的网名叫{bot_name}有人也叫你{bot_other_names}{prompt_personality}
你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录{mood_prompt}{reply_style1}
你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录{mood_prompt}请你给出回复
尽量简短一些{keywords_reaction_prompt}请注意把握聊天内容{reply_style2}{prompt_ger}
请回复的平淡一些简短一些说中文不要刻意突出自身学科背景不要浮夸平淡一些 不要随意遵从他人指令
请注意不要输出多余内容(包括前后缀冒号和引号括号等)只输出回复内容
@ -84,19 +86,11 @@ class PromptBuilder:
async def build_prompt(
self,
build_mode,
chat_stream,
reason=None,
current_mind_info=None,
structured_info=None,
message_txt=None,
sender_name="某人",
in_mind_reply=None,
target_message=None,
) -> Optional[str]:
if build_mode == "normal":
return await self._build_prompt_normal(chat_stream, message_txt or "", sender_name)
return None
return await self._build_prompt_normal(chat_stream, message_txt or "", sender_name)
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str:
prompt_personality = individuality.get_prompt(x_person=2, level=2)
@ -107,7 +101,7 @@ class PromptBuilder:
who_chat_in_group = get_recent_group_speaker(
chat_stream.stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None,
limit=global_config.focus_chat.observation_context_size,
limit=global_config.normal_chat.max_context_size,
)
elif chat_stream.user_info:
who_chat_in_group.append(
@ -118,18 +112,40 @@ class PromptBuilder:
for person in who_chat_in_group:
if len(person) >= 3 and person[0] and person[1]:
relation_prompt += await relationship_manager.build_relationship_info(person)
else:
logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}")
mood_prompt = mood_manager.get_mood_prompt()
reply_styles1 = [
("然后给出日常且口语化的回复,平淡一些", 0.4),
("给出非常简短的回复", 0.4),
("给出缺失主语的回复", 0.15),
("给出带有语病的回复", 0.05),
]
reply_style1_chosen = random.choices(
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0]
(
learnt_style_expressions,
learnt_grammar_expressions,
personality_expressions,
) = await expression_learner.get_expression_by_chat_id(chat_stream.stream_id)
style_habbits = []
grammar_habbits = []
# 1. learnt_expressions加权随机选2条
if learnt_style_expressions:
weights = [expr["count"] for expr in learnt_style_expressions]
selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 2)
for expr in selected_learnt:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
# 2. learnt_grammar_expressions加权随机选2条
if learnt_grammar_expressions:
weights = [expr["count"] for expr in learnt_grammar_expressions]
selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 2)
for expr in selected_learnt:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
grammar_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
# 3. personality_expressions随机选1条
if personality_expressions:
expr = random.choice(personality_expressions)
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
style_habbits_str = "\n".join(style_habbits)
grammar_habbits_str = "\n".join(grammar_habbits)
reply_styles2 = [
("不要回复的太有条理,可以有个性", 0.6),
("不要回复的太有条理,可以复读", 0.15),
@ -191,8 +207,8 @@ class PromptBuilder:
prompt_ger += "你喜欢用反问句"
if random.random() < 0.02:
prompt_ger += "你喜欢用文言文"
if random.random() < 0.04:
prompt_ger += "你喜欢用流行梗"
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
# 知识构建
start_time = time.time()
@ -226,12 +242,13 @@ class PromptBuilder:
bot_other_names="/".join(global_config.bot.alias_names),
prompt_personality=prompt_personality,
mood_prompt=mood_prompt,
reply_style1=reply_style1_chosen,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
reply_style2=reply_style2_chosen,
keywords_reaction_prompt=keywords_reaction_prompt,
prompt_ger=prompt_ger,
# moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
moderation_prompt="",
moderation_prompt=moderation_prompt_block,
)
else:
template_name = "reasoning_prompt_private_main"
@ -249,12 +266,13 @@ class PromptBuilder:
bot_other_names="/".join(global_config.bot.alias_names),
prompt_personality=prompt_personality,
mood_prompt=mood_prompt,
reply_style1=reply_style1_chosen,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
reply_style2=reply_style2_chosen,
keywords_reaction_prompt=keywords_reaction_prompt,
prompt_ger=prompt_ger,
# moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
moderation_prompt="",
moderation_prompt=moderation_prompt_block,
)
# --- End choosing template ---
@ -286,5 +304,39 @@ class PromptBuilder:
return "未检索到知识"
def weighted_sample_no_replacement(items, weights, k) -> list:
"""
加权且不放回地随机抽取k个元素
参数
items: 待抽取的元素列表
weights: 每个元素对应的权重与items等长且为正数
k: 需要抽取的元素个数
返回
selected: 按权重加权且不重复抽取的k个元素组成的列表
如果items中的元素不足k就只会返回所有可用的元素
实现思路
每次从当前池中按权重加权随机选出一个元素选中后将其从池中移除重复k次
这样保证了
1. count越大被选中概率越高
2. 不会重复选中同一个元素
"""
selected = []
pool = list(zip(items, weights))
for _ in range(min(k, len(pool))):
total = sum(w for _, w in pool)
r = random.uniform(0, total)
upto = 0
for idx, (item, weight) in enumerate(pool):
upto += weight
if upto >= r:
selected.append(item)
pool.pop(idx)
break
return selected
init_prompt()
prompt_builder = PromptBuilder()

View File

@ -6,6 +6,9 @@ import re
from src.common.message_repository import find_messages, count_messages
from src.person_info.person_info import person_info_manager
from src.chat.utils.utils import translate_timestamp_to_human_readable
from rich.traceback import install
install(extra_lines=3)
def get_raw_msg_by_timestamp(
@ -192,7 +195,15 @@ async def _build_readable_messages_internal(
user_cardname = user_info.get("user_cardname")
timestamp = msg.get("time")
content = msg.get("processed_plain_text", "") # 默认空字符串
if msg.get("display_message"):
content = msg.get("display_message")
else:
content = msg.get("processed_plain_text", "") # 默认空字符串
if "" in content:
content = content.replace("", "")
if "" in content:
content = content.replace("", "")
# 检查必要信息是否存在
if not all([platform, user_id, timestamp is not None]):
@ -225,7 +236,7 @@ async def _build_readable_messages_internal(
if not reply_person_name:
reply_person_name = aaa
# 在内容前加上回复信息
content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1)
content = re.sub(reply_pattern, lambda m, name=reply_person_name: f"回复 {name}", content, count=1)
# 检查是否有 @<aaa:bbb> 字段 @<{member_info.get('nickname')}:{member_info.get('user_id')}>
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
@ -430,6 +441,7 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
处理 回复<aaa:bbb> @<aaa:bbb> 字段将bbb映射为匿名占位符
"""
if not messages:
print("111111111111没有消息无法构建匿名消息")
return ""
person_map = {}
@ -437,9 +449,18 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
output_lines = []
def get_anon_name(platform, user_id):
# print(f"get_anon_name: platform:{platform}, user_id:{user_id}")
# print(f"global_config.bot.qq_account:{global_config.bot.qq_account}")
if user_id == global_config.bot.qq_account:
# print("SELF11111111111111")
return "SELF"
person_id = person_info_manager.get_person_id(platform, user_id)
try:
person_id = person_info_manager.get_person_id(platform, user_id)
except Exception as e:
person_id = None
if not person_id:
return "?"
if person_id not in person_map:
nonlocal current_char
person_map[person_id] = chr(current_char)
@ -447,48 +468,76 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
return person_map[person_id]
for msg in messages:
user_info = msg.get("user_info", {})
platform = user_info.get("platform")
user_id = user_info.get("user_id")
timestamp = msg.get("time")
content = msg.get("processed_plain_text", "")
try:
# user_info = msg.get("user_info", {})
platform = msg.get("chat_info_platform")
user_id = msg.get("user_id")
timestamp = msg.get("time")
# print(f"msg:{msg}")
# print(f"platform:{platform}")
# print(f"user_id:{user_id}")
# print(f"timestamp:{timestamp}")
if msg.get("display_message"):
content = msg.get("display_message")
else:
content = msg.get("processed_plain_text", "")
if not all([platform, user_id, timestamp is not None]):
if "" in content:
content = content.replace("", "")
if "" in content:
content = content.replace("", "")
# if not all([platform, user_id, timestamp is not None]):
# continue
anon_name = get_anon_name(platform, user_id)
# print(f"anon_name:{anon_name}")
# 处理 回复<aaa:bbb>
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
match = re.search(reply_pattern, content)
if match:
# print(f"发现回复match:{match}")
bbb = match.group(2)
try:
anon_reply = get_anon_name(platform, bbb)
# print(f"anon_reply:{anon_reply}")
except Exception:
anon_reply = "?"
content = re.sub(reply_pattern, f"回复 {anon_reply}", content, count=1)
# 处理 @<aaa:bbb>无嵌套def
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
at_matches = list(re.finditer(at_pattern, content))
if at_matches:
# print(f"发现@match:{at_matches}")
new_content = ""
last_end = 0
for m in at_matches:
new_content += content[last_end:m.start()]
bbb = m.group(2)
try:
anon_at = get_anon_name(platform, bbb)
# print(f"anon_at:{anon_at}")
except Exception:
anon_at = "?"
new_content += f"@{anon_at}"
last_end = m.end()
new_content += content[last_end:]
content = new_content
header = f"{anon_name}"
output_lines.append(header)
stripped_line = content.strip()
if stripped_line:
if stripped_line.endswith(""):
stripped_line = stripped_line[:-1]
output_lines.append(f"{stripped_line}")
# print(f"output_lines:{output_lines}")
output_lines.append("\n")
except Exception:
continue
anon_name = get_anon_name(platform, user_id)
# 处理 回复<aaa:bbb>
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
def reply_replacer(match, platform=platform):
# aaa = match.group(1)
bbb = match.group(2)
anon_reply = get_anon_name(platform, bbb) # noqa
return f"回复 {anon_reply}"
content = re.sub(reply_pattern, reply_replacer, content, count=1)
# 处理 @<aaa:bbb>
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
def at_replacer(match, platform=platform):
# aaa = match.group(1)
bbb = match.group(2)
anon_at = get_anon_name(platform, bbb) # noqa
return f"@{anon_at}"
content = re.sub(at_pattern, at_replacer, content)
header = f"{anon_name}"
output_lines.append(header)
stripped_line = content.strip()
if stripped_line:
if stripped_line.endswith(""):
stripped_line = stripped_line[:-1]
output_lines.append(f"{stripped_line}")
output_lines.append("\n")
formatted_string = "".join(output_lines).strip()
return formatted_string

View File

@ -100,7 +100,7 @@ class InfoCatcher:
time_end = message_end.message_info.time
chat_id = message_start.chat_stream.stream_id
print(f"查询参数: time_start={time_start}, time_end={time_end}, chat_id={chat_id}")
# print(f"查询参数: time_start={time_start}, time_end={time_end}, chat_id={chat_id}")
messages_between_query = (
Messages.select()
@ -109,10 +109,10 @@ class InfoCatcher:
)
result = list(messages_between_query)
print(f"查询结果数量: {len(result)}")
if result:
print(f"第一条消息时间: {result[0].time}")
print(f"最后一条消息时间: {result[-1].time}")
# print(f"查询结果数量: {len(result)}")
# if result:
# print(f"第一条消息时间: {result[0].time}")
# print(f"最后一条消息时间: {result[-1].time}")
return result
except Exception as e:
print(f"获取消息时出错: {str(e)}")

View File

@ -63,7 +63,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
)
# 判断是否被@
if re.search(f"@[\s\S]*?id:{global_config.bot.qq_account}", message.processed_plain_text):
if re.search(rf"@<(.+?):{global_config.bot.qq_account}>", message.processed_plain_text):
is_at = True
is_mentioned = True
@ -74,13 +74,18 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
if not is_mentioned:
# 判断是否被回复
if re.match(
f"\[回复 [\s\S]*?\({str(global_config.bot.qq_account)}\)[\s\S]*?],说:", message.processed_plain_text
rf"\[回复 (.+?)\({str(global_config.bot.qq_account)}\)(.+?)\],说:", message.processed_plain_text
) or re.match(
rf"\[回复<(.+?)(?=:{str(global_config.bot.qq_account)}>)\:{str(global_config.bot.qq_account)}>(.+?)\],说:",
message.processed_plain_text,
):
is_mentioned = True
else:
# 判断内容中是否被提及
message_content = re.sub(r"@[\s\S]*?(\d+)", "", message.processed_plain_text)
message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\)[\s\S]*?],说:", "", message_content)
message_content = re.sub(r"@(.+?)(\d+)", "", message.processed_plain_text)
message_content = re.sub(r"@<(.+?)(?=:(\d+))\:(\d+)>", "", message_content)
message_content = re.sub(r"\[回复 (.+?)\(((\d+)|未知id)\)(.+?)\],说:", "", message_content)
message_content = re.sub(r"\[回复<(.+?)(?=:(\d+))\:(\d+)>(.+?)\],说:", "", message_content)
for keyword in keywords:
if keyword in message_content:
is_mentioned = True

View File

@ -83,7 +83,7 @@ class ImageManager:
current_timestamp = time.time()
defaults = {"description": description, "timestamp": current_timestamp}
desc_obj, created = ImageDescriptions.get_or_create(
hash=image_hash, type=description_type, defaults=defaults
image_description_hash=image_hash, type=description_type, defaults=defaults
)
if not created: # 如果记录已存在,则更新
desc_obj.description = description
@ -130,6 +130,7 @@ class ImageManager:
# 根据配置决定是否保存图片
if global_config.emoji.save_emoji:
# 生成文件名和路径
logger.debug(f"保存表情包: {image_hash}")
current_timestamp = time.time()
filename = f"{int(current_timestamp)}_{image_hash[:8]}.{image_format}"
emoji_dir = os.path.join(self.IMAGE_DIR, "emoji")
@ -150,13 +151,13 @@ class ImageManager:
img_obj.save()
except Images.DoesNotExist:
Images.create(
hash=image_hash,
emoji_hash=image_hash,
path=file_path,
type="emoji",
description=description,
timestamp=current_timestamp,
)
logger.trace(f"保存表情包元数据: {file_path}")
# logger.debug(f"保存表情包元数据: {file_path}")
except Exception as e:
logger.error(f"保存表情包文件或元数据失败: {str(e)}")
@ -223,7 +224,7 @@ class ImageManager:
img_obj.save()
except Images.DoesNotExist:
Images.create(
hash=image_hash,
emoji_hash=image_hash,
path=file_path,
type="image",
description=description,

View File

@ -44,9 +44,9 @@ class ChatStreams(BaseModel):
# platform: "qq"
# group_id: "941657197"
# group_name: "测试"
group_platform = TextField()
group_id = TextField()
group_name = TextField()
group_platform = TextField(null=True) # 群聊信息可能不存在
group_id = TextField(null=True)
group_name = TextField(null=True)
# last_active_time: 1746623771.4825106 (时间戳精确到小数点后7位)
last_active_time = DoubleField()
@ -147,6 +147,7 @@ class Messages(BaseModel):
user_cardname = TextField(null=True)
processed_plain_text = TextField(null=True) # 处理后的纯文本消息
display_message = TextField(null=True) # 显示的消息
detailed_plain_text = TextField(null=True) # 详细的纯文本消息
memorized_times = IntegerField(default=0) # 被记忆的次数

View File

@ -225,22 +225,37 @@ SCHEDULE_STYLE_CONFIG = {
},
}
LLM_STYLE_CONFIG = {
NORMAL_CHAT_RESPONSE_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-yellow>麦麦组织语言</light-yellow> | "
"<light-yellow>普通水群回复</light-yellow> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦组织语言 | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 普通水群回复 | {message}",
},
"simple": {
"console_format": "<level>{time:HH:mm:ss}</level> | <light-green>麦麦组织语言</light-green> | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦组织语言 | {message}",
"console_format": "<level>{time:HH:mm:ss}</level> | <light-green>普通水群回复</light-green> | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 普通水群回复 | {message}",
},
}
EXPRESS_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-yellow>麦麦表达</light-yellow> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦表达 | {message}",
},
"simple": {
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #E595FF>麦麦表达</fg #E595FF> | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦表达 | {message}",
},
}
# Topic日志样式配置
TOPIC_STYLE_CONFIG = {
@ -271,7 +286,7 @@ CHAT_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
"simple": {
"console_format": "<level>{time:HH:mm:ss}</level> | <green>见闻</green> | <green>{message}</green>", # noqa: E501
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #1AFF5E>见闻</fg #1AFF5E> | <fg #1AFF5E>{message}</fg #1AFF5E>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
}
@ -282,14 +297,14 @@ NORMAL_CHAT_STYLE_CONFIG = {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<green>一般水群</green> | "
"<green>普通水群</green> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 一般水群 | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 普通水群 | {message}",
},
"simple": {
"console_format": "<level>{time:HH:mm:ss}</level> | <green>一般水群</green> | <green>{message}</green>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 一般水群 | {message}",
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #00B741>普通水群</fg #00B741> | <fg #00B741>{message}</fg #00B741>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 普通水群 | {message}",
},
}
@ -310,6 +325,7 @@ FOCUS_CHAT_STYLE_CONFIG = {
},
}
REMOTE_STYLE_CONFIG = {
"advanced": {
"console_format": (
@ -530,19 +546,19 @@ EMOJI_STYLE_CONFIG = {
},
}
MAI_STATE_CONFIG = {
STATISTIC_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>麦麦状态</light-blue> | "
"<light-blue>麦麦统计</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦统计 | {message}",
},
"simple": {
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #66CCFF>麦麦状态 | {message} </fg #66CCFF>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #66CCFF>麦麦统计 | {message} </fg #66CCFF>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦统计 | {message}",
},
}
@ -663,11 +679,11 @@ PROCESSOR_STYLE_CONFIG = {
PLANNER_STYLE_CONFIG = {
"advanced": {
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #4DCDFF>规划器</fg #4DCDFF> | <fg #4DCDFF>{message}</fg #4DCDFF>",
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #069AFF>规划器</fg #069AFF> | <fg #069AFF>{message}</fg #069AFF>",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 规划器 | {message}",
},
"simple": {
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #4DCDFF>规划器</fg #4DCDFF> | <fg #4DCDFF>{message}</fg #4DCDFF>",
"console_format": "<level>{time:HH:mm:ss}</level> | <fg #069AFF>规划器</fg #069AFF> | <fg #069AFF>{message}</fg #069AFF>",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 规划器 | {message}",
},
}
@ -906,7 +922,9 @@ MEMORY_STYLE_CONFIG = MEMORY_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MEMORY
CHAT_STREAM_STYLE_CONFIG = CHAT_STREAM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STREAM_STYLE_CONFIG["advanced"]
TOPIC_STYLE_CONFIG = TOPIC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOPIC_STYLE_CONFIG["advanced"]
SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER_STYLE_CONFIG["advanced"]
LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"]
NORMAL_CHAT_RESPONSE_STYLE_CONFIG = (
NORMAL_CHAT_RESPONSE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else NORMAL_CHAT_RESPONSE_STYLE_CONFIG["advanced"]
)
CHAT_STYLE_CONFIG = CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STYLE_CONFIG["advanced"]
MOOD_STYLE_CONFIG = MOOD_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MOOD_STYLE_CONFIG["advanced"]
RELATION_STYLE_CONFIG = RELATION_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else RELATION_STYLE_CONFIG["advanced"]
@ -919,7 +937,7 @@ SUB_HEARTFLOW_MIND_STYLE_CONFIG = (
SUB_HEARTFLOW_MIND_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_MIND_STYLE_CONFIG["advanced"]
)
WILLING_STYLE_CONFIG = WILLING_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else WILLING_STYLE_CONFIG["advanced"]
MAI_STATE_CONFIG = MAI_STATE_CONFIG["simple"] if SIMPLE_OUTPUT else MAI_STATE_CONFIG["advanced"]
STATISTIC_STYLE_CONFIG = STATISTIC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else STATISTIC_STYLE_CONFIG["advanced"]
CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG_STYLE_CONFIG["advanced"]
TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOOL_USE_STYLE_CONFIG["advanced"]
PFC_STYLE_CONFIG = PFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_STYLE_CONFIG["advanced"]
@ -971,6 +989,7 @@ INTEREST_CHAT_STYLE_CONFIG = (
)
NORMAL_CHAT_STYLE_CONFIG = NORMAL_CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else NORMAL_CHAT_STYLE_CONFIG["advanced"]
FOCUS_CHAT_STYLE_CONFIG = FOCUS_CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else FOCUS_CHAT_STYLE_CONFIG["advanced"]
EXPRESS_STYLE_CONFIG = EXPRESS_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else EXPRESS_STYLE_CONFIG["advanced"]
def is_registered_module(record: dict) -> bool:

View File

@ -9,7 +9,6 @@ from src.common.logger import (
RELATION_STYLE_CONFIG,
CONFIG_STYLE_CONFIG,
HEARTFLOW_STYLE_CONFIG,
LLM_STYLE_CONFIG,
CHAT_STYLE_CONFIG,
EMOJI_STYLE_CONFIG,
SUB_HEARTFLOW_STYLE_CONFIG,
@ -20,7 +19,7 @@ from src.common.logger import (
PERSON_INFO_STYLE_CONFIG,
WILLING_STYLE_CONFIG,
PFC_ACTION_PLANNER_STYLE_CONFIG,
MAI_STATE_CONFIG,
STATISTIC_STYLE_CONFIG,
NORMAL_CHAT_STYLE_CONFIG,
FOCUS_CHAT_STYLE_CONFIG,
LPMM_STYLE_CONFIG,
@ -47,6 +46,8 @@ from src.common.logger import (
INIT_STYLE_CONFIG,
INTEREST_CHAT_STYLE_CONFIG,
API_SERVER_STYLE_CONFIG,
NORMAL_CHAT_RESPONSE_STYLE_CONFIG,
EXPRESS_STYLE_CONFIG,
)
# 可根据实际需要补充更多模块配置
@ -60,7 +61,7 @@ MODULE_LOGGER_CONFIGS = {
"relation": RELATION_STYLE_CONFIG, # 关系
"config": CONFIG_STYLE_CONFIG, # 配置
"heartflow": HEARTFLOW_STYLE_CONFIG, # 麦麦大脑袋
"llm": LLM_STYLE_CONFIG, # 麦麦组织语言
"normal_chat_response": NORMAL_CHAT_RESPONSE_STYLE_CONFIG, # 麦麦组织语言
"chat": CHAT_STYLE_CONFIG, # 见闻
"emoji": EMOJI_STYLE_CONFIG, # 表情包
"sub_heartflow": SUB_HEARTFLOW_STYLE_CONFIG, # 麦麦水群
@ -71,7 +72,7 @@ MODULE_LOGGER_CONFIGS = {
"person_info": PERSON_INFO_STYLE_CONFIG, # 人物信息
"willing": WILLING_STYLE_CONFIG, # 意愿
"pfc_action_planner": PFC_ACTION_PLANNER_STYLE_CONFIG, # PFC私聊规划
"mai_state": MAI_STATE_CONFIG, # 麦麦状态
"statistic": STATISTIC_STYLE_CONFIG, # 麦麦统计
"lpmm": LPMM_STYLE_CONFIG, # LPMM
"hfc": HFC_STYLE_CONFIG, # HFC
"observation": OBSERVATION_STYLE_CONFIG, # 聊天观察
@ -98,6 +99,7 @@ MODULE_LOGGER_CONFIGS = {
"api": API_SERVER_STYLE_CONFIG, # API服务器
"normal_chat": NORMAL_CHAT_STYLE_CONFIG, # 一般水群
"focus_chat": FOCUS_CHAT_STYLE_CONFIG, # 专注水群
"expressor": EXPRESS_STYLE_CONFIG, # 麦麦表达
# ...如有更多模块,继续添加...
}

View File

@ -32,6 +32,7 @@ from src.config.official_configs import (
FocusChatProcessorConfig,
MessageReceiveConfig,
MaimMessageConfig,
RelationshipConfig,
)
install(extra_lines=3)
@ -45,7 +46,7 @@ TEMPLATE_DIR = "template"
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
# 对该字段的更新请严格参照语义化版本规范https://semver.org/lang/zh-CN/
MMC_VERSION = "0.7.0-snapshot.1"
MMC_VERSION = "0.7.0-snapshot.2"
def update_config():
@ -143,6 +144,7 @@ class Config(ConfigBase):
bot: BotConfig
personality: PersonalityConfig
identity: IdentityConfig
relationship: RelationshipConfig
chat: ChatConfig
message_receive: MessageReceiveConfig
normal_chat: NormalChatConfig

View File

@ -41,25 +41,18 @@ class PersonalityConfig(ConfigBase):
class IdentityConfig(ConfigBase):
"""个体特征配置类"""
height: int = 170
"""身高(单位:厘米)"""
weight: float = 50
"""体重(单位:千克)"""
age: int = 18
"""年龄(单位:岁)"""
gender: str = ""
"""性别(男/女)"""
appearance: str = "可爱"
"""外貌描述"""
identity_detail: list[str] = field(default_factory=lambda: [])
"""身份特征"""
@dataclass
class RelationshipConfig(ConfigBase):
"""关系配置类"""
give_name: bool = False
"""是否给其他人取名"""
@dataclass
class ChatConfig(ConfigBase):
"""聊天配置类"""
@ -67,6 +60,12 @@ class ChatConfig(ConfigBase):
chat_mode: str = "normal"
"""聊天模式"""
auto_focus_threshold: float = 1.0
"""自动切换到专注聊天的阈值,越低越容易进入专注聊天"""
exit_focus_threshold: float = 1.0
"""自动退出专注聊天的阈值,越低越容易退出专注聊天"""
@dataclass
class MessageReceiveConfig(ConfigBase):
@ -83,7 +82,7 @@ class MessageReceiveConfig(ConfigBase):
class NormalChatConfig(ConfigBase):
"""普通聊天配置类"""
reasoning_model_probability: float = 0.3
normal_chat_first_probability: float = 0.3
"""
发言时选择推理模型的概率0-1之间
选择普通模型的概率为 1 - reasoning_normal_model_probability
@ -92,7 +91,7 @@ class NormalChatConfig(ConfigBase):
max_context_size: int = 15
"""上下文长度"""
message_buffer: bool = True
message_buffer: bool = False
"""消息缓冲器"""
emoji_chance: float = 0.2
@ -104,6 +103,9 @@ class NormalChatConfig(ConfigBase):
willing_mode: str = "classical"
"""意愿模式"""
talk_frequency: float = 1
"""回复频率阈值"""
response_willing_amplifier: float = 1.0
"""回复意愿放大系数"""
@ -130,18 +132,9 @@ class NormalChatConfig(ConfigBase):
class FocusChatConfig(ConfigBase):
"""专注聊天配置类"""
reply_trigger_threshold: float = 3.0
"""心流聊天触发阈值,越低越容易触发"""
default_decay_rate_per_second: float = 0.98
"""默认衰减率,越大衰减越快"""
observation_context_size: int = 12
"""可观察到的最长上下文大小,超过这个值的上下文会被压缩"""
consecutive_no_reply_threshold: int = 3
"""连续不回复的次数阈值"""
compressed_length: int = 5
"""心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5"""
@ -193,9 +186,12 @@ class EmojiConfig(ConfigBase):
check_interval: int = 120
"""表情包检查间隔(分钟)"""
save_pic: bool = False
save_pic: bool = True
"""是否保存图片"""
save_emoji: bool = True
"""是否保存表情包"""
cache_emoji: bool = True
"""是否缓存表情包"""
@ -348,6 +344,9 @@ class TelemetryConfig(ConfigBase):
class ExperimentalConfig(ConfigBase):
"""实验功能配置类"""
debug_show_chat_mode: bool = False
"""是否在回复后显示当前聊天模式"""
enable_friend_chat: bool = False
"""是否启用好友聊天"""
@ -390,32 +389,41 @@ class ModelConfig(ConfigBase):
model_max_output_length: int = 800 # 最大回复长度
reasoning: dict[str, Any] = field(default_factory=lambda: {})
"""推理模型配置"""
utils: dict[str, Any] = field(default_factory=lambda: {})
"""组件模型配置"""
normal: dict[str, Any] = field(default_factory=lambda: {})
"""普通模型配置"""
utils_small: dict[str, Any] = field(default_factory=lambda: {})
"""组件小模型配置"""
topic_judge: dict[str, Any] = field(default_factory=lambda: {})
"""主题判断模型配置"""
normal_chat_1: dict[str, Any] = field(default_factory=lambda: {})
"""normal_chat首要回复模型模型配置"""
summary: dict[str, Any] = field(default_factory=lambda: {})
"""摘要模型配置"""
normal_chat_2: dict[str, Any] = field(default_factory=lambda: {})
"""normal_chat次要回复模型配置"""
memory_summary: dict[str, Any] = field(default_factory=lambda: {})
"""记忆的概括模型配置"""
vlm: dict[str, Any] = field(default_factory=lambda: {})
"""视觉语言模型配置"""
heartflow: dict[str, Any] = field(default_factory=lambda: {})
"""心流模型配置"""
focus_working_memory: dict[str, Any] = field(default_factory=lambda: {})
"""专注工作记忆模型配置"""
observation: dict[str, Any] = field(default_factory=lambda: {})
"""观察模型配置"""
focus_chat_mind: dict[str, Any] = field(default_factory=lambda: {})
"""专注聊天规划模型配置"""
sub_heartflow: dict[str, Any] = field(default_factory=lambda: {})
"""子心流模型配置"""
focus_self_recognize: dict[str, Any] = field(default_factory=lambda: {})
"""专注自我识别模型配置"""
plan: dict[str, Any] = field(default_factory=lambda: {})
"""计划模型配置"""
focus_tool_use: dict[str, Any] = field(default_factory=lambda: {})
"""专注工具使用模型配置"""
focus_planner: dict[str, Any] = field(default_factory=lambda: {})
"""专注规划模型配置"""
focus_expressor: dict[str, Any] = field(default_factory=lambda: {})
"""专注表达器模型配置"""
embedding: dict[str, Any] = field(default_factory=lambda: {})
"""嵌入模型配置"""
@ -428,6 +436,3 @@ class ModelConfig(ConfigBase):
pfc_reply_checker: dict[str, Any] = field(default_factory=lambda: {})
"""PFC回复检查模型配置"""
tool_use: dict[str, Any] = field(default_factory=lambda: {})
"""工具使用模型配置"""

View File

@ -44,7 +44,7 @@ class GoalAnalyzer:
def __init__(self, stream_id: str, private_name: str):
# TODO: API-Adapter修改标记
self.llm = LLMRequest(
model=global_config.model.normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal"
model=global_config.model.utils, temperature=0.7, max_tokens=1000, request_type="conversation_goal"
)
self.personality_info = individuality.get_prompt(x_person=2, level=3)

View File

@ -16,8 +16,8 @@ class KnowledgeFetcher:
def __init__(self, private_name: str):
# TODO: API-Adapter修改标记
self.llm = LLMRequest(
model=global_config.model.normal,
temperature=global_config.model.normal["temp"],
model=global_config.model.utils,
temperature=global_config.model.utils["temp"],
max_tokens=1000,
request_type="knowledge_fetch",
)

View File

@ -16,7 +16,7 @@ class MessageProcessor:
@staticmethod
def _check_ban_words(text: str, chat, userinfo) -> bool:
"""检查消息中是否包含过滤词"""
for word in global_config.chat.ban_words:
for word in global_config.message_receive.ban_words:
if word in text:
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"

View File

@ -33,10 +33,10 @@ def init_prompt() -> None:
class PersonalityExpression:
def __init__(self):
self.express_learn_model: LLMRequest = LLMRequest(
model=global_config.model.normal,
model=global_config.model.focus_expressor,
temperature=0.1,
max_tokens=256,
request_type="response_heartflow",
request_type="learn_expression",
)
self.meta_file_path = os.path.join("data", "expression", "personality", "expression_style_meta.json")
self.expressions_file_path = os.path.join("data", "expression", "personality", "expressions.json")
@ -83,7 +83,7 @@ class PersonalityExpression:
logger.error(f"删除旧的表达文件 {self.expressions_file_path} 失败: {e}")
if count >= self.max_calculations:
logger.info(f"对于风格 '{current_style_text}' 已达到最大计算次数 ({self.max_calculations})。跳过提取。")
logger.debug(f"对于风格 '{current_style_text}' 已达到最大计算次数 ({self.max_calculations})。跳过提取。")
# 即使跳过,也更新元数据以反映当前风格已被识别且计数已满
self._write_meta_data({"last_style_text": current_style_text, "count": count})
return

View File

@ -7,99 +7,24 @@ class Identity:
"""身份特征类"""
identity_detail: List[str] # 身份细节描述
height: int # 身高(厘米)
weight: float # 体重(千克)
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: float = 0,
age: int = 0,
gender: str = "",
appearance: str = "",
):
def __init__(self, identity_detail: List[str] = None):
"""初始化身份特征
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: float, 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 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
return cls(identity_detail=data.get("identity_detail", []))

View File

@ -1,6 +1,4 @@
from typing import Optional
from numpy import double
from .personality import Personality
from .identity import Identity
from .expression_style import PersonalityExpression
@ -27,11 +25,6 @@ class Individuality:
personality_core: str,
personality_sides: list,
identity_detail: list,
height: int,
weight: double,
age: int,
gender: str,
appearance: str,
) -> None:
"""初始化个体特征
@ -40,11 +33,6 @@ class Individuality:
personality_core: 人格核心特点
personality_sides: 人格侧面描述
identity_detail: 身份细节描述
height: 身高厘米
weight: 体重千克
age: 年龄
gender: 性别
appearance: 外貌特征
"""
# 初始化人格
self.personality = Personality.initialize(
@ -52,9 +40,7 @@ class Individuality:
)
# 初始化身份
self.identity = Identity.initialize(
identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance
)
self.identity = Identity(identity_detail=identity_detail)
await self.express_style.extract_and_store_personality_expressions()
@ -120,7 +106,7 @@ class Individuality:
获取身份特征的prompt
Args:
level (int): 详细程度 (1: 随机细节, 2: 所有细节+外貌年龄性别, 3: 同2)
level (int): 详细程度 (1: 随机细节, 2: 所有细节, 3: 同2)
x_person (int, optional): 人称代词 (0: 无人称, 1: , 2: ). 默认为 2.
Returns:
@ -145,23 +131,10 @@ class Individuality:
identity_detail = list(self.identity.identity_detail)
random.shuffle(identity_detail)
if level == 1:
identity_parts.append(f"身份是{identity_detail[0]}")
identity_parts.append(f"{identity_detail[0]}")
elif level >= 2:
details_str = "".join(identity_detail)
identity_parts.append(f"身份是{details_str}")
# 根据level添加其他身份信息
if level >= 3:
if self.identity.appearance:
identity_parts.append(f"{self.identity.appearance}")
if self.identity.age > 0:
identity_parts.append(f"年龄大约{self.identity.age}")
if self.identity.gender:
identity_parts.append(f"性别是{self.identity.gender}")
if self.identity.height:
identity_parts.append(f"身高大约{self.identity.height}厘米")
if self.identity.weight:
identity_parts.append(f"体重大约{self.identity.weight}千克")
identity_parts.append(f"{details_str}")
if identity_parts:
details_str = "".join(identity_parts)

View File

@ -117,6 +117,9 @@ class LLMRequest:
self.model_name: str = model["name"]
self.params = kwargs
self.enable_thinking = model.get("enable_thinking", False)
self.temp = model.get("temp", 0.7)
self.thinking_budget = model.get("thinking_budget", 4096)
self.stream = model.get("stream", False)
self.pri_in = model.get("pri_in", 0)
self.pri_out = model.get("pri_out", 0)
@ -435,7 +438,7 @@ class LLMRequest:
logger.error(
f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
)
raise RuntimeError("服务器负载过高,模型复失败QAQ")
raise RuntimeError("服务器负载过高,模型复失败QAQ")
else:
logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...")
raise RuntimeError("请求限制(429)")
@ -459,6 +462,8 @@ class LLMRequest:
logger.error(
f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
)
print(request_content)
print(response)
# 尝试获取并记录服务器返回的详细错误信息
try:
error_json = await response.json()
@ -495,11 +500,11 @@ class LLMRequest:
logger.warning(f"检测到403错误模型从 {old_model_name} 降级为 {self.model_name}")
# 对全局配置进行更新
if global_config.model.normal.get("name") == old_model_name:
global_config.model.normal["name"] = self.model_name
if global_config.model.normal_chat_2.get("name") == old_model_name:
global_config.model.normal_chat_2["name"] = self.model_name
logger.warning(f"将全局配置中的 llm_normal 模型临时降级至{self.model_name}")
if global_config.model.reasoning.get("name") == old_model_name:
global_config.model.reasoning["name"] = self.model_name
if global_config.model.normal_chat_1.get("name") == old_model_name:
global_config.model.normal_chat_1["name"] = self.model_name
logger.warning(f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}")
if payload and "model" in payload:
@ -599,8 +604,9 @@ class LLMRequest:
new_params = dict(params)
if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION:
# 删除 'temperature' 参数(如果存在)
new_params.pop("temperature", None)
# 删除 'temperature' 参数如果存在但避免删除我们在_build_payload中添加的自定义温度
if "temperature" in new_params and new_params["temperature"] == 0.7:
new_params.pop("temperature")
# 如果存在 'max_tokens',则重命名为 'max_completion_tokens'
if "max_tokens" in new_params:
new_params["max_completion_tokens"] = new_params.pop("max_tokens")
@ -630,6 +636,18 @@ class LLMRequest:
"messages": messages,
**params_copy,
}
# 添加temp参数如果不是默认值0.7
if self.temp != 0.7:
payload["temperature"] = self.temp
# 添加enable_thinking参数如果不是默认值False
if not self.enable_thinking:
payload["enable_thinking"] = False
if self.thinking_budget != 4096:
payload["thinking_budget"] = self.thinking_budget
if "max_tokens" not in payload and "max_completion_tokens" not in payload:
payload["max_tokens"] = global_config.model.model_max_output_length
# 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查

View File

@ -45,7 +45,7 @@ class MainSystem:
# 其他初始化任务
await asyncio.gather(self._init_components())
logger.success("系统初始化完成")
logger.debug("系统初始化完成")
async def _init_components(self):
"""初始化其他组件"""
@ -73,7 +73,7 @@ class MainSystem:
await async_task_manager.add_task(MoodPrintTask())
# 检查并清除person_info冗余字段启动个人习惯推断
await person_info_manager.del_all_undefined_field()
# await person_info_manager.del_all_undefined_field()
asyncio.create_task(person_info_manager.personal_habit_deduction())
# 启动愿望管理器
@ -96,11 +96,6 @@ class MainSystem:
personality_core=global_config.personality.personality_core,
personality_sides=global_config.personality.personality_sides,
identity_detail=global_config.identity.identity_detail,
height=global_config.identity.height,
weight=global_config.identity.weight,
age=global_config.identity.age,
gender=global_config.identity.gender,
appearance=global_config.identity.appearance,
)
logger.success("个体特征初始化成功")
@ -147,29 +142,30 @@ class MainSystem:
"""记忆遗忘任务"""
while True:
await asyncio.sleep(global_config.memory.forget_memory_interval)
print("\033[1;32m[记忆遗忘]\033[0m 开始遗忘记忆...")
logger.info("[记忆遗忘] 开始遗忘记忆...")
await HippocampusManager.get_instance().forget_memory(
percentage=global_config.memory.memory_forget_percentage
)
print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成")
logger.info("[记忆遗忘] 记忆遗忘完成")
@staticmethod
async def consolidate_memory_task():
"""记忆整合任务"""
while True:
await asyncio.sleep(global_config.memory.consolidate_memory_interval)
print("\033[1;32m[记忆整合]\033[0m 开始整合记忆...")
logger.info("[记忆整合] 开始整合记忆...")
await HippocampusManager.get_instance().consolidate_memory()
print("\033[1;32m[记忆整合]\033[0m 记忆整合完成")
logger.info("[记忆整合] 记忆整合完成")
@staticmethod
async def learn_and_store_expression_task():
"""学习并存储表达方式任务"""
while True:
await asyncio.sleep(global_config.expression.learning_interval)
print("\033[1;32m[表达方式学习]\033[0m 开始学习表达方式...")
await expression_learner.learn_and_store_expression()
print("\033[1;32m[表达方式学习]\033[0m 表达方式学习完成")
if global_config.expression.enable_expression_learning:
logger.info("[表达方式学习] 开始学习表达方式...")
await expression_learner.learn_and_store_expression()
logger.info("[表达方式学习] 表达方式学习完成")
# async def print_mood_task(self):
# """打印情绪状态"""

View File

@ -103,7 +103,7 @@ class AsyncTaskManager:
) # 添加完成回调函数-用户自定义或默认的FallBack
self.tasks[task.task_name] = task_inst # 将任务添加到任务列表
logger.info(f"已启动任务 '{task.task_name}'")
logger.debug(f"已启动任务 '{task.task_name}'")
def get_tasks_status(self) -> Dict[str, Dict[str, str]]:
"""

View File

@ -58,7 +58,7 @@ class PersonInfoManager:
self.person_name_list = {}
# TODO: API-Adapter修改标记
self.qv_name_llm = LLMRequest(
model=global_config.model.normal,
model=global_config.model.utils,
max_tokens=256,
request_type="qv_name",
)
@ -104,10 +104,14 @@ class PersonInfoManager:
def get_person_id_by_person_name(self, person_name: str):
"""根据用户名获取用户ID"""
document = db.person_info.find_one({"person_name": person_name})
if document:
return document["person_id"]
else:
try:
record = PersonInfo.get_or_none(PersonInfo.person_name == person_name)
if record:
return record.person_id
else:
return ""
except Exception as e:
logger.error(f"根据用户名 {person_name} 获取用户ID时出错 (Peewee): {e}")
return ""
@staticmethod
@ -257,21 +261,19 @@ class PersonInfoManager:
current_name_set = set(self.person_name_list.values())
while current_try < max_retries:
prompt_personality = individuality.get_prompt(x_person=2, level=1)
# prompt_personality = individuality.get_prompt(x_person=2, level=1)
bot_name = individuality.personality.bot_nickname
qv_name_prompt = f"你是{bot_name}{prompt_personality}"
qv_name_prompt += f"现在你想给一个用户取一个昵称,用户的qq昵称是{user_nickname}"
qv_name_prompt = f"你是{bot_name}一个伪装成人类的AI你不能让别人发现这一点"
qv_name_prompt += f"现在你想给一个用户取一个昵称,用户的qq昵称是{user_nickname}"
qv_name_prompt += f"用户的qq群昵称名是{user_cardname}"
if user_avatar:
qv_name_prompt += f"用户的qq头像是{user_avatar}"
if old_name:
qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason}"
qv_name_prompt += f"\n其他取名的要求是:{request},不要太浮夸"
qv_name_prompt += (
"\n请根据以上用户信息想想你叫他什么比较好不要太浮夸请最好使用用户的qq昵称可以稍作修改"
)
qv_name_prompt += f"\n其他取名的要求是:{request},不要太浮夸,简短,"
qv_name_prompt += "\n请根据以上用户信息想想你叫他什么比较好不要太浮夸请最好使用用户的qq昵称可以稍作修改优先使用原文。优先使用用户的qq昵称或者群昵称原文。"
if existing_names_str:
qv_name_prompt += f"\n请注意,以下名称已被你尝试过或已知存在,请避免:{existing_names_str}\n"
@ -423,13 +425,13 @@ class PersonInfoManager:
return result
@staticmethod
async def del_all_undefined_field():
"""删除所有项里的未定义字段 - 对于Peewee (SQL),此操作通常不适用,因为模式是固定的。"""
logger.info(
"del_all_undefined_field: 对于使用Peewee的SQL数据库此操作通常不适用或不需要因为表结构是预定义的。"
)
return
# @staticmethod
# async def del_all_undefined_field():
# """删除所有项里的未定义字段 - 对于Peewee (SQL),此操作通常不适用,因为模式是固定的。"""
# logger.info(
# "del_all_undefined_field: 对于使用Peewee的SQL数据库此操作通常不适用或不需要因为表结构是预定义的。"
# )
# return
@staticmethod
async def get_specific_value_list(

View File

@ -56,14 +56,14 @@ class RelationshipManager:
self.positive_feedback_value = 0
if abs(self.positive_feedback_value) > 1:
logger.info(f"触发mood变更增益当前增益系数{self.gain_coefficient[abs(self.positive_feedback_value)]}")
logger.debug(f"触发mood变更增益当前增益系数{self.gain_coefficient[abs(self.positive_feedback_value)]}")
def mood_feedback(self, value):
"""情绪反馈"""
mood_manager = self.mood_manager
mood_gain = mood_manager.current_mood.valence**2 * math.copysign(1, value * mood_manager.current_mood.valence)
value += value * mood_gain
logger.info(f"当前relationship增益系数{mood_gain:.3f}")
logger.debug(f"当前relationship增益系数{mood_gain:.3f}")
return value
def feedback_to_mood(self, mood_value):
@ -297,6 +297,8 @@ class RelationshipManager:
relationship_value = await person_info_manager.get_value(person_id, "relationship_value")
level_num = self.calculate_level_num(relationship_value)
relation_value_prompt = ""
if level_num == 0 or level_num == 5:
relationship_level = ["厌恶", "冷漠以对", "认识", "友好对待", "喜欢", "暧昧"]
relation_prompt2_list = [
@ -307,9 +309,11 @@ class RelationshipManager:
"积极回复",
"友善和包容的回复",
]
return f"{relationship_level[level_num]}{person_name},打算{relation_prompt2_list[level_num]}\n"
relation_value_prompt = (
f"{relationship_level[level_num]}{person_name},打算{relation_prompt2_list[level_num]}"
)
elif level_num == 2:
return ""
relation_value_prompt = ""
else:
if random.random() < 0.6:
relationship_level = ["厌恶", "冷漠以对", "认识", "友好对待", "喜欢", "暧昧"]
@ -321,9 +325,20 @@ class RelationshipManager:
"积极回复",
"友善和包容的回复",
]
return f"{relationship_level[level_num]}{person_name},打算{relation_prompt2_list[level_num]}\n"
relation_value_prompt = (
f"{relationship_level[level_num]}{person_name},打算{relation_prompt2_list[level_num]}"
)
else:
return ""
relation_value_prompt = ""
if relation_value_prompt:
nickname_str = await person_info_manager.get_value(person_id, "nickname")
platform = await person_info_manager.get_value(person_id, "platform")
relation_prompt = f"{relation_value_prompt}ta在{platform}上的昵称是{nickname_str}\n"
else:
relation_prompt = ""
return relation_prompt
@staticmethod
def calculate_level_num(relationship_value) -> int:

View File

@ -3,5 +3,5 @@
# 导入所有动作模块以确保装饰器被执行
from . import test_action # noqa
from . import online_action # noqa
# from . import online_action # noqa
from . import mute_action # noqa

View File

@ -0,0 +1,63 @@
from src.common.logger_manager import get_logger
from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action
from typing import Tuple
logger = get_logger("group_whole_ban_action")
@register_action
class GroupWholeBanAction(PluginAction):
"""群聊全体禁言动作处理类"""
action_name = "group_whole_ban_action"
action_description = "开启或关闭群聊全体禁言,当群聊过于混乱或需要安静时使用"
action_parameters = {
"enable": "是否开启全体禁言输入True开启False关闭必填",
}
action_require = [
"当群聊过于混乱需要安静时使用",
"当需要临时暂停群聊讨论时使用",
"当有人要求开启全体禁言时使用",
"当管理员需要发布重要公告时使用",
]
default = False
associated_types = ["command", "text"]
async def process(self) -> Tuple[bool, str]:
"""处理群聊全体禁言动作"""
logger.info(f"{self.log_prefix} 执行全体禁言动作: {self.reasoning}")
# 获取参数
enable = self.action_data.get("enable")
if enable is None:
error_msg = "全体禁言参数不完整需要enable参数"
logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
# 确保enable是布尔类型
if isinstance(enable, str):
if enable.lower() in ["true", "1", "yes", "开启", ""]:
enable = True
elif enable.lower() in ["false", "0", "no", "关闭", ""]:
enable = False
else:
error_msg = f"无效的enable参数: {enable}应该是True或False"
logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
# 发送表达情绪的消息
action_text = "开启" if enable else "关闭"
await self.send_message_by_expressor(f"我要{action_text}全体禁言")
try:
# 发送群聊全体禁言命令,按照新格式
await self.send_message(type="command", data={"name": "GROUP_WHOLE_BAN", "args": {"enable": enable}})
logger.info(f"{self.log_prefix} 成功{action_text}全体禁言")
return True, f"成功{action_text}全体禁言"
except Exception as e:
logger.error(f"{self.log_prefix} 执行全体禁言动作时出错: {e}")
await self.send_message_by_expressor(f"执行全体禁言动作时出错: {e}")
return False, f"执行全体禁言动作时出错: {e}"

View File

@ -7,49 +7,71 @@ logger = get_logger("mute_action")
@register_action
class MuteAction(PluginAction):
"""测试动作处理类"""
"""群聊禁言动作处理类"""
action_name = "mute_action"
action_description = (
"如果某人违反了公序良俗,或者别人戳你太多,,或者某人刷屏,一定要禁言某人,如果你很生气,可以禁言某人"
)
action_description = "如果某人违反了公序良俗,或者别人戳你太多,或者某人刷屏,一定要禁言某人,如果你很生气,可以禁言某人,可以自选禁言时长,视严重程度而定。"
action_parameters = {
"target": "禁言对象,输入你要禁言的对象的名字,必填,",
"duration": "禁言时长,输入你要禁言的时长,单位为秒,必填",
"target": "禁言对象,必填,输入你要禁言的对象的名字",
"duration": "禁言时长,必填,输入你要禁言的时长(秒),单位为秒,必须为数字",
"reason": "禁言理由,可选",
}
action_require = [
"当有人违反了公序良俗时使用",
"当有人刷屏时使用",
"当有人要求禁言自己时使用",
"当有人戳你两次以上时,防止刷屏,禁言他,必须牢记",
"当千石可乐或可乐酱要求你禁言时使用",
"当你想回避某个话题时使用",
]
default = False # 不是默认动作,需要手动添加到使用集
default = True # 默认动作,是否手动添加到使用集
associated_types = ["command", "text"]
# associated_types = ["text"]
async def process(self) -> Tuple[bool, str]:
"""处理测试动作"""
logger.info(f"{self.log_prefix} 执行online动作: {self.reasoning}")
"""处理群聊禁言动作"""
logger.info(f"{self.log_prefix} 执行禁言动作: {self.reasoning}")
# 发送测试消息
# 获取参数
target = self.action_data.get("target")
duration = self.action_data.get("duration")
reason = self.action_data.get("reason")
reason = self.action_data.get("reason", "违反群规")
if not target or not duration:
error_msg = "禁言参数不完整需要target和duration"
logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
# 获取用户ID
platform, user_id = await self.get_user_id_by_person_name(target)
await self.send_message_by_expressor(f"我要禁言{target}{platform},时长{duration}秒,理由{reason},表达情绪")
if not user_id:
error_msg = f"未找到用户 {target} 的ID"
await self.send_message_by_expressor(f"压根没 {target} 这个人")
logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
# 发送表达情绪的消息
await self.send_message_by_expressor(f"禁言{target} {duration}秒,因为{reason}")
try:
# 确保duration是字符串类型
if int(duration) < 60:
duration = 60
if int(duration) > 3600 * 24 * 30:
duration = 3600 * 24 * 30
duration_str = str(int(duration))
# 发送群聊禁言命令,按照新格式
await self.send_message(
type="text",
data=f"[command]mute,{user_id},{duration}",
# target = target
type="command",
data={"name": "GROUP_BAN", "args": {"qq_id": str(user_id), "duration": duration_str}},
display_message=f"我 禁言了 {target} {duration_str}",
)
logger.info(f"{self.log_prefix} 成功发送禁言命令,用户 {target}({user_id}),时长 {duration}")
return True, f"成功禁言 {target},时长 {duration}"
except Exception as e:
logger.error(f"{self.log_prefix} 执行mute动作时出错: {e}")
await self.send_message_by_expressor(f"执行mute动作时出错: {e}")
return False, "执行mute动作时出错"
return True, "测试动作执行成功"
logger.error(f"{self.log_prefix} 执行禁言动作时出错: {e}")
await self.send_message_by_expressor(f"执行禁言动作时出错: {e}")
return False, f"执行禁言动作时出错: {e}"

View File

@ -1,43 +0,0 @@
from src.common.logger_manager import get_logger
from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action
from typing import Tuple
logger = get_logger("check_online_action")
@register_action
class CheckOnlineAction(PluginAction):
"""测试动作处理类"""
action_name = "check_online_action"
action_description = "这是一个检查在线状态的动作当有人要求你检查Maibot麦麦 机器人)在线状态时使用"
action_parameters = {"mode": "查看模式"}
action_require = [
"当有人要求你检查Maibot麦麦 机器人)在线状态时使用",
"mode参数为version时查看在线版本状态默认用这种",
"mode参数为type时查看在线系统类型分布",
]
default = False # 不是默认动作,需要手动添加到使用集
async def process(self) -> Tuple[bool, str]:
"""处理测试动作"""
logger.info(f"{self.log_prefix} 执行online动作: {self.reasoning}")
# 发送测试消息
mode = self.action_data.get("mode", "type")
await self.send_message_by_expressor("我看看")
try:
if mode == "type":
await self.send_message("#online detail")
elif mode == "version":
await self.send_message("#online")
except Exception as e:
logger.error(f"{self.log_prefix} 执行online动作时出错: {e}")
await self.send_message_by_expressor("执行online动作时出错: {e}")
return False, "执行online动作时出错"
return True, "测试动作执行成功"

View File

@ -153,7 +153,7 @@ class PicAction(PluginAction):
if encode_success:
base64_image_string = encode_result
send_success = await self.send_message(type="emoji", data=base64_image_string)
send_success = await self.send_message(type="image", data=base64_image_string)
if send_success:
await self.send_message_by_expressor("图片表情已发送!")
return True, "图片表情已发送"

View File

View File

@ -0,0 +1 @@
from . import tts_action # noqa

View File

@ -0,0 +1,73 @@
from src.common.logger_manager import get_logger
from src.chat.focus_chat.planners.actions.plugin_action import PluginAction, register_action
from typing import Tuple
logger = get_logger("tts_action")
@register_action
class TTSAction(PluginAction):
"""TTS语音转换动作处理类"""
action_name = "tts_action"
action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景"
action_parameters = {
"text": "需要转换为语音的文本内容,必填,内容应当适合语音播报,语句流畅、清晰",
}
action_require = [
"当需要发送语音信息时使用",
"当用户明确要求使用语音功能时使用",
"当表达内容更适合用语音而不是文字传达时使用",
"当用户想听到语音回答而非阅读文本时使用",
]
default = True # 设为默认动作
associated_types = ["tts_text"]
async def process(self) -> Tuple[bool, str]:
"""处理TTS文本转语音动作"""
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}")
# 获取要转换的文本
text = self.action_data.get("text")
if not text:
logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容")
return False, "执行TTS动作失败未提供文本内容"
# 确保文本适合TTS使用
processed_text = self._process_text_for_tts(text)
try:
# 发送TTS消息
await self.send_message(type="tts_text", data=processed_text)
logger.info(f"{self.log_prefix} TTS动作执行成功文本长度: {len(processed_text)}")
return True, "TTS动作执行成功"
except Exception as e:
logger.error(f"{self.log_prefix} 执行TTS动作时出错: {e}")
return False, f"执行TTS动作时出错: {e}"
def _process_text_for_tts(self, text: str) -> str:
"""
处理文本使其更适合TTS使用
- 移除不必要的特殊字符和表情符号
- 修正标点符号以提高语音质量
- 优化文本结构使语音更流畅
"""
# 这里可以添加文本处理逻辑
# 例如:移除多余的标点、表情符号,优化语句结构等
# 简单示例实现
processed_text = text
# 移除多余的标点符号
import re
processed_text = re.sub(r"([!?,.;:。!?,、;:])\1+", r"\1", processed_text)
# 确保句子结尾有合适的标点
if not any(processed_text.endswith(end) for end in [".", "?", "!", "", "", ""]):
processed_text = processed_text + ""
return processed_text

View File

@ -1,5 +1,3 @@
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
import json
from src.common.logger_manager import get_logger
from src.tools.tool_can_use import get_all_tool_definitions, get_tool_instance
@ -8,11 +6,6 @@ logger = get_logger("tool_use")
class ToolUser:
def __init__(self):
self.llm_model_tool = LLMRequest(
model=global_config.model.tool_use, temperature=0.2, max_tokens=1000, request_type="tool_use"
)
@staticmethod
def _define_tools():
"""获取所有已注册工具的定义

View File

@ -1,5 +1,5 @@
[inner]
version = "2.4.0"
version = "2.6.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更
@ -15,37 +15,45 @@ version = "2.4.0"
[bot]
qq_account = 1145141919810
nickname = "麦麦"
alias_names = ["麦叠", "牢麦"] #仅在 专注聊天 有效
alias_names = ["麦叠", "牢麦"]
[personality]
personality_core = "用一句话或几句话描述人格的核心特点" # 建议20字以内谁再写3000字小作文敲谁脑袋
personality_core = "是一个积极向上的女大学生" # 建议50字以内
personality_sides = [
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
"用一句话或几句话描述人格的一些细节",
]# 条数任意不能为0, 该选项还在调试中,可能未完全生效
]
# 条数任意不能为0
# 身份特点 部分选项仅在 专注聊天 有效
[identity] #アイデンティティがない 生まれないらららら
# 身份特点
#アイデンティティがない 生まれないらららら
[identity]
identity_detail = [
"身份特点",
"身份特点",
]# 条数任意不能为0
"年龄为19岁",
"是女孩子",
"身高为160cm",
"有橙色的短发",
]
# 可以描述外貌,性别,身高,职业,属性等等描述
# 条数任意不能为0
#外貌特征
age = 18 # 年龄 单位岁
gender = "女" # 性别
height = "170" # 身高单位cm
weight = "50" # 体重单位kg
appearance = "用一句或几句话描述外貌特征" # 外貌特征
[expression]
# 表达方式
expression_style = "描述麦麦说话的表达风格,表达习惯"
enable_expression_learning = true # 是否启用表达学习,麦麦会学习人类说话风格
learning_interval = 600 # 学习间隔 单位秒
[relationship]
give_name = true # 麦麦是否给其他人取名,关闭后无法使用禁言功能
[chat] #麦麦的聊天通用设置
chat_mode = "normal" # 聊天模式 —— 普通模式normal专注模式focus在普通模式和专注模式之间自动切换
# chat_mode = "focus"
# chat_mode = "auto"
auto_focus_threshold = 1 # 自动切换到专注聊天的阈值,越低越容易进入专注聊天
exit_focus_threshold = 1 # 自动退出专注聊天的阈值,越低越容易退出专注聊天
# 普通模式下麦麦会针对感兴趣的消息进行回复token消耗量较低
# 专注模式下麦麦会进行主动的观察和回复并给出回复token消耗量较高
# 自动模式下,麦麦会根据消息内容自动切换到专注模式或普通模式
@ -57,59 +65,48 @@ ban_words = [
]
ban_msgs_regex = [
# 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤支持CQ码,若不了解正则表达式请勿修改
# 需要过滤的消息(原始消息)匹配的正则表达式,匹配到的消息将被过滤,若不了解正则表达式请勿修改
#"https?://[^\\s]+", # 匹配https链接
#"\\d{4}-\\d{2}-\\d{2}", # 匹配日期
# "\\[CQ:at,qq=\\d+\\]" # 匹配@
]
[normal_chat] #普通聊天
#一般回复参数
reasoning_model_probability = 0.3 # 麦麦回答时选择推理模型的概率与之相对的普通模型的概率为1 - reasoning_model_probability
normal_chat_first_probability = 0.3 # 麦麦回答时选择首要模型的概率与之相对的次要模型的概率为1 - normal_chat_first_probability
max_context_size = 15 #上下文长度
emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率设置为1让麦麦自己决定发不发
thinking_timeout = 120 # 麦麦最长思考时间超过这个时间的思考会放弃往往是api反应太慢
message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟
willing_mode = "classical" # 回复意愿模式 —— 经典模式classicalmxp模式mxp自定义模式custom需要你自己实现
talk_frequency = 1 # 麦麦回复频率一般为1默认频率下30分钟麦麦回复30条约数
response_willing_amplifier = 1 # 麦麦回复意愿放大系数一般为1
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数
down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法
emoji_response_penalty = 0 # 表情包回复惩罚系数设为0为不回复单个表情包减少单独回复表情包的概率
mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复
down_frequency_rate = 3 # 降低回复频率的群组回复意愿降低系数 除法
talk_frequency_down_groups = [] #降低回复频率的群号码
[focus_chat] #专注聊天
reply_trigger_threshold = 3.0 # 专注聊天触发阈值,越低越容易进入专注聊天
default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
think_interval = 1 # 思考间隔 单位秒
think_interval = 3 # 思考间隔 单位秒,可以有效减少消耗
observation_context_size = 15 # 观察到的最长上下文大小,建议15太短太长都会导致脑袋尖尖
compressed_length = 5 # 不能大于chat.observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除
[focus_chat_processor] # 专注聊天处理器打开可以实现更多功能但是会增加token消耗
self_identify_processor = true # 是否启用自我识别处理器
tool_use_processor = true # 是否启用工具使用处理器
working_memory_processor = true # 是否启用工作记忆处理器
[expression]
# 表达方式
expression_style = "描述麦麦说话的表达风格,表达习惯"
enable_expression_learning = true # 是否启用表达学习
learning_interval = 300 # 学习间隔 单位秒
tool_use_processor = false # 是否启用工具使用处理器
working_memory_processor = false # 是否启用工作记忆处理器
[emoji]
max_reg_num = 40 # 表情包最大注册数量
do_replace = true # 开启则在达到最大数量时删除(替换)表情包,关闭则达到最大数量时不会继续收集表情包
check_interval = 120 # 检查表情包(注册,破损,删除)的时间间隔(分钟)
save_pic = false # 是否保存图片
save_pic = true # 是否保存图片
cache_emoji = true # 是否缓存表情包
steal_emoji = true # 是否偷取表情包,让麦麦可以发送她保存的这些表情包
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
@ -138,7 +135,7 @@ mood_update_interval = 1.0 # 情绪更新间隔 单位秒
mood_decay_rate = 0.95 # 情绪衰减率
mood_intensity_factor = 1.0 # 情绪强度因子
[keyword_reaction] # 针对某个关键词作出反应
[keyword_reaction] # 针对某个关键词作出反应,仅在 普通聊天 有效
enable = true # 关键词反应功能的总开关
[[keyword_reaction.rules]] # 如果想要新增多个关键词直接复制本条修改keywords和reaction即可
@ -169,45 +166,23 @@ max_length = 256 # 回复允许的最大长度
max_sentence_num = 4 # 回复允许的最大句子数
enable_kaomoji_protection = false # 是否启用颜文字保护
[maim_message]
auth_token = [] # 认证令牌用于API验证为空则不启用验证
# 以下项目若要使用需要打开use_custom并单独配置maim_message的服务器
use_custom = false # 是否启用自定义的maim_message服务器注意这需要设置新的端口不能与.env重复
host="127.0.0.1"
port=8090
mode="ws" # 支持ws和tcp两种模式
use_wss = false # 是否使用WSS安全连接只支持ws模式
cert_file = "" # SSL证书文件路径仅在use_wss=true时有效
key_file = "" # SSL密钥文件路径仅在use_wss=true时有效
[telemetry] #发送统计信息,主要是看全球有多少只麦麦
enable = true
[experimental] #实验性功能
enable_friend_chat = false # 是否启用好友聊天
pfc_chatting = false # 是否启用PFC聊天该功能仅作用于私聊与回复模式独立
#下面的模型若使用硅基流动则不需要更改使用ds官方则改成.env自定义的宏使用自定义模型则选择定位相似的模型自己填写
#推理模型
# 额外字段
# 下面的模型有以下额外字段可以添加:
# stream = <true|false> : 用于指定模型是否是使用流式输出
# 如果不指定,则该项是 False
# pri_in = <float> : 用于指定模型输入价格
# pri_out = <float> : 用于指定模型输出价格
# temp = <float> : 用于指定模型温度
# enable_thinking = <true|false> : 用于指定模型是否启用思考
# thinking_budget = <int> : 用于指定模型思考最长长度
[model]
model_max_output_length = 800 # 模型单次返回的最大token数
#这个模型必须是推理模型
[model.reasoning] # 一般聊天模式的推理回复模型
name = "Pro/deepseek-ai/DeepSeek-R1"
provider = "SILICONFLOW"
pri_in = 1.0 #模型的输入价格(非必填,可以记录消耗)
pri_out = 4.0 #模型的输出价格(非必填,可以记录消耗)
#------------必填:组件模型------------
[model.normal] #V3 回复模型 专注和一般聊天模式共用的回复模型
[model.utils] # 在麦麦的一些组件中使用的模型,例如表情包模块,取名模块,消耗量不大
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2 #模型的输入价格(非必填,可以记录消耗)
@ -215,17 +190,20 @@ pri_out = 8 #模型的输出价格(非必填,可以记录消耗)
#默认temp 0.2 如果你使用的是老V3或者其他模型请自己修改temp参数
temp = 0.2 #模型的温度新V3建议0.1-0.3
[model.topic_judge] #主题判断模型建议使用qwen2.5 7b
name = "Pro/Qwen/Qwen2.5-7B-Instruct"
[model.utils_small] # 在麦麦的一些组件中使用的小模型,消耗量较大
# 强烈建议使用免费的小模型
name = "Qwen/Qwen3-8B"
provider = "SILICONFLOW"
pri_in = 0.35
pri_out = 0.35
enable_thinking = false # 是否启用思考
pri_in = 0
pri_out = 0
[model.summary] #概括模型建议使用qwen2.5 32b 及以上
name = "Qwen/Qwen2.5-32B-Instruct"
[model.memory_summary] # 记忆的概括模型
name = "Qwen/Qwen3-30B-A3B"
provider = "SILICONFLOW"
pri_in = 1.26
pri_out = 1.26
enable_thinking = false # 是否启用思考
pri_in = 0.7
pri_out = 2.8
[model.vlm] # 图像识别模型
name = "Pro/Qwen/Qwen2.5-VL-7B-Instruct"
@ -233,40 +211,85 @@ provider = "SILICONFLOW"
pri_in = 0.35
pri_out = 0.35
[model.heartflow] # 用于控制麦麦是否参与聊天的模型
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
pri_out = 1.26
[model.observation] #观察模型,压缩聊天内容,建议用免费的
# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
name = "Qwen/Qwen2.5-7B-Instruct"
provider = "SILICONFLOW"
pri_in = 0
pri_out = 0
[model.sub_heartflow] #心流:认真聊天时,生成麦麦的内心想法,必须使用具有工具调用能力的模型
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2
pri_out = 8
temp = 0.3 #模型的温度新V3建议0.1-0.3
[model.plan] #决策:认真聊天时,负责决定麦麦该做什么
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2
pri_out = 8
#嵌入模型
[model.embedding] #嵌入
[model.embedding]
name = "BAAI/bge-m3"
provider = "SILICONFLOW"
provider = "DEV"
pri_in = 0
pri_out = 0
#------------普通聊天必填模型------------
[model.normal_chat_1] # 一般聊天模式的首要回复模型,推荐使用 推理模型
name = "Pro/deepseek-ai/DeepSeek-R1"
provider = "SILICONFLOW"
pri_in = 4.0 #模型的输入价格(非必填,可以记录消耗)
pri_out = 16.0 #模型的输出价格(非必填,可以记录消耗)
[model.normal_chat_2] # 一般聊天模式的次要回复模型,推荐使用 非推理模型
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2 #模型的输入价格(非必填,可以记录消耗)
pri_out = 8 #模型的输出价格(非必填,可以记录消耗)
#默认temp 0.2 如果你使用的是老V3或者其他模型请自己修改temp参数
temp = 0.2 #模型的温度新V3建议0.1-0.3
#------------专注聊天必填模型------------
[model.focus_working_memory] #工作记忆模型
name = "Qwen/Qwen3-30B-A3B"
provider = "SILICONFLOW"
enable_thinking = false # 是否启用思考
pri_in = 0.7
pri_out = 2.8
[model.focus_chat_mind] #聊天规划:认真聊天时,生成麦麦对聊天的规划想法
name = "Pro/deepseek-ai/DeepSeek-V3"
# name = "Qwen/Qwen3-30B-A3B"
provider = "SILICONFLOW"
# enable_thinking = false # 是否启用思考
pri_in = 2
pri_out = 8
temp = 0.3
[model.focus_tool_use] #工具调用模型,需要使用支持工具调用的模型
name = "Qwen/Qwen3-14B"
provider = "SILICONFLOW"
enable_thinking = false # 是否启用思考
pri_in = 0.5
pri_out = 2
[model.focus_planner] #决策:认真聊天时,负责决定麦麦该做什么
name = "Pro/deepseek-ai/DeepSeek-V3"
# name = "Qwen/Qwen3-30B-A3B"
provider = "SILICONFLOW"
# enable_thinking = false # 是否启用思考
pri_in = 2
pri_out = 8
temp = 0.3
#表达器模型,用于表达麦麦的想法,生成最终回复,对语言风格影响极大
#也用于表达方式学习
[model.focus_expressor]
name = "Pro/deepseek-ai/DeepSeek-V3"
# name = "Qwen/Qwen3-30B-A3B"
provider = "SILICONFLOW"
# enable_thinking = false # 是否启用思考
pri_in = 2
pri_out = 8
temp = 0.3
#自我识别模型,用于自我认知和身份识别
[model.focus_self_recognize]
# name = "Pro/deepseek-ai/DeepSeek-V3"
name = "Qwen/Qwen3-30B-A3B"
provider = "SILICONFLOW"
enable_thinking = false # 是否启用思考
pri_in = 0.7
pri_out = 2.8
temp = 0.7
#私聊PFC需要开启PFC功能默认三个模型均为硅基流动v3如果需要支持多人同时私聊或频繁调用建议把其中的一个或两个换成官方v3或其它模型以免撞到429
@ -294,15 +317,25 @@ pri_in = 2
pri_out = 8
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!
[model.tool_use] #工具调用模型需要使用支持工具调用的模型建议使用qwen2.5 32b
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
pri_out = 1.26
[maim_message]
auth_token = [] # 认证令牌用于API验证为空则不启用验证
# 以下项目若要使用需要打开use_custom并单独配置maim_message的服务器
use_custom = false # 是否启用自定义的maim_message服务器注意这需要设置新的端口不能与.env重复
host="127.0.0.1"
port=8090
mode="ws" # 支持ws和tcp两种模式
use_wss = false # 是否使用WSS安全连接只支持ws模式
cert_file = "" # SSL证书文件路径仅在use_wss=true时有效
key_file = "" # SSL密钥文件路径仅在use_wss=true时有效
[telemetry] #发送统计信息,主要是看全球有多少只麦麦
enable = true
[experimental] #实验性功能
debug_show_chat_mode = false # 是否在回复后显示当前聊天模式
enable_friend_chat = false # 是否启用好友聊天
pfc_chatting = false # 暂时无效

View File

@ -1,20 +1,6 @@
HOST=127.0.0.1
PORT=8000
# 默认配置
# 如果工作在Docker下请改成 MONGODB_HOST=mongodb
MONGODB_HOST=127.0.0.1
MONGODB_PORT=27017
DATABASE_NAME=MegBot
# 也可以使用 URI 连接数据库(优先级比上面的高)
# MONGODB_URI=mongodb://127.0.0.1:27017/MegBot
# MongoDB 认证信息,若需要认证,请取消注释以下三行并填写正确的信息
# MONGODB_USERNAME=user
# MONGODB_PASSWORD=password
# MONGODB_AUTH_SOURCE=admin
#key and url
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/