mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' of https://github.com/SnowindMe/MaiBot into dev
commit
81db37ae1e
|
|
@ -5,6 +5,8 @@ NapCat.Framework.Windows.Once/
|
|||
log/
|
||||
logs/
|
||||
tool_call_benchmark.py
|
||||
run_maibot_core.bat
|
||||
run_napcat_adapter.bat
|
||||
run_ad.bat
|
||||
llm_tool_benchmark_results.json
|
||||
MaiBot-Napcat-Adapter-main
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# 麦麦!MaiCore-MaiMBot (编辑中)
|
||||
<br />
|
||||
<div align="center">
|
||||
<div style="text-align: center">
|
||||
|
||||

|
||||

|
||||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<p style="text-align: center">
|
||||
<a href="https://github.com/MaiM-with-u/MaiBot/">
|
||||
<img src="depends-data/maimai.png" alt="Logo" style="width: 200px">
|
||||
</a>
|
||||
|
|
@ -21,8 +21,8 @@
|
|||
画师:略nd
|
||||
</a>
|
||||
|
||||
<h3 align="center">MaiBot(麦麦)</h3>
|
||||
<p align="center">
|
||||
<h3 style="text-align: center">MaiBot(麦麦)</h3>
|
||||
<p style="text-align: center">
|
||||
一款专注于<strong> 群组聊天 </strong>的赛博网友
|
||||
<br />
|
||||
<a href="https://docs.mai-mai.org"><strong>探索本项目的文档 »</strong></a>
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储
|
||||
- 🔄 **动态人格系统**:自适应的性格特征
|
||||
|
||||
<div align="center">
|
||||
<div style="text-align: center">
|
||||
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
|
||||
<img src="depends-data/video.png" style="max-width: 200px" alt="麦麦演示视频">
|
||||
<br>
|
||||
|
|
@ -97,9 +97,9 @@
|
|||
- [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】
|
||||
|
||||
|
||||
<div align="left">
|
||||
<h2>📚 文档 </h2>
|
||||
</div>
|
||||
|
||||
## 📚 文档
|
||||
|
||||
|
||||
### (部分内容可能过时,请注意版本对应)
|
||||
|
||||
|
|
|
|||
6
bot.py
6
bot.py
|
|
@ -13,6 +13,9 @@ from src.common.logger_manager import get_logger
|
|||
# from src.common.logger import LogConfig, CONFIRM_STYLE_CONFIG
|
||||
from src.common.crash_logger import install_crash_handler
|
||||
from src.main import MainSystem
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
logger = get_logger("main")
|
||||
|
|
@ -119,7 +122,6 @@ async def graceful_shutdown():
|
|||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"麦麦关闭失败: {e}")
|
||||
|
||||
|
|
@ -131,9 +133,7 @@ def check_eula():
|
|||
privacy_file = Path("PRIVACY.md")
|
||||
|
||||
eula_updated = True
|
||||
eula_new_hash = None
|
||||
privacy_updated = True
|
||||
privacy_new_hash = None
|
||||
|
||||
eula_confirmed = False
|
||||
privacy_confirmed = False
|
||||
|
|
|
|||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
|
@ -8,7 +8,6 @@ import sys
|
|||
import os
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
from typing import Dict, List
|
||||
|
||||
from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config
|
||||
from src.plugins.knowledge.src.embedding_store import EmbeddingManager
|
||||
|
|
@ -26,8 +25,8 @@ logger = get_module_logger("LPMM知识库-OpenIE导入")
|
|||
|
||||
|
||||
def hash_deduplicate(
|
||||
raw_paragraphs: Dict[str, str],
|
||||
triple_list_data: Dict[str, List[List[str]]],
|
||||
raw_paragraphs: dict[str, str],
|
||||
triple_list_data: dict[str, list[list[str]]],
|
||||
stored_pg_hashes: set,
|
||||
stored_paragraph_hashes: set,
|
||||
):
|
||||
|
|
@ -126,7 +125,7 @@ def main():
|
|||
)
|
||||
|
||||
# 初始化Embedding库
|
||||
embed_manager = embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]])
|
||||
embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]])
|
||||
logger.info("正在从文件加载Embedding库")
|
||||
try:
|
||||
embed_manager.load_from_file()
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def process_single_text(pg_hash, raw_data, llm_client_list):
|
|||
return doc_item, None
|
||||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
def signal_handler(_signum, _frame):
|
||||
"""处理Ctrl+C信号"""
|
||||
logger.info("\n接收到中断信号,正在优雅地关闭程序...")
|
||||
shutdown_event.set()
|
||||
|
|
|
|||
|
|
@ -28,8 +28,26 @@ matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
|
|||
matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号'-'显示为方块的问题
|
||||
|
||||
|
||||
def get_random_color():
|
||||
"""生成随机颜色用于区分线条"""
|
||||
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
|
||||
|
||||
|
||||
def format_timestamp(ts):
|
||||
"""辅助函数:格式化时间戳,处理 None 或无效值"""
|
||||
if ts is None:
|
||||
return "N/A"
|
||||
try:
|
||||
# 假设 ts 是 float 类型的时间戳
|
||||
dt_object = datetime.fromtimestamp(float(ts))
|
||||
return dt_object.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError):
|
||||
return "Invalid Time"
|
||||
|
||||
|
||||
class InterestMonitorApp:
|
||||
def __init__(self, root):
|
||||
self._main_mind_loaded = None
|
||||
self.root = root
|
||||
self.root.title(WINDOW_TITLE)
|
||||
self.root.geometry("1800x800") # 调整窗口大小以适应图表
|
||||
|
|
@ -173,10 +191,6 @@ class InterestMonitorApp:
|
|||
"""当 Combobox 选择改变时调用,更新单个流的图表"""
|
||||
self.update_single_stream_plot()
|
||||
|
||||
def get_random_color(self):
|
||||
"""生成随机颜色用于区分线条"""
|
||||
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
|
||||
|
||||
def load_main_mind_history(self):
|
||||
"""只读取包含main_mind的日志行,维护历史想法队列"""
|
||||
if not os.path.exists(LOG_FILE_PATH):
|
||||
|
|
@ -332,7 +346,7 @@ class InterestMonitorApp:
|
|||
new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # 创建概率 deque
|
||||
# 检查是否已有颜色,没有则分配
|
||||
if stream_id not in self.stream_colors:
|
||||
self.stream_colors[stream_id] = self.get_random_color()
|
||||
self.stream_colors[stream_id] = get_random_color()
|
||||
|
||||
# *** 存储此 stream_id 最新的显示名称 ***
|
||||
new_stream_display_names[stream_id] = group_name
|
||||
|
|
@ -593,17 +607,6 @@ class InterestMonitorApp:
|
|||
# --- 新增:重新绘制画布 ---
|
||||
self.canvas_single.draw()
|
||||
|
||||
def format_timestamp(self, ts):
|
||||
"""辅助函数:格式化时间戳,处理 None 或无效值"""
|
||||
if ts is None:
|
||||
return "N/A"
|
||||
try:
|
||||
# 假设 ts 是 float 类型的时间戳
|
||||
dt_object = datetime.fromtimestamp(float(ts))
|
||||
return dt_object.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError):
|
||||
return "Invalid Time"
|
||||
|
||||
def update_single_stream_details(self, stream_id):
|
||||
"""更新单个流详情区域的标签内容"""
|
||||
if stream_id:
|
||||
|
|
@ -616,8 +619,8 @@ class InterestMonitorApp:
|
|||
self.single_stream_sub_mind.set(f"想法: {sub_mind}")
|
||||
self.single_stream_chat_state.set(f"状态: {chat_state}")
|
||||
self.single_stream_threshold.set(f"阈值以上: {'是' if threshold else '否'}")
|
||||
self.single_stream_last_active.set(f"最后活跃: {self.format_timestamp(last_active_ts)}")
|
||||
self.single_stream_last_interaction.set(f"最后交互: {self.format_timestamp(last_interaction_ts)}")
|
||||
self.single_stream_last_active.set(f"最后活跃: {format_timestamp(last_active_ts)}")
|
||||
self.single_stream_last_interaction.set(f"最后交互: {format_timestamp(last_interaction_ts)}")
|
||||
else:
|
||||
# 如果没有选择流,则清空详情
|
||||
self.single_stream_sub_mind.set("想法: N/A")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
|
||||
# ==============================================
|
||||
# Environment Initialization
|
||||
# ==============================================
|
||||
|
||||
# Step 1: Locate project root directory
|
||||
SCRIPTS_DIR="scripts"
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||
|
||||
# Step 2: Verify scripts directory exists
|
||||
if [ ! -d "$PROJECT_ROOT/$SCRIPTS_DIR" ]; then
|
||||
echo "❌ Error: scripts directory not found in project root" >&2
|
||||
echo "Current path: $PROJECT_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Set up Python environment
|
||||
export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH"
|
||||
cd "$PROJECT_ROOT" || {
|
||||
echo "❌ Failed to cd to project root: $PROJECT_ROOT" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Debug info
|
||||
echo "============================"
|
||||
echo "Project Root: $PROJECT_ROOT"
|
||||
echo "Python Path: $PYTHONPATH"
|
||||
echo "Working Dir: $(pwd)"
|
||||
echo "============================"
|
||||
|
||||
# ==============================================
|
||||
# Python Script Execution
|
||||
# ==============================================
|
||||
|
||||
run_python_script() {
|
||||
local script_name=$1
|
||||
echo "🔄 Running $script_name"
|
||||
if ! python3 "$SCRIPTS_DIR/$script_name"; then
|
||||
echo "❌ $script_name failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute scripts in order
|
||||
run_python_script "raw_data_preprocessor.py"
|
||||
run_python_script "info_extraction.py"
|
||||
run_python_script "import_openie.py"
|
||||
|
||||
echo "✅ All scripts completed successfully"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, List, Optional
|
||||
from typing import List, Optional
|
||||
import strawberry
|
||||
|
||||
# from packaging.version import Version, InvalidVersion
|
||||
|
|
@ -128,22 +128,22 @@ class BotConfig:
|
|||
enable_pfc_chatting: bool # 是否启用PFC聊天
|
||||
|
||||
# 模型配置
|
||||
llm_reasoning: Dict[str, str] # LLM推理
|
||||
# llm_reasoning_minor: Dict[str, str]
|
||||
llm_normal: Dict[str, str] # LLM普通
|
||||
llm_topic_judge: Dict[str, str] # LLM话题判断
|
||||
llm_summary: Dict[str, str] # LLM话题总结
|
||||
llm_emotion_judge: Dict[str, str] # LLM情感判断
|
||||
embedding: Dict[str, str] # 嵌入
|
||||
vlm: Dict[str, str] # VLM
|
||||
moderation: Dict[str, str] # 审核
|
||||
llm_reasoning: dict[str, str] # LLM推理
|
||||
# llm_reasoning_minor: dict[str, str]
|
||||
llm_normal: dict[str, str] # LLM普通
|
||||
llm_topic_judge: dict[str, str] # LLM话题判断
|
||||
llm_summary: dict[str, str] # LLM话题总结
|
||||
llm_emotion_judge: dict[str, str] # LLM情感判断
|
||||
embedding: dict[str, str] # 嵌入
|
||||
vlm: dict[str, str] # VLM
|
||||
moderation: dict[str, str] # 审核
|
||||
|
||||
# 实验性
|
||||
llm_observation: Dict[str, str] # LLM观察
|
||||
llm_sub_heartflow: Dict[str, str] # LLM子心流
|
||||
llm_heartflow: Dict[str, str] # LLM心流
|
||||
llm_observation: dict[str, str] # LLM观察
|
||||
llm_sub_heartflow: dict[str, str] # LLM子心流
|
||||
llm_heartflow: dict[str, str] # LLM心流
|
||||
|
||||
api_urls: Dict[str, str] # API URLs
|
||||
api_urls: dict[str, str] # API URLs
|
||||
|
||||
|
||||
@strawberry.type
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import os
|
||||
from pymongo import MongoClient
|
||||
from pymongo.database import Database
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
_client = None
|
||||
_db = None
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import functools
|
|||
import inspect
|
||||
from typing import Callable, Any
|
||||
from .logger import logger, add_custom_style_handler
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
def use_log_style(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from loguru import logger
|
||||
from typing import Dict, Optional, Union, List, Tuple
|
||||
from typing import Optional, Union, List, Tuple
|
||||
import sys
|
||||
import os
|
||||
from types import ModuleType
|
||||
|
|
@ -75,8 +75,8 @@ if default_handler_id is not None:
|
|||
LoguruLogger = logger.__class__
|
||||
|
||||
# 全局注册表:记录模块与处理器ID的映射
|
||||
_handler_registry: Dict[str, List[int]] = {}
|
||||
_custom_style_handlers: Dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID
|
||||
_handler_registry: dict[str, List[int]] = {}
|
||||
_custom_style_handlers: dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID
|
||||
|
||||
# 获取日志存储根地址
|
||||
current_file_path = Path(__file__).resolve()
|
||||
|
|
@ -321,7 +321,7 @@ CHAT_STYLE_CONFIG = {
|
|||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
|
||||
},
|
||||
"simple": {
|
||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <green>见闻</green> | <green>{message}</green>"), # noqa: E501
|
||||
"console_format": "<level>{time:MM-DD HH:mm}</level> | <green>见闻</green> | <green>{message}</green>", # noqa: E501
|
||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
|
||||
},
|
||||
}
|
||||
|
|
@ -353,7 +353,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = {
|
|||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
||||
},
|
||||
"simple": {
|
||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #3399FF>麦麦水群 | {message}</fg #3399FF>"), # noqa: E501
|
||||
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #3399FF>麦麦水群 | {message}</fg #3399FF>", # noqa: E501
|
||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}",
|
||||
},
|
||||
}
|
||||
|
|
@ -369,7 +369,7 @@ SUB_HEARTFLOW_MIND_STYLE_CONFIG = {
|
|||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
||||
},
|
||||
"simple": {
|
||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>麦麦小脑袋 | {message}</fg #66CCFF>"), # noqa: E501
|
||||
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>麦麦小脑袋 | {message}</fg #66CCFF>", # noqa: E501
|
||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
||||
},
|
||||
}
|
||||
|
|
@ -385,7 +385,7 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
|
|||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
|
||||
},
|
||||
"simple": {
|
||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>麦麦水群[管理] | {message}</fg #005BA2>"), # noqa: E501
|
||||
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>麦麦水群[管理] | {message}</fg #005BA2>", # noqa: E501
|
||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
|
||||
},
|
||||
}
|
||||
|
|
@ -633,7 +633,7 @@ HFC_STYLE_CONFIG = {
|
|||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
|
||||
},
|
||||
"simple": {
|
||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>"),
|
||||
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>",
|
||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
|
||||
},
|
||||
}
|
||||
|
|
@ -1031,7 +1031,7 @@ def add_custom_style_handler(
|
|||
# retention=current_config["retention"],
|
||||
# compression=current_config["compression"],
|
||||
# encoding="utf-8",
|
||||
# filter=lambda record: record["extra"].get("module") == module_name
|
||||
# message_filter=lambda record: record["extra"].get("module") == module_name
|
||||
# and record["extra"].get("custom_style") == style_name,
|
||||
# enqueue=True,
|
||||
# )
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
from src.common.database import db
|
||||
from src.common.logger import get_module_logger
|
||||
import traceback
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Any, Optional
|
||||
|
||||
logger = get_module_logger(__name__)
|
||||
|
||||
|
||||
def find_messages(
|
||||
filter: Dict[str, Any], sort: Optional[List[tuple[str, int]]] = None, limit: int = 0, limit_mode: str = "latest"
|
||||
) -> List[Dict[str, Any]]:
|
||||
message_filter: dict[str, Any],
|
||||
sort: Optional[List[tuple[str, int]]] = None,
|
||||
limit: int = 0,
|
||||
limit_mode: str = "latest",
|
||||
) -> List[dict[str, Any]]:
|
||||
"""
|
||||
根据提供的过滤器、排序和限制条件查找消息。
|
||||
|
||||
Args:
|
||||
filter: MongoDB 查询过滤器。
|
||||
message_filter: MongoDB 查询过滤器。
|
||||
sort: MongoDB 排序条件列表,例如 [('time', 1)]。仅在 limit 为 0 时生效。
|
||||
limit: 返回的最大文档数,0表示不限制。
|
||||
limit_mode: 当 limit > 0 时生效。 'earliest' 表示获取最早的记录, 'latest' 表示获取最新的记录(结果仍按时间正序排列)。默认为 'latest'。
|
||||
|
|
@ -22,8 +25,7 @@ def find_messages(
|
|||
消息文档列表,如果出错则返回空列表。
|
||||
"""
|
||||
try:
|
||||
query = db.messages.find(filter)
|
||||
results: List[Dict[str, Any]] = []
|
||||
query = db.messages.find(message_filter)
|
||||
|
||||
if limit > 0:
|
||||
if limit_mode == "earliest":
|
||||
|
|
@ -46,28 +48,28 @@ def find_messages(
|
|||
return results
|
||||
except Exception as e:
|
||||
log_message = (
|
||||
f"查找消息失败 (filter={filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n"
|
||||
f"查找消息失败 (filter={message_filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n"
|
||||
+ traceback.format_exc()
|
||||
)
|
||||
logger.error(log_message)
|
||||
return []
|
||||
|
||||
|
||||
def count_messages(filter: Dict[str, Any]) -> int:
|
||||
def count_messages(message_filter: dict[str, Any]) -> int:
|
||||
"""
|
||||
根据提供的过滤器计算消息数量。
|
||||
|
||||
Args:
|
||||
filter: MongoDB 查询过滤器。
|
||||
message_filter: MongoDB 查询过滤器。
|
||||
|
||||
Returns:
|
||||
符合条件的消息数量,如果出错则返回 0。
|
||||
"""
|
||||
try:
|
||||
count = db.messages.count_documents(filter)
|
||||
count = db.messages.count_documents(message_filter)
|
||||
return count
|
||||
except Exception as e:
|
||||
log_message = f"计数消息失败 (filter={filter}): {e}\n" + traceback.format_exc()
|
||||
log_message = f"计数消息失败 (message_filter={message_filter}): {e}\n" + traceback.format_exc()
|
||||
logger.error(log_message)
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ from fastapi import FastAPI, APIRouter
|
|||
from typing import Optional
|
||||
from uvicorn import Config, Server as UvicornServer
|
||||
import os
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
class Server:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ from packaging.version import Version, InvalidVersion
|
|||
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
# 配置主程序日志格式
|
||||
|
|
@ -22,7 +25,7 @@ logger = get_logger("config")
|
|||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||
is_test = False
|
||||
mai_version_main = "0.6.3"
|
||||
mai_version_fix = "fix-1"
|
||||
mai_version_fix = "fix-2"
|
||||
|
||||
if mai_version_fix:
|
||||
if is_test:
|
||||
|
|
@ -268,11 +271,12 @@ class BotConfig:
|
|||
# experimental
|
||||
enable_friend_chat: bool = False # 是否启用好友聊天
|
||||
# enable_think_flow: bool = False # 是否启用思考流程
|
||||
talk_allowed_private = set()
|
||||
enable_pfc_chatting: bool = False # 是否启用PFC聊天
|
||||
|
||||
# 模型配置
|
||||
llm_reasoning: Dict[str, str] = field(default_factory=lambda: {})
|
||||
# llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {})
|
||||
llm_reasoning: dict[str, str] = field(default_factory=lambda: {})
|
||||
# llm_reasoning_minor: dict[str, str] = field(default_factory=lambda: {})
|
||||
llm_normal: Dict[str, str] = field(default_factory=lambda: {})
|
||||
llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {})
|
||||
llm_summary: Dict[str, str] = field(default_factory=lambda: {})
|
||||
|
|
@ -651,6 +655,7 @@ class BotConfig:
|
|||
experimental_config = parent["experimental"]
|
||||
config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat)
|
||||
# config.enable_think_flow = experimental_config.get("enable_think_flow", config.enable_think_flow)
|
||||
config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", []))
|
||||
if config.INNER_VERSION in SpecifierSet(">=1.1.0"):
|
||||
config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from src.config.config import global_config
|
|||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.moods.moods import MoodManager
|
||||
|
||||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
|
||||
logger = get_logger("change_mood_tool")
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ class ChangeMoodTool(BaseTool):
|
|||
"required": ["text", "response_set"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||
"""执行心情改变
|
||||
|
||||
Args:
|
||||
|
|
@ -30,7 +30,7 @@ class ChangeMoodTool(BaseTool):
|
|||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
dict: 工具执行结果
|
||||
"""
|
||||
try:
|
||||
response_set = function_args.get("response_set")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ class RelationshipTool(BaseTool):
|
|||
"required": ["text", "changed_value", "reason"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> dict:
|
||||
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict:
|
||||
"""执行工具功能
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||
from src.plugins.schedule.schedule_generator import bot_schedule
|
||||
from src.common.logger import get_module_logger
|
||||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
|
||||
logger = get_module_logger("get_current_task_tool")
|
||||
|
|
@ -21,7 +21,7 @@ class GetCurrentTaskTool(BaseTool):
|
|||
"required": ["start_time", "end_time"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||
"""执行获取当前任务或指定时间段的日程信息
|
||||
|
||||
Args:
|
||||
|
|
@ -29,7 +29,7 @@ class GetCurrentTaskTool(BaseTool):
|
|||
message_txt: 原始消息文本,此工具不使用
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
dict: 工具执行结果
|
||||
"""
|
||||
start_time = function_args.get("start_time")
|
||||
end_time = function_args.get("end_time")
|
||||
|
|
@ -55,5 +55,6 @@ class GetCurrentTaskTool(BaseTool):
|
|||
task_info = "\n".join(task_list)
|
||||
else:
|
||||
task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息"
|
||||
|
||||
else:
|
||||
task_info = "请提供有效的开始时间和结束时间"
|
||||
return {"name": "get_current_task", "content": f"日程信息: {task_info}"}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||
from src.common.logger import get_module_logger
|
||||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
|
||||
logger = get_module_logger("get_mid_memory_tool")
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ class GetMidMemoryTool(BaseTool):
|
|||
"required": ["id"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||
"""执行记忆获取
|
||||
|
||||
Args:
|
||||
|
|
@ -26,7 +26,7 @@ class GetMidMemoryTool(BaseTool):
|
|||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
dict: 工具执行结果
|
||||
"""
|
||||
try:
|
||||
id = function_args.get("id")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||
from src.common.logger import get_module_logger
|
||||
|
||||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
|
||||
logger = get_module_logger("send_emoji_tool")
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ class SendEmojiTool(BaseTool):
|
|||
"required": ["text"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||
text = function_args.get("text", message_txt)
|
||||
return {
|
||||
"name": "send_emoji",
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class MyNewTool(BaseTool):
|
|||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 包含执行结果的字典,必须包含name和content字段
|
||||
dict: 包含执行结果的字典,必须包含name和content字段
|
||||
"""
|
||||
# 实现工具逻辑
|
||||
result = f"工具执行结果: {function_args.get('param1')}"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
from typing import Dict, List, Any, Optional, Type
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Any, Optional, Type
|
||||
import inspect
|
||||
import importlib
|
||||
import pkgutil
|
||||
import os
|
||||
from src.common.logger_manager import get_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_logger("base_tool")
|
||||
|
||||
|
|
@ -23,11 +25,11 @@ class BaseTool(ABC):
|
|||
parameters = None
|
||||
|
||||
@classmethod
|
||||
def get_tool_definition(cls) -> Dict[str, Any]:
|
||||
def get_tool_definition(cls) -> dict[str, Any]:
|
||||
"""获取工具定义,用于LLM工具调用
|
||||
|
||||
Returns:
|
||||
Dict: 工具定义字典
|
||||
dict: 工具定义字典
|
||||
"""
|
||||
if not cls.name or not cls.description or not cls.parameters:
|
||||
raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性")
|
||||
|
|
@ -37,14 +39,14 @@ class BaseTool(ABC):
|
|||
"function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters},
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行工具函数
|
||||
|
||||
Args:
|
||||
function_args: 工具调用参数
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
dict: 工具执行结果
|
||||
"""
|
||||
raise NotImplementedError("子类必须实现execute方法")
|
||||
|
||||
|
|
@ -89,11 +91,11 @@ def discover_tools():
|
|||
logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具")
|
||||
|
||||
|
||||
def get_all_tool_definitions() -> List[Dict[str, Any]]:
|
||||
def get_all_tool_definitions() -> List[dict[str, Any]]:
|
||||
"""获取所有已注册工具的定义
|
||||
|
||||
Returns:
|
||||
List[Dict]: 工具定义列表
|
||||
List[dict]: 工具定义列表
|
||||
"""
|
||||
return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from src.do_tool.tool_can_use.base_tool import BaseTool, run_lua_code
|
||||
from src.common.logger import get_module_logger
|
||||
from typing import Dict, Any
|
||||
from typing import Any
|
||||
|
||||
logger = get_module_logger("compare_numbers_tool")
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ class CompareNumbersTool(BaseTool):
|
|||
"""比较两个数大小的工具"""
|
||||
|
||||
name = "compare_numbers"
|
||||
description = "比较两个数的大小,返回较大的数"
|
||||
description = "使用工具 比较两个数的大小,返回较大的数"
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -19,15 +19,14 @@ class CompareNumbersTool(BaseTool):
|
|||
"required": ["num1", "num2"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行比较两个数的大小
|
||||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
dict: 工具执行结果
|
||||
"""
|
||||
try:
|
||||
num1 = function_args.get("num1")
|
||||
|
|
@ -42,10 +41,10 @@ class CompareNumbersTool(BaseTool):
|
|||
CompareNumbers = run_lua_code(lua_code).CompareNumbers
|
||||
result = CompareNumbers(num1, num2)
|
||||
|
||||
return {"name": self.name, "content": result}
|
||||
return {"type": "comparison_result", "id": f"{num1}_vs_{num2}", "content": result}
|
||||
except Exception as e:
|
||||
logger.error(f"比较数字失败: {str(e)}")
|
||||
return {"name": self.name, "content": f"比较数字失败: {str(e)}"}
|
||||
return {"type": "info", "id": f"{num1}_vs_{num2}", "content": f"比较数字失败,炸了: {str(e)}"}
|
||||
|
||||
|
||||
# 注册工具
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool
|
|||
from src.plugins.chat.utils import get_embedding
|
||||
from src.common.database import db
|
||||
from src.common.logger_manager import get_logger
|
||||
from typing import Dict, Any, Union
|
||||
from typing import Any, Union
|
||||
|
||||
logger = get_logger("get_knowledge_tool")
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ class SearchKnowledgeTool(BaseTool):
|
|||
"""从知识库中搜索相关信息的工具"""
|
||||
|
||||
name = "search_knowledge"
|
||||
description = "从知识库中搜索相关信息"
|
||||
description = "使用工具从知识库中搜索相关信息"
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -21,15 +21,14 @@ class SearchKnowledgeTool(BaseTool):
|
|||
"required": ["query"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行知识库搜索
|
||||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
dict: 工具执行结果
|
||||
"""
|
||||
try:
|
||||
query = function_args.get("query")
|
||||
|
|
@ -43,11 +42,11 @@ class SearchKnowledgeTool(BaseTool):
|
|||
content = f"你知道这些知识: {knowledge_info}"
|
||||
else:
|
||||
content = f"你不太了解有关{query}的知识"
|
||||
return {"name": "search_knowledge", "content": content}
|
||||
return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"}
|
||||
return {"type": "knowledge", "id": query, "content": content}
|
||||
return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你知识库炸了"}
|
||||
except Exception as e:
|
||||
logger.error(f"知识库搜索工具执行失败: {str(e)}")
|
||||
return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"}
|
||||
return {"type": "info", "id": query, "content": f"知识库搜索失败,炸了: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
def get_info_from_db(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class GetMemoryTool(BaseTool):
|
|||
"""从记忆系统中获取相关记忆的工具"""
|
||||
|
||||
name = "get_memory"
|
||||
description = "从记忆系统中获取相关记忆"
|
||||
description = "使用工具从记忆系统中获取相关记忆"
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -25,7 +25,6 @@ class GetMemoryTool(BaseTool):
|
|||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
|
|
@ -54,10 +53,11 @@ class GetMemoryTool(BaseTool):
|
|||
else:
|
||||
content = f"{topic}的记忆,你记不太清"
|
||||
|
||||
return {"name": "get_memory", "content": content}
|
||||
return {"type": "memory", "id": topic_list, "content": content}
|
||||
except Exception as e:
|
||||
logger.error(f"记忆获取工具执行失败: {str(e)}")
|
||||
return {"name": "get_memory", "content": f"记忆获取失败: {str(e)}"}
|
||||
# 在失败时也保持格式一致,但id可能不适用或设为None/Error
|
||||
return {"type": "memory_error", "id": topic_list, "content": f"记忆获取失败: {str(e)}"}
|
||||
|
||||
|
||||
# 注册工具
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||
from src.do_tool.tool_can_use.base_tool import BaseTool,run_lua_code
|
||||
from src.common.logger_manager import get_logger
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
logger = get_logger("get_time_date")
|
||||
|
||||
|
|
@ -21,7 +23,6 @@ class GetCurrentDateTimeTool(BaseTool):
|
|||
|
||||
Args:
|
||||
function_args: 工具参数(此工具不使用)
|
||||
message_txt: 原始消息文本(此工具不使用)
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
|
|||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
message_txt: 原始消息文本
|
||||
|
||||
Returns:
|
||||
Dict: 工具执行结果
|
||||
|
|
@ -47,11 +46,14 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
|
|||
content = f"你知道这些知识: {knowledge_info}"
|
||||
else:
|
||||
content = f"你不太了解有关{query}的知识"
|
||||
return {"name": "search_knowledge", "content": content}
|
||||
return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"}
|
||||
return {"type": "lpmm_knowledge", "id": query, "content": content}
|
||||
# 如果获取嵌入失败
|
||||
return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你lpmm知识库炸了"}
|
||||
except Exception as e:
|
||||
logger.error(f"知识库搜索工具执行失败: {str(e)}")
|
||||
return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"}
|
||||
# 在其他异常情况下,确保 id 仍然是 query (如果它被定义了)
|
||||
query_id = query if "query" in locals() else "unknown_query"
|
||||
return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {str(e)}"}
|
||||
|
||||
# def get_info_from_db(
|
||||
# self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False
|
||||
|
|
@ -134,6 +136,27 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
|
|||
# # 返回所有找到的内容,用换行分隔
|
||||
# return "\n".join(str(result["content"]) for result in results)
|
||||
|
||||
def _format_results(self, results: list) -> str:
|
||||
"""格式化结果"""
|
||||
if not results:
|
||||
return "未找到相关知识。"
|
||||
|
||||
formatted_string = "我找到了一些相关知识:\n"
|
||||
for i, result in enumerate(results):
|
||||
# chunk_id = result.get("chunk_id")
|
||||
text = result.get("text", "")
|
||||
source = result.get("source", "未知来源")
|
||||
source_type = result.get("source_type", "未知类型")
|
||||
similarity = result.get("similarity", 0.0)
|
||||
|
||||
formatted_string += (
|
||||
f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source} \n内容片段: {text}\n\n"
|
||||
)
|
||||
# 暂时去掉chunk_id
|
||||
# formatted_string += f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source}, Chunk ID: {chunk_id} \n内容片段: {text}\n\n"
|
||||
|
||||
return formatted_string
|
||||
|
||||
|
||||
# 注册工具
|
||||
# register_tool(SearchKnowledgeTool)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool
|
||||
from src.plugins.person_info.person_info import person_info_manager
|
||||
from src.common.logger_manager import get_logger
|
||||
import time
|
||||
|
||||
logger = get_logger("rename_person_tool")
|
||||
|
||||
|
||||
class RenamePersonTool(BaseTool):
|
||||
name = "rename_person"
|
||||
description = "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。"
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"},
|
||||
"message_content": {
|
||||
"type": "string",
|
||||
"description": "可选的。当前的聊天内容或特定要求,用于提供取名建议的上下文。",
|
||||
},
|
||||
},
|
||||
"required": ["person_name"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: dict, message_txt=""):
|
||||
"""
|
||||
执行取名工具逻辑
|
||||
|
||||
Args:
|
||||
function_args (dict): 包含 'person_name' 和可选 'message_content' 的字典
|
||||
message_txt (str): 原始消息文本 (这里未使用,因为 message_content 更明确)
|
||||
|
||||
Returns:
|
||||
dict: 包含执行结果的字典
|
||||
"""
|
||||
person_name_to_find = function_args.get("person_name")
|
||||
request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串
|
||||
|
||||
if not person_name_to_find:
|
||||
return {"name": self.name, "content": "错误:必须提供需要重命名的用户昵称 (person_name)。"}
|
||||
|
||||
try:
|
||||
# 1. 根据昵称查找用户信息
|
||||
logger.debug(f"尝试根据昵称 '{person_name_to_find}' 查找用户...")
|
||||
person_info = await person_info_manager.get_person_info_by_name(person_name_to_find)
|
||||
|
||||
if not person_info:
|
||||
logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。")
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。",
|
||||
}
|
||||
|
||||
person_id = person_info.get("person_id")
|
||||
user_nickname = person_info.get("nickname") # 这是用户原始昵称
|
||||
user_cardname = person_info.get("user_cardname")
|
||||
user_avatar = person_info.get("user_avatar")
|
||||
|
||||
if not person_id:
|
||||
logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id")
|
||||
return {"name": self.name, "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。"}
|
||||
|
||||
# 2. 调用 qv_person_name 进行取名
|
||||
logger.debug(
|
||||
f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'"
|
||||
)
|
||||
result = await person_info_manager.qv_person_name(
|
||||
person_id=person_id,
|
||||
user_nickname=user_nickname,
|
||||
user_cardname=user_cardname,
|
||||
user_avatar=user_avatar,
|
||||
request=request_context,
|
||||
)
|
||||
|
||||
# 3. 处理结果
|
||||
if result and result.get("nickname"):
|
||||
new_name = result["nickname"]
|
||||
# reason = result.get("reason", "未提供理由")
|
||||
logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}")
|
||||
|
||||
content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}"
|
||||
logger.info(content)
|
||||
return {"type": "info", "id": f"rename_success_{time.time()}", "content": content}
|
||||
else:
|
||||
logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。")
|
||||
# 尝试从内存中获取可能已经更新的名字
|
||||
current_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
if current_name and current_name != person_name_to_find:
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"重命名失败: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg}
|
||||
|
||||
|
||||
# 注册工具
|
||||
register_tool(RenamePersonTool)
|
||||
|
|
@ -106,7 +106,6 @@ class ToolUser:
|
|||
|
||||
Args:
|
||||
message_txt: 用户消息文本
|
||||
sender_name: 发送者名称
|
||||
chat_stream: 聊天流对象
|
||||
observation: 观察对象(可选)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,42 @@ INTEREST_EVAL_INTERVAL_SECONDS = 5
|
|||
# 新增聊天超时检查间隔
|
||||
NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60
|
||||
# 新增状态评估间隔
|
||||
HF_JUDGE_STATE_UPDATE_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(
|
||||
task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs
|
||||
):
|
||||
"""周期性任务主循环"""
|
||||
while True:
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
# logger.debug(f"开始执行后台任务: {task_name}")
|
||||
|
||||
try:
|
||||
await task_func(**kwargs) # 执行实际任务
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"任务 {task_name} 已取消")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"任务 {task_name} 执行出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 计算并执行间隔等待
|
||||
elapsed = asyncio.get_event_loop().time() - start_time
|
||||
sleep_time = max(0, interval - elapsed)
|
||||
# if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点
|
||||
# logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)")
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
logger.debug(f"任务循环结束: {task_name}") # 调整日志信息
|
||||
|
||||
|
||||
class BackgroundTaskManager:
|
||||
"""管理 Heartflow 的后台周期性任务。"""
|
||||
|
||||
|
|
@ -44,9 +73,10 @@ class BackgroundTaskManager:
|
|||
self._state_update_task: Optional[asyncio.Task] = None
|
||||
self._cleanup_task: Optional[asyncio.Task] = None
|
||||
self._logging_task: Optional[asyncio.Task] = None
|
||||
self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None # Nyaa~ 添加聊天超时检查任务的引用
|
||||
self._hf_judge_state_update_task: Optional[asyncio.Task] = None # Nyaa~ 添加状态评估任务的引用
|
||||
self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务的引用
|
||||
self._normal_chat_timeout_check_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
|
||||
|
||||
async def start_tasks(self):
|
||||
|
|
@ -97,6 +127,14 @@ class BackgroundTaskManager:
|
|||
f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s",
|
||||
"_into_focus_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",
|
||||
),
|
||||
]
|
||||
|
||||
# 统一启动所有任务
|
||||
|
|
@ -143,32 +181,6 @@ class BackgroundTaskManager:
|
|||
# 第三步:清空任务列表
|
||||
self._tasks = [] # 重置任务列表
|
||||
|
||||
async def _run_periodic_loop(
|
||||
self, task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs
|
||||
):
|
||||
"""周期性任务主循环"""
|
||||
while True:
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
# logger.debug(f"开始执行后台任务: {task_name}")
|
||||
|
||||
try:
|
||||
await task_func(**kwargs) # 执行实际任务
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"任务 {task_name} 已取消")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"任务 {task_name} 执行出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 计算并执行间隔等待
|
||||
elapsed = asyncio.get_event_loop().time() - start_time
|
||||
sleep_time = max(0, interval - elapsed)
|
||||
# if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点
|
||||
# logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)")
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
logger.debug(f"任务循环结束: {task_name}") # 调整日志信息
|
||||
|
||||
async def _perform_state_update_work(self):
|
||||
"""执行状态更新工作"""
|
||||
previous_status = self.mai_state_info.get_current_state()
|
||||
|
|
@ -249,34 +261,38 @@ class BackgroundTaskManager:
|
|||
|
||||
# --- Specific Task Runners --- #
|
||||
async def _run_state_update_cycle(self, interval: int):
|
||||
await self._run_periodic_loop(
|
||||
task_name="State Update", interval=interval, task_func=self._perform_state_update_work
|
||||
)
|
||||
await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work)
|
||||
|
||||
async def _run_absent_into_chat(self, interval: int):
|
||||
await self._run_periodic_loop(
|
||||
task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat
|
||||
)
|
||||
await _run_periodic_loop(task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat)
|
||||
|
||||
async def _run_normal_chat_timeout_check_cycle(self, interval: int):
|
||||
await self._run_periodic_loop(
|
||||
await _run_periodic_loop(
|
||||
task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work
|
||||
)
|
||||
|
||||
async def _run_cleanup_cycle(self):
|
||||
await self._run_periodic_loop(
|
||||
await _run_periodic_loop(
|
||||
task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
|
||||
)
|
||||
|
||||
async def _run_logging_cycle(self):
|
||||
await self._run_periodic_loop(
|
||||
await _run_periodic_loop(
|
||||
task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work
|
||||
)
|
||||
|
||||
# --- 新增兴趣评估任务运行器 ---
|
||||
async def _run_into_focus_cycle(self):
|
||||
await self._run_periodic_loop(
|
||||
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(
|
||||
task_name="Private Chat Activation Check",
|
||||
interval=interval,
|
||||
task_func=self.subheartflow_manager.sbhf_absent_private_into_focus,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ LOG_DIRECTORY = "logs/interest"
|
|||
HISTORY_LOG_FILENAME = "interest_history.log"
|
||||
|
||||
|
||||
def _ensure_log_directory():
|
||||
"""确保日志目录存在。"""
|
||||
os.makedirs(LOG_DIRECTORY, exist_ok=True)
|
||||
logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在")
|
||||
|
||||
|
||||
class InterestLogger:
|
||||
"""负责定期记录主心流和所有子心流的状态到日志文件。"""
|
||||
|
||||
|
|
@ -37,12 +43,7 @@ class InterestLogger:
|
|||
self.subheartflow_manager = subheartflow_manager
|
||||
self.heartflow = heartflow # 存储 Heartflow 实例
|
||||
self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME)
|
||||
self._ensure_log_directory()
|
||||
|
||||
def _ensure_log_directory(self):
|
||||
"""确保日志目录存在。"""
|
||||
os.makedirs(LOG_DIRECTORY, exist_ok=True)
|
||||
logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在")
|
||||
_ensure_log_directory()
|
||||
|
||||
async def get_all_subflow_states(self) -> Dict[str, Dict]:
|
||||
"""并发获取所有活跃子心流的当前完整状态。"""
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class MaiState(enum.Enum):
|
|||
return MAX_NORMAL_CHAT_NUM_NORMAL
|
||||
elif self == MaiState.FOCUSED_CHAT:
|
||||
return MAX_NORMAL_CHAT_NUM_FOCUSED
|
||||
return None
|
||||
|
||||
def get_focused_chat_max_num(self):
|
||||
# 调试用
|
||||
|
|
@ -76,6 +77,7 @@ class MaiState(enum.Enum):
|
|||
return MAX_FOCUSED_CHAT_NUM_NORMAL
|
||||
elif self == MaiState.FOCUSED_CHAT:
|
||||
return MAX_FOCUSED_CHAT_NUM_FOCUSED
|
||||
return None
|
||||
|
||||
|
||||
class MaiStateInfo:
|
||||
|
|
@ -135,7 +137,8 @@ class MaiStateManager:
|
|||
def __init__(self):
|
||||
pass
|
||||
|
||||
def check_and_decide_next_state(self, current_state_info: MaiStateInfo) -> Optional[MaiState]:
|
||||
@staticmethod
|
||||
def check_and_decide_next_state(current_state_info: MaiStateInfo) -> Optional[MaiState]:
|
||||
"""
|
||||
根据当前状态和规则检查是否需要转换状态,并决定下一个状态。
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,32 @@ from src.plugins.utils.chat_message_builder import (
|
|||
num_new_messages_since,
|
||||
get_person_id_list,
|
||||
)
|
||||
from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from typing import Optional
|
||||
import difflib
|
||||
from src.plugins.chat.message import MessageRecv # 添加 MessageRecv 导入
|
||||
|
||||
# Import the new utility function
|
||||
from .utils_chat import get_chat_type_and_target_info
|
||||
|
||||
logger = get_logger("observation")
|
||||
|
||||
# --- Define Prompt Templates for Chat Summary ---
|
||||
Prompt(
|
||||
"""这是qq群聊的聊天记录,请总结以下聊天记录的主题:
|
||||
{chat_logs}
|
||||
请用一句话概括,包括人物、事件和主要信息,不要分点。""",
|
||||
"chat_summary_group_prompt", # Template for group chat
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""这是你和{chat_target}的私聊记录,请总结以下聊天记录的主题:
|
||||
{chat_logs}
|
||||
请用一句话概括,包括事件,时间,和主要信息,不要分点。""",
|
||||
"chat_summary_private_prompt", # Template for private chat
|
||||
)
|
||||
# --- End Prompt Template Definition ---
|
||||
|
||||
|
||||
# 所有观察的基类
|
||||
class Observation:
|
||||
|
|
@ -34,28 +57,37 @@ class ChattingObservation(Observation):
|
|||
super().__init__("chat", chat_id)
|
||||
self.chat_id = chat_id
|
||||
|
||||
# --- Initialize attributes (defaults) ---
|
||||
self.is_group_chat: bool = False
|
||||
self.chat_target_info: Optional[dict] = None
|
||||
# --- End Initialization ---
|
||||
|
||||
# --- Other attributes initialized in __init__ ---
|
||||
self.talking_message = []
|
||||
self.talking_message_str = ""
|
||||
self.talking_message_str_truncate = ""
|
||||
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.nick_name = global_config.BOT_ALIAS_NAMES
|
||||
|
||||
self.max_now_obs_len = global_config.observation_context_size
|
||||
self.overlap_len = global_config.compressed_length
|
||||
self.mid_memorys = []
|
||||
self.max_mid_memory_len = global_config.compress_length_limit
|
||||
self.mid_memory_info = ""
|
||||
|
||||
self.person_list = []
|
||||
|
||||
self.llm_summary = LLMRequest(
|
||||
model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation"
|
||||
)
|
||||
|
||||
async def initialize(self):
|
||||
# --- 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.chat_id)
|
||||
# logger.debug(f"is_group_chat: {self.is_group_chat}")
|
||||
# logger.debug(f"chat_target_info: {self.chat_target_info}")
|
||||
# --- End using utility function ---
|
||||
|
||||
# Fetch initial messages (existing logic)
|
||||
initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10)
|
||||
self.talking_message = initial_messages # 将这些消息设为初始上下文
|
||||
self.talking_message = initial_messages
|
||||
self.talking_message_str = await build_readable_messages(self.talking_message)
|
||||
|
||||
# 进行一次观察 返回观察结果observe_info
|
||||
|
|
@ -109,18 +141,51 @@ class ChattingObservation(Observation):
|
|||
messages=oldest_messages, timestamp_mode="normal", read_mark=0
|
||||
)
|
||||
|
||||
# 调用 LLM 总结主题
|
||||
prompt = (
|
||||
f"请总结以下聊天记录的主题:\n{oldest_messages_str}\n用一句话概括包括人物事件和主要信息,不要分点:"
|
||||
)
|
||||
summary = "没有主题的闲聊" # 默认值
|
||||
# --- Build prompt using template ---
|
||||
prompt = None # Initialize prompt as None
|
||||
try:
|
||||
summary_result, _ = await self.llm_summary.generate_response_async(prompt)
|
||||
if summary_result: # 确保结果不为空
|
||||
summary = summary_result
|
||||
# 构建 Prompt - 根据 is_group_chat 选择模板
|
||||
if self.is_group_chat:
|
||||
prompt_template_name = "chat_summary_group_prompt"
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
prompt_template_name, chat_logs=oldest_messages_str
|
||||
)
|
||||
else:
|
||||
# For private chat, add chat_target to the prompt variables
|
||||
prompt_template_name = "chat_summary_private_prompt"
|
||||
# Determine the target name for the prompt
|
||||
chat_target_name = "对方" # Default fallback
|
||||
if self.chat_target_info:
|
||||
# Prioritize person_name, then nickname
|
||||
chat_target_name = (
|
||||
self.chat_target_info.get("person_name")
|
||||
or self.chat_target_info.get("user_nickname")
|
||||
or chat_target_name
|
||||
)
|
||||
|
||||
# Format the private chat prompt
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
prompt_template_name,
|
||||
# Assuming the private prompt template uses {chat_target}
|
||||
chat_target=chat_target_name,
|
||||
chat_logs=oldest_messages_str,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"总结主题失败 for chat {self.chat_id}: {e}")
|
||||
# 保留默认总结 "没有主题的闲聊"
|
||||
logger.error(f"构建总结 Prompt 失败 for chat {self.chat_id}: {e}")
|
||||
# prompt remains None
|
||||
|
||||
summary = "没有主题的闲聊" # 默认值
|
||||
|
||||
if prompt: # Check if prompt was built successfully
|
||||
try:
|
||||
summary_result, _, _ = await self.llm_summary.generate_response(prompt)
|
||||
if summary_result: # 确保结果不为空
|
||||
summary = summary_result
|
||||
except Exception as e:
|
||||
logger.error(f"总结主题失败 for chat {self.chat_id}: {e}")
|
||||
# 保留默认总结 "没有主题的闲聊"
|
||||
else:
|
||||
logger.warning(f"因 Prompt 构建失败,跳过 LLM 总结 for chat {self.chat_id}")
|
||||
|
||||
mid_memory = {
|
||||
"id": str(int(datetime.now().timestamp())),
|
||||
|
|
@ -164,6 +229,70 @@ class ChattingObservation(Observation):
|
|||
f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}"
|
||||
)
|
||||
|
||||
async def find_best_matching_message(self, search_str: str, min_similarity: float = 0.6) -> Optional[MessageRecv]:
|
||||
"""
|
||||
在 talking_message 中查找与 search_str 最匹配的消息。
|
||||
|
||||
Args:
|
||||
search_str: 要搜索的字符串。
|
||||
min_similarity: 要求的最低相似度(0到1之间)。
|
||||
|
||||
Returns:
|
||||
匹配的 MessageRecv 实例,如果找不到则返回 None。
|
||||
"""
|
||||
best_match_score = -1.0
|
||||
best_match_dict = None
|
||||
|
||||
if not self.talking_message:
|
||||
logger.debug(f"Chat {self.chat_id}: talking_message is empty, cannot find match for '{search_str}'")
|
||||
return None
|
||||
|
||||
for message_dict in self.talking_message:
|
||||
try:
|
||||
# 临时创建 MessageRecv 以处理文本
|
||||
temp_msg = MessageRecv(message_dict)
|
||||
await temp_msg.process() # 处理消息以获取 processed_plain_text
|
||||
current_text = temp_msg.processed_plain_text
|
||||
|
||||
if not current_text: # 跳过没有文本内容的消息
|
||||
continue
|
||||
|
||||
# 计算相似度
|
||||
matcher = difflib.SequenceMatcher(None, search_str, current_text)
|
||||
score = matcher.ratio()
|
||||
|
||||
# logger.debug(f"Comparing '{search_str}' with '{current_text}', score: {score}") # 可选:用于调试
|
||||
|
||||
if score > best_match_score:
|
||||
best_match_score = score
|
||||
best_match_dict = message_dict
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message for matching in chat {self.chat_id}: {e}", exc_info=True)
|
||||
continue # 继续处理下一条消息
|
||||
|
||||
if best_match_dict is not None and best_match_score >= min_similarity:
|
||||
logger.debug(f"Found best match for '{search_str}' with score {best_match_score:.2f}")
|
||||
try:
|
||||
final_msg = MessageRecv(best_match_dict)
|
||||
await final_msg.process()
|
||||
# 确保 MessageRecv 实例有关联的 chat_stream
|
||||
if hasattr(self, "chat_stream"):
|
||||
final_msg.update_chat_stream(self.chat_stream)
|
||||
else:
|
||||
logger.warning(
|
||||
f"ChattingObservation instance for chat {self.chat_id} does not have a chat_stream attribute set."
|
||||
)
|
||||
return final_msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating final MessageRecv for chat {self.chat_id}: {e}", exc_info=True)
|
||||
return None
|
||||
else:
|
||||
logger.debug(
|
||||
f"No suitable match found for '{search_str}' in chat {self.chat_id} (best score: {best_match_score:.2f}, threshold: {min_similarity})"
|
||||
)
|
||||
return None
|
||||
|
||||
async def has_new_messages_since(self, timestamp: float) -> bool:
|
||||
"""检查指定时间戳之后是否有新消息"""
|
||||
count = num_new_messages_since(chat_id=self.chat_id, timestamp_start=timestamp)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from src.plugins.heartFC_chat.normal_chat import NormalChat
|
|||
from src.heart_flow.mai_state_manager import MaiStateInfo
|
||||
from src.heart_flow.chat_state_info import ChatState, ChatStateInfo
|
||||
from src.heart_flow.sub_mind import SubMind
|
||||
from .utils_chat import get_chat_type_and_target_info
|
||||
|
||||
|
||||
# 定义常量 (从 interest.py 移动过来)
|
||||
|
|
@ -238,6 +239,11 @@ class SubHeartflow:
|
|||
self.chat_state_last_time: float = 0
|
||||
self.history_chat_state: List[Tuple[ChatState, float]] = []
|
||||
|
||||
# --- Initialize attributes ---
|
||||
self.is_group_chat: bool = False
|
||||
self.chat_target_info: Optional[dict] = None
|
||||
# --- End Initialization ---
|
||||
|
||||
# 兴趣检测器
|
||||
self.interest_chatting: InterestChatting = InterestChatting()
|
||||
|
||||
|
|
@ -260,11 +266,24 @@ class SubHeartflow:
|
|||
subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations
|
||||
)
|
||||
|
||||
# 日志前缀
|
||||
self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id
|
||||
# 日志前缀 - Moved determination to initialize
|
||||
self.log_prefix = str(subheartflow_id) # Initial default prefix
|
||||
|
||||
async def initialize(self):
|
||||
"""异步初始化方法,创建兴趣流"""
|
||||
"""异步初始化方法,创建兴趣流并确定聊天类型"""
|
||||
|
||||
# --- 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.chat_id)
|
||||
# Update log prefix after getting info (potential stream name)
|
||||
self.log_prefix = (
|
||||
chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id
|
||||
) # Keep this line or adjust if utils provides name
|
||||
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 实例已初始化。")
|
||||
|
||||
|
|
@ -286,26 +305,33 @@ class SubHeartflow:
|
|||
|
||||
async def _start_normal_chat(self) -> bool:
|
||||
"""
|
||||
启动 NormalChat 实例,
|
||||
进入 CHAT 状态时使用
|
||||
|
||||
确保 HeartFChatting 已停止
|
||||
启动 NormalChat 实例,并进行异步初始化。
|
||||
进入 CHAT 状态时使用。
|
||||
确保 HeartFChatting 已停止。
|
||||
"""
|
||||
await self._stop_heart_fc_chat() # 确保 专注聊天已停止
|
||||
|
||||
log_prefix = self.log_prefix
|
||||
try:
|
||||
# 获取聊天流并创建 NormalChat 实例
|
||||
# 获取聊天流并创建 NormalChat 实例 (同步部分)
|
||||
chat_stream = chat_manager.get_stream(self.chat_id)
|
||||
if not chat_stream:
|
||||
logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。")
|
||||
return False
|
||||
|
||||
self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict())
|
||||
|
||||
# 进行异步初始化
|
||||
await self.normal_chat_instance.initialize()
|
||||
|
||||
# 启动聊天任务
|
||||
logger.info(f"{log_prefix} 开始普通聊天,随便水群...")
|
||||
await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat
|
||||
await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 启动 NormalChat 时出错: {e}")
|
||||
logger.error(f"{log_prefix} 启动 NormalChat 或其初始化时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
self.normal_chat_instance = None # 启动失败,清理实例
|
||||
self.normal_chat_instance = None # 启动/初始化失败,清理实例
|
||||
return False
|
||||
|
||||
async def _stop_heart_fc_chat(self):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .observation import Observation
|
||||
from .observation import ChattingObservation
|
||||
from src.plugins.models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
import time
|
||||
|
|
@ -20,34 +20,63 @@ logger = get_logger("sub_heartflow")
|
|||
|
||||
|
||||
def init_prompt():
|
||||
prompt = ""
|
||||
prompt += "{extra_info}\n"
|
||||
prompt += "{relation_prompt}\n"
|
||||
prompt += "你的名字是{bot_name},{prompt_personality}\n"
|
||||
prompt += "{last_loop_prompt}\n"
|
||||
prompt += "{cycle_info_block}\n"
|
||||
prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n"
|
||||
prompt += "\n你现在{mood_info}\n"
|
||||
prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。"
|
||||
prompt += "思考并输出你的内心想法\n"
|
||||
prompt += "输出要求:\n"
|
||||
prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n"
|
||||
prompt += "2. 不要分点、不要使用表情符号\n"
|
||||
prompt += "3. 避免多余符号(冒号、引号、括号等)\n"
|
||||
prompt += "4. 语言简洁自然,不要浮夸\n"
|
||||
prompt += "5. 如果你刚发言,并且没有人回复你,不要回复\n"
|
||||
prompt += "工具使用说明:\n"
|
||||
prompt += "1. 输出想法后考虑是否需要使用工具\n"
|
||||
prompt += "2. 工具可获取信息或执行操作\n"
|
||||
prompt += "3. 如需处理消息或回复,请使用工具\n"
|
||||
# --- Group Chat Prompt ---
|
||||
group_prompt = """
|
||||
{extra_info}
|
||||
{relation_prompt}
|
||||
你的名字是{bot_name},{prompt_personality}
|
||||
{last_loop_prompt}
|
||||
{cycle_info_block}
|
||||
现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
|
||||
{chat_observe_info}
|
||||
|
||||
Prompt(prompt, "sub_heartflow_prompt_before")
|
||||
你现在{mood_info}
|
||||
请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。
|
||||
思考并输出你的内心想法
|
||||
输出要求:
|
||||
1. 根据聊天内容生成你的想法,{hf_do_next}
|
||||
2. 不要分点、不要使用表情符号
|
||||
3. 避免多余符号(冒号、引号、括号等)
|
||||
4. 语言简洁自然,不要浮夸
|
||||
5. 如果你刚发言,并且没有人回复你,不要回复
|
||||
工具使用说明:
|
||||
1. 输出想法后考虑是否需要使用工具
|
||||
2. 工具可获取信息或执行操作
|
||||
3. 如需处理消息或回复,请使用工具。"""
|
||||
Prompt(group_prompt, "sub_heartflow_prompt_before")
|
||||
|
||||
prompt = ""
|
||||
prompt += "刚刚你的内心想法是:{current_thinking_info}\n"
|
||||
prompt += "{if_replan_prompt}\n"
|
||||
# --- Private Chat Prompt ---
|
||||
private_prompt = """
|
||||
{extra_info}
|
||||
{relation_prompt}
|
||||
你的名字是{bot_name},{prompt_personality}
|
||||
{last_loop_prompt}
|
||||
{cycle_info_block}
|
||||
现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容:
|
||||
{chat_observe_info}
|
||||
|
||||
Prompt(prompt, "last_loop")
|
||||
你现在{mood_info}
|
||||
请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。
|
||||
请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具。
|
||||
思考并输出你的内心想法
|
||||
输出要求:
|
||||
1. 根据聊天内容生成你的想法,{hf_do_next}
|
||||
2. 不要分点、不要使用表情符号
|
||||
3. 避免多余符号(冒号、引号、括号等)
|
||||
4. 语言简洁自然,不要浮夸
|
||||
5. 如果你刚发言,对方没有回复你,请谨慎回复
|
||||
工具使用说明:
|
||||
1. 输出想法后考虑是否需要使用工具
|
||||
2. 工具可获取信息或执行操作
|
||||
3. 如需处理消息或回复,请使用工具。"""
|
||||
Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name
|
||||
|
||||
# --- Last Loop Prompt (remains the same) ---
|
||||
last_loop_t = """
|
||||
刚刚你的内心想法是:{current_thinking_info}
|
||||
{if_replan_prompt}
|
||||
"""
|
||||
Prompt(last_loop_t, "last_loop")
|
||||
|
||||
|
||||
def calculate_similarity(text_a: str, text_b: str) -> float:
|
||||
|
|
@ -78,14 +107,15 @@ def calculate_replacement_probability(similarity: float) -> float:
|
|||
# p = 3.5 * s - 1.4
|
||||
probability = 3.5 * similarity - 1.4
|
||||
return max(0.0, probability)
|
||||
elif 0.6 < similarity < 0.9:
|
||||
else: # 0.6 < similarity < 0.9
|
||||
# p = s + 0.1
|
||||
probability = similarity + 0.1
|
||||
return min(1.0, max(0.0, probability))
|
||||
|
||||
|
||||
class SubMind:
|
||||
def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation):
|
||||
def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: ChattingObservation):
|
||||
self.last_active_time = None
|
||||
self.subheartflow_id = subheartflow_id
|
||||
|
||||
self.llm_model = LLMRequest(
|
||||
|
|
@ -100,10 +130,40 @@ class SubMind:
|
|||
|
||||
self.current_mind = ""
|
||||
self.past_mind = []
|
||||
self.structured_info = {}
|
||||
self.structured_info = []
|
||||
self.structured_info_str = ""
|
||||
|
||||
name = chat_manager.get_stream_name(self.subheartflow_id)
|
||||
self.log_prefix = f"[{name}] "
|
||||
self._update_structured_info_str()
|
||||
|
||||
def _update_structured_info_str(self):
|
||||
"""根据 structured_info 更新 structured_info_str"""
|
||||
if not self.structured_info:
|
||||
self.structured_info_str = ""
|
||||
return
|
||||
|
||||
lines = ["【信息】"]
|
||||
for item in self.structured_info:
|
||||
# 简化展示,突出内容和类型,包含TTL供调试
|
||||
type_str = item.get("type", "未知类型")
|
||||
content_str = item.get("content", "")
|
||||
|
||||
if type_str == "info":
|
||||
lines.append(f"刚刚: {content_str}")
|
||||
elif type_str == "memory":
|
||||
lines.append(f"{content_str}")
|
||||
elif type_str == "comparison_result":
|
||||
lines.append(f"数字大小比较结果: {content_str}")
|
||||
elif type_str == "time_info":
|
||||
lines.append(f"{content_str}")
|
||||
elif type_str == "lpmm_knowledge":
|
||||
lines.append(f"你知道:{content_str}")
|
||||
else:
|
||||
lines.append(f"{type_str}的信息: {content_str}")
|
||||
|
||||
self.structured_info_str = "\n".join(lines)
|
||||
logger.debug(f"{self.log_prefix} 更新 structured_info_str: \n{self.structured_info_str}")
|
||||
|
||||
async def do_thinking_before_reply(self, history_cycle: list[CycleInfo] = None):
|
||||
"""
|
||||
|
|
@ -115,18 +175,50 @@ class SubMind:
|
|||
# 更新活跃时间
|
||||
self.last_active_time = time.time()
|
||||
|
||||
# ---------- 0. 更新和清理 structured_info ----------
|
||||
if self.structured_info:
|
||||
logger.debug(
|
||||
f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
|
||||
)
|
||||
updated_info = []
|
||||
for item in self.structured_info:
|
||||
item["ttl"] -= 1
|
||||
if item["ttl"] > 0:
|
||||
updated_info.append(item)
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}")
|
||||
self.structured_info = updated_info
|
||||
logger.debug(
|
||||
f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
|
||||
)
|
||||
self._update_structured_info_str()
|
||||
logger.debug(
|
||||
f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
|
||||
)
|
||||
|
||||
# ---------- 1. 准备基础数据 ----------
|
||||
# 获取现有想法和情绪状态
|
||||
previous_mind = self.current_mind if self.current_mind else ""
|
||||
mood_info = self.chat_state.mood
|
||||
|
||||
# 获取观察对象
|
||||
observation = self.observations[0]
|
||||
if not observation:
|
||||
logger.error(f"{self.log_prefix} 无法获取观察对象")
|
||||
self.update_current_mind("(我没看到任何聊天内容...)")
|
||||
observation: ChattingObservation = self.observations[0] if self.observations else None
|
||||
if not observation or not hasattr(observation, "is_group_chat"): # Ensure it's ChattingObservation or similar
|
||||
logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息")
|
||||
self.update_current_mind("(观察出错了...)")
|
||||
return self.current_mind, self.past_mind
|
||||
|
||||
is_group_chat = observation.is_group_chat
|
||||
# logger.debug(f"is_group_chat: {is_group_chat}")
|
||||
|
||||
chat_target_info = observation.chat_target_info
|
||||
chat_target_name = "对方" # Default for private
|
||||
if not is_group_chat and chat_target_info:
|
||||
chat_target_name = (
|
||||
chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name
|
||||
)
|
||||
# --- End getting observation info ---
|
||||
|
||||
# 获取观察内容
|
||||
chat_observe_info = observation.get_observe_info()
|
||||
person_list = observation.person_list
|
||||
|
|
@ -168,7 +260,7 @@ class SubMind:
|
|||
|
||||
last_cycle = history_cycle[-1] if history_cycle else None
|
||||
# 上一次决策信息
|
||||
if last_cycle != None:
|
||||
if last_cycle is not None:
|
||||
last_action = last_cycle.action_type
|
||||
last_reasoning = last_cycle.reasoning
|
||||
is_replan = last_cycle.replanned
|
||||
|
|
@ -237,19 +329,39 @@ class SubMind:
|
|||
)[0]
|
||||
|
||||
# ---------- 4. 构建最终提示词 ----------
|
||||
# 获取提示词模板并填充数据
|
||||
prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format(
|
||||
extra_info="", # 可以在这里添加额外信息
|
||||
prompt_personality=prompt_personality,
|
||||
relation_prompt=relation_prompt,
|
||||
bot_name=individuality.name,
|
||||
time_now=time_now,
|
||||
chat_observe_info=chat_observe_info,
|
||||
mood_info=mood_info,
|
||||
hf_do_next=hf_do_next,
|
||||
last_loop_prompt=last_loop_prompt,
|
||||
cycle_info_block=cycle_info_block,
|
||||
)
|
||||
# --- Choose template based on chat type ---
|
||||
logger.debug(f"is_group_chat: {is_group_chat}")
|
||||
if is_group_chat:
|
||||
template_name = "sub_heartflow_prompt_before"
|
||||
prompt = (await global_prompt_manager.get_prompt_async(template_name)).format(
|
||||
extra_info=self.structured_info_str,
|
||||
prompt_personality=prompt_personality,
|
||||
relation_prompt=relation_prompt,
|
||||
bot_name=individuality.name,
|
||||
time_now=time_now,
|
||||
chat_observe_info=chat_observe_info,
|
||||
mood_info=mood_info,
|
||||
hf_do_next=hf_do_next,
|
||||
last_loop_prompt=last_loop_prompt,
|
||||
cycle_info_block=cycle_info_block,
|
||||
# chat_target_name is not used in group prompt
|
||||
)
|
||||
else: # Private chat
|
||||
template_name = "sub_heartflow_prompt_private_before"
|
||||
prompt = (await global_prompt_manager.get_prompt_async(template_name)).format(
|
||||
extra_info=self.structured_info_str,
|
||||
prompt_personality=prompt_personality,
|
||||
relation_prompt=relation_prompt, # Might need adjustment for private context
|
||||
bot_name=individuality.name,
|
||||
time_now=time_now,
|
||||
chat_target_name=chat_target_name, # Pass target name
|
||||
chat_observe_info=chat_observe_info,
|
||||
mood_info=mood_info,
|
||||
hf_do_next=hf_do_next,
|
||||
last_loop_prompt=last_loop_prompt,
|
||||
cycle_info_block=cycle_info_block,
|
||||
)
|
||||
# --- End choosing template ---
|
||||
|
||||
# ---------- 5. 执行LLM请求并处理响应 ----------
|
||||
content = "" # 初始化内容变量
|
||||
|
|
@ -389,7 +501,7 @@ class SubMind:
|
|||
tool_instance: 工具使用器实例
|
||||
"""
|
||||
tool_results = []
|
||||
structured_info = {} # 动态生成键
|
||||
new_structured_items = [] # 收集新产生的结构化信息
|
||||
|
||||
# 执行所有工具调用
|
||||
for tool_call in tool_calls:
|
||||
|
|
@ -397,23 +509,34 @@ class SubMind:
|
|||
result = await tool_instance._execute_tool_call(tool_call)
|
||||
if result:
|
||||
tool_results.append(result)
|
||||
# 创建新的结构化信息项
|
||||
new_item = {
|
||||
"type": result.get("type", "unknown_type"), # 使用 'type' 键
|
||||
"id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键
|
||||
"content": result.get("content", ""), # 'content' 键保持不变
|
||||
"ttl": 3,
|
||||
}
|
||||
new_structured_items.append(new_item)
|
||||
|
||||
# 使用工具名称作为键
|
||||
tool_name = result["name"]
|
||||
if tool_name not in structured_info:
|
||||
structured_info[tool_name] = []
|
||||
|
||||
structured_info[tool_name].append({"name": result["name"], "content": result["content"]})
|
||||
except Exception as tool_e:
|
||||
logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}")
|
||||
logger.error(traceback.format_exc()) # 添加 traceback 记录
|
||||
|
||||
# 如果有工具结果,记录并更新结构化信息
|
||||
if structured_info:
|
||||
logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}")
|
||||
self.structured_info = structured_info
|
||||
# 如果有新的工具结果,记录并更新结构化信息
|
||||
if new_structured_items:
|
||||
self.structured_info.extend(new_structured_items) # 添加到现有列表
|
||||
logger.debug(f"工具调用收集到新的结构化信息: {safe_json_dumps(new_structured_items, ensure_ascii=False)}")
|
||||
# logger.debug(f"当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") # 可以取消注释以查看完整列表
|
||||
self._update_structured_info_str() # 添加新信息后,更新字符串表示
|
||||
|
||||
def update_current_mind(self, response):
|
||||
self.past_mind.append(self.current_mind)
|
||||
if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind
|
||||
self.past_mind.append(self.current_mind)
|
||||
# 可以考虑限制 past_mind 的大小,例如:
|
||||
# max_past_mind_size = 10
|
||||
# if len(self.past_mind) > max_past_mind_size:
|
||||
# self.past_mind.pop(0) # 移除最旧的
|
||||
|
||||
self.current_mind = response
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,40 @@ INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
|
|||
NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟
|
||||
|
||||
|
||||
async def _try_set_subflow_absent_internal(subflow: "SubHeartflow", log_prefix: str) -> bool:
|
||||
"""
|
||||
尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。
|
||||
|
||||
Args:
|
||||
subflow: 子心流对象。
|
||||
log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。
|
||||
|
||||
Returns:
|
||||
bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。
|
||||
"""
|
||||
flow_id = subflow.subheartflow_id
|
||||
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
|
||||
|
||||
if subflow.chat_state.chat_status != ChatState.ABSENT:
|
||||
logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT")
|
||||
try:
|
||||
await subflow.change_chat_state(ChatState.ABSENT)
|
||||
# 再次检查以确认状态已更改 (change_chat_state 内部应确保)
|
||||
if subflow.chat_state.chat_status == ChatState.ABSENT:
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True)
|
||||
return False
|
||||
else:
|
||||
logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态")
|
||||
return True # 已经是目标状态,视为成功
|
||||
|
||||
|
||||
class SubHeartflowManager:
|
||||
"""管理所有活跃的 SubHeartflow 实例。"""
|
||||
|
||||
|
|
@ -93,6 +127,8 @@ class SubHeartflowManager:
|
|||
|
||||
# 添加聊天观察者
|
||||
observation = ChattingObservation(chat_id=subheartflow_id)
|
||||
await observation.initialize()
|
||||
|
||||
new_subflow.add_observation(observation)
|
||||
|
||||
# 注册子心流
|
||||
|
|
@ -109,38 +145,6 @@ class SubHeartflowManager:
|
|||
return None
|
||||
|
||||
# --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT ---
|
||||
async def _try_set_subflow_absent_internal(self, subflow: "SubHeartflow", log_prefix: str) -> bool:
|
||||
"""
|
||||
尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。
|
||||
|
||||
Args:
|
||||
subflow: 子心流对象。
|
||||
log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。
|
||||
|
||||
Returns:
|
||||
bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。
|
||||
"""
|
||||
flow_id = subflow.subheartflow_id
|
||||
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
|
||||
|
||||
if subflow.chat_state.chat_status != ChatState.ABSENT:
|
||||
logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT")
|
||||
try:
|
||||
await subflow.change_chat_state(ChatState.ABSENT)
|
||||
# 再次检查以确认状态已更改 (change_chat_state 内部应确保)
|
||||
if subflow.chat_state.chat_status == ChatState.ABSENT:
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True)
|
||||
return False
|
||||
else:
|
||||
logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态")
|
||||
return True # 已经是目标状态,视为成功
|
||||
|
||||
# --- 结束新增 ---
|
||||
|
||||
|
|
@ -154,7 +158,7 @@ class SubHeartflowManager:
|
|||
logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}")
|
||||
|
||||
# 调用内部方法处理状态变更
|
||||
success = await self._try_set_subflow_absent_internal(subheartflow, log_prefix)
|
||||
success = await _try_set_subflow_absent_internal(subheartflow, log_prefix)
|
||||
|
||||
return success
|
||||
# 锁在此处自动释放
|
||||
|
|
@ -241,7 +245,7 @@ class SubHeartflowManager:
|
|||
# 记录原始状态,以便统计实际改变的数量
|
||||
original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT
|
||||
|
||||
success = await self._try_set_subflow_absent_internal(subflow, log_prefix)
|
||||
success = await _try_set_subflow_absent_internal(subflow, log_prefix)
|
||||
|
||||
# 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数
|
||||
if success and not original_state_was_absent:
|
||||
|
|
@ -333,28 +337,37 @@ class SubHeartflowManager:
|
|||
|
||||
async def sbhf_absent_into_chat(self):
|
||||
"""
|
||||
随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态。
|
||||
随机选一个 ABSENT 状态的 *群聊* 子心流,评估是否应转换为 CHAT 状态。
|
||||
每次调用最多转换一个。
|
||||
私聊会被忽略。
|
||||
"""
|
||||
current_mai_state = self.mai_state_info.get_current_state()
|
||||
chat_limit = current_mai_state.get_normal_chat_max_num()
|
||||
|
||||
async with self._lock:
|
||||
# 1. 筛选出所有 ABSENT 状态的子心流
|
||||
absent_subflows = [
|
||||
hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT
|
||||
# 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流
|
||||
absent_group_subflows = [
|
||||
hf
|
||||
for hf in self.subheartflows.values()
|
||||
if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat
|
||||
]
|
||||
|
||||
if not absent_subflows:
|
||||
logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉
|
||||
if not absent_group_subflows:
|
||||
# logger.debug("没有摸鱼的群聊子心流可以评估。") # 日志太频繁
|
||||
return # 没有目标,直接返回
|
||||
|
||||
# 2. 随机选一个幸运儿
|
||||
sub_hf_to_evaluate = random.choice(absent_subflows)
|
||||
sub_hf_to_evaluate = random.choice(absent_group_subflows)
|
||||
flow_id = sub_hf_to_evaluate.subheartflow_id
|
||||
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
|
||||
log_prefix = f"[{stream_name}]"
|
||||
|
||||
# --- Private chat check (redundant due to filter above, but safe) ---
|
||||
# if not sub_hf_to_evaluate.is_group_chat:
|
||||
# logger.debug(f"{log_prefix} 是私聊,跳过 CHAT 状态评估。")
|
||||
# return
|
||||
# --- End check ---
|
||||
|
||||
# 3. 检查 CHAT 上限
|
||||
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
|
||||
if current_chat_count >= chat_limit:
|
||||
|
|
@ -656,8 +669,10 @@ class SubHeartflowManager:
|
|||
# --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
|
||||
async def sbhf_focus_into_absent(self, subflow_id: Any):
|
||||
"""
|
||||
接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。
|
||||
接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT 或 CHAT。
|
||||
通常在连续多次 "no_reply" 后被调用。
|
||||
对于私聊,总是转换为 ABSENT。
|
||||
对于群聊,随机决定转换为 ABSENT 或 CHAT (如果 CHAT 未达上限)。
|
||||
|
||||
Args:
|
||||
subflow_id: 需要转换状态的子心流 ID。
|
||||
|
|
@ -665,50 +680,46 @@ class SubHeartflowManager:
|
|||
async with self._lock:
|
||||
subflow = self.subheartflows.get(subflow_id)
|
||||
if not subflow:
|
||||
logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT")
|
||||
logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT/CHAT")
|
||||
return
|
||||
|
||||
stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id
|
||||
current_state = subflow.chat_state.chat_status
|
||||
|
||||
# 仅当子心流处于 FOCUSED 状态时才进行转换
|
||||
# 因为 HeartFChatting 只在 FOCUSED 状态下运行
|
||||
if current_state == ChatState.FOCUSED:
|
||||
target_state = ChatState.ABSENT # 默认目标状态
|
||||
log_reason = "默认转换"
|
||||
target_state = ChatState.ABSENT # Default target
|
||||
log_reason = "默认转换 (私聊或群聊)"
|
||||
|
||||
# 决定是去 ABSENT 还是 CHAT
|
||||
if random.random() < 0.5:
|
||||
target_state = ChatState.ABSENT
|
||||
log_reason = "随机选择 ABSENT"
|
||||
logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT")
|
||||
else:
|
||||
# 尝试进入 CHAT,先检查限制
|
||||
current_mai_state = self.mai_state_info.get_current_state()
|
||||
chat_limit = current_mai_state.get_normal_chat_max_num()
|
||||
# 使用不上锁的版本,因为我们已经在锁内
|
||||
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
|
||||
# --- Modify logic based on chat type --- #
|
||||
if subflow.is_group_chat:
|
||||
# Group chat: Decide between ABSENT or CHAT
|
||||
if random.random() < 0.5: # 50% chance to try CHAT
|
||||
current_mai_state = self.mai_state_info.get_current_state()
|
||||
chat_limit = current_mai_state.get_normal_chat_max_num()
|
||||
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
|
||||
|
||||
if current_chat_count < chat_limit:
|
||||
target_state = ChatState.CHAT
|
||||
log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})"
|
||||
logger.debug(
|
||||
f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,未达上限 ({current_chat_count}/{chat_limit})"
|
||||
)
|
||||
else:
|
||||
if current_chat_count < chat_limit:
|
||||
target_state = ChatState.CHAT
|
||||
log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})"
|
||||
else:
|
||||
target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached
|
||||
log_reason = (
|
||||
f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT"
|
||||
)
|
||||
else: # 50% chance to go directly to ABSENT
|
||||
target_state = ChatState.ABSENT
|
||||
log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT"
|
||||
logger.debug(
|
||||
f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT"
|
||||
)
|
||||
log_reason = "群聊随机选择 ABSENT"
|
||||
else:
|
||||
# Private chat: Always go to ABSENT
|
||||
target_state = ChatState.ABSENT
|
||||
log_reason = "私聊退出 FOCUSED,转为 ABSENT"
|
||||
# --- End modification --- #
|
||||
|
||||
# 开始转换
|
||||
logger.info(
|
||||
f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
|
||||
)
|
||||
try:
|
||||
await subflow.change_chat_state(target_state)
|
||||
# 检查最终状态
|
||||
final_state = subflow.chat_state.chat_status
|
||||
if final_state == target_state:
|
||||
logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}")
|
||||
|
|
@ -728,3 +739,106 @@ class SubHeartflowManager:
|
|||
)
|
||||
|
||||
# --- 结束新增 --- #
|
||||
|
||||
# --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- #
|
||||
async def sbhf_absent_private_into_focus(self):
|
||||
"""检查 ABSENT 状态的私聊子心流是否有新活动,若有且未达 FOCUSED 上限,则直接转换为 FOCUSED。"""
|
||||
log_prefix_task = "[私聊激活检查]"
|
||||
transitioned_count = 0
|
||||
checked_count = 0
|
||||
|
||||
# --- 获取当前状态和 FOCUSED 上限 --- #
|
||||
current_mai_state = self.mai_state_info.get_current_state()
|
||||
focused_limit = current_mai_state.get_focused_chat_max_num()
|
||||
|
||||
# --- 检查是否允许 FOCUS 模式 --- #
|
||||
if not global_config.allow_focus_mode:
|
||||
# Log less frequently to avoid spam
|
||||
# if int(time.time()) % 60 == 0:
|
||||
# logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态")
|
||||
return
|
||||
|
||||
if focused_limit <= 0:
|
||||
# logger.debug(f"{log_prefix_task} 当前状态 ({current_mai_state.value}) 不允许 FOCUSED 子心流")
|
||||
return
|
||||
|
||||
async with self._lock:
|
||||
# --- 获取当前 FOCUSED 计数 (不上锁版本) --- #
|
||||
current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED)
|
||||
|
||||
# --- 筛选出所有 ABSENT 状态的私聊子心流 --- #
|
||||
eligible_subflows = [
|
||||
hf
|
||||
for hf in self.subheartflows.values()
|
||||
if hf.chat_state.chat_status == ChatState.ABSENT and not hf.is_group_chat
|
||||
]
|
||||
checked_count = len(eligible_subflows)
|
||||
|
||||
if not eligible_subflows:
|
||||
# logger.debug(f"{log_prefix_task} 没有 ABSENT 状态的私聊子心流可以评估。")
|
||||
return
|
||||
|
||||
# --- 遍历评估每个符合条件的私聊 --- #
|
||||
for sub_hf in eligible_subflows:
|
||||
# --- 再次检查 FOCUSED 上限,因为可能有多个同时激活 --- #
|
||||
if current_focused_count >= focused_limit:
|
||||
logger.debug(
|
||||
f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。"
|
||||
)
|
||||
break # 已满,无需再检查其他私聊
|
||||
|
||||
flow_id = sub_hf.subheartflow_id
|
||||
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
|
||||
log_prefix = f"[{stream_name}]({log_prefix_task})"
|
||||
|
||||
try:
|
||||
# --- 检查是否有新活动 --- #
|
||||
observation = sub_hf._get_primary_observation() # 获取主要观察者
|
||||
is_active = False
|
||||
if observation:
|
||||
# 检查自上次状态变为 ABSENT 后是否有新消息
|
||||
# 使用 chat_state_changed_time 可能更精确
|
||||
# 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等
|
||||
timestamp_to_check = sub_hf.chat_state_changed_time - 1
|
||||
has_new = await observation.has_new_messages_since(timestamp_to_check)
|
||||
if has_new:
|
||||
is_active = True
|
||||
logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。")
|
||||
# 可选:检查兴趣度是否大于0 (如果需要)
|
||||
# interest_level = await sub_hf.interest_chatting.get_interest()
|
||||
# if interest_level > 0:
|
||||
# is_active = True
|
||||
# logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。")
|
||||
else:
|
||||
logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。")
|
||||
|
||||
# --- 如果活跃且未达上限,则尝试转换 --- #
|
||||
if is_active:
|
||||
logger.info(
|
||||
f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。"
|
||||
)
|
||||
await sub_hf.change_chat_state(ChatState.FOCUSED)
|
||||
# 确认转换成功
|
||||
if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
|
||||
transitioned_count += 1
|
||||
current_focused_count += 1 # 更新计数器以供本轮后续检查
|
||||
logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。")
|
||||
else:
|
||||
logger.warning(
|
||||
f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}"
|
||||
)
|
||||
# else: # 不活跃,无需操作
|
||||
# logger.debug(f"{log_prefix} 未检测到新活动,保持 ABSENT。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 检查私聊活动或转换状态时出错: {e}", exc_info=True)
|
||||
|
||||
# --- 循环结束后记录总结日志 --- #
|
||||
if transitioned_count > 0:
|
||||
logger.debug(
|
||||
f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。"
|
||||
)
|
||||
|
||||
# --- 结束新增 --- #
|
||||
|
||||
# --- 结束新增:处理来自 HeartFChatting 的状态转换请求 --- #
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import asyncio
|
||||
from typing import Optional, Tuple, Dict
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.chat.chat_stream import chat_manager
|
||||
from src.plugins.person_info.person_info import person_info_manager
|
||||
|
||||
logger = get_logger("heartflow_utils")
|
||||
|
||||
|
||||
async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Dict]]:
|
||||
"""
|
||||
获取聊天类型(是否群聊)和私聊对象信息。
|
||||
|
||||
Args:
|
||||
chat_id: 聊天流ID
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[Dict]]:
|
||||
- bool: 是否为群聊 (True 是群聊, False 是私聊或未知)
|
||||
- Optional[Dict]: 如果是私聊,包含对方信息的字典;否则为 None。
|
||||
字典包含: platform, user_id, user_nickname, person_id, person_name
|
||||
"""
|
||||
is_group_chat = False # Default to private/unknown
|
||||
chat_target_info = None
|
||||
|
||||
try:
|
||||
chat_stream = await asyncio.to_thread(chat_manager.get_stream, chat_id) # Use to_thread if get_stream is sync
|
||||
# If get_stream is already async, just use: chat_stream = await chat_manager.get_stream(chat_id)
|
||||
|
||||
if chat_stream:
|
||||
if chat_stream.group_info:
|
||||
is_group_chat = True
|
||||
chat_target_info = None # Explicitly None for group chat
|
||||
elif chat_stream.user_info: # It's a private chat
|
||||
is_group_chat = False
|
||||
user_info = chat_stream.user_info
|
||||
platform = chat_stream.platform
|
||||
user_id = user_info.user_id
|
||||
|
||||
# Initialize target_info with basic info
|
||||
target_info = {
|
||||
"platform": platform,
|
||||
"user_id": user_id,
|
||||
"user_nickname": user_info.user_nickname,
|
||||
"person_id": None,
|
||||
"person_name": None,
|
||||
}
|
||||
|
||||
# Try to fetch person info
|
||||
try:
|
||||
# Assume get_person_id is sync (as per original code), keep using to_thread
|
||||
person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id)
|
||||
person_name = None
|
||||
if person_id:
|
||||
# get_value is async, so await it directly
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
|
||||
target_info["person_id"] = person_id
|
||||
target_info["person_name"] = person_name
|
||||
except Exception as person_e:
|
||||
logger.warning(
|
||||
f"获取 person_id 或 person_name 时出错 for {platform}:{user_id} in utils: {person_e}"
|
||||
)
|
||||
|
||||
chat_target_info = target_info
|
||||
else:
|
||||
logger.warning(f"无法获取 chat_stream for {chat_id} in utils")
|
||||
# Keep defaults: is_group_chat=False, chat_target_info=None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取聊天类型和目标信息时出错 for {chat_id}: {e}", exc_info=True)
|
||||
# Keep defaults on error
|
||||
|
||||
return is_group_chat, chat_target_info
|
||||
|
|
@ -2,6 +2,9 @@ from typing import Optional
|
|||
from .personality import Personality
|
||||
from .identity import Identity
|
||||
import random
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
class Individuality:
|
||||
|
|
@ -113,7 +116,6 @@ class Individuality:
|
|||
p_pronoun = "我"
|
||||
prompt_personality = f"{p_pronoun}{self.personality.personality_core}"
|
||||
else: # x_person == 0
|
||||
p_pronoun = "" # 无人称
|
||||
# 对于无人称,直接描述核心特征
|
||||
prompt_personality = f"{self.personality.personality_core}"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from typing import Tuple, Union
|
|||
import aiohttp
|
||||
import requests
|
||||
from src.common.logger import get_module_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_module_logger("offline_llm")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import json
|
||||
from typing import Dict
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
|
||||
def load_scenes() -> Dict:
|
||||
def load_scenes() -> dict[str, Any]:
|
||||
"""
|
||||
从JSON文件加载场景数据
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ def load_scenes() -> Dict:
|
|||
PERSONALITY_SCENES = load_scenes()
|
||||
|
||||
|
||||
def get_scene_by_factor(factor: str) -> Dict:
|
||||
def get_scene_by_factor(factor: str) -> dict | None:
|
||||
"""
|
||||
根据人格因子获取对应的情景测试
|
||||
|
||||
|
|
@ -28,12 +28,12 @@ def get_scene_by_factor(factor: str) -> Dict:
|
|||
factor (str): 人格因子名称
|
||||
|
||||
Returns:
|
||||
Dict: 包含情景描述的字典
|
||||
dict: 包含情景描述的字典
|
||||
"""
|
||||
return PERSONALITY_SCENES.get(factor, None)
|
||||
|
||||
|
||||
def get_all_scenes() -> Dict:
|
||||
def get_all_scenes() -> dict:
|
||||
"""
|
||||
获取所有情景测试
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ from .common.logger_manager import get_logger
|
|||
from .plugins.remote import heartbeat_thread # noqa: F401
|
||||
from .individuality.individuality import Individuality
|
||||
from .common.server import global_server
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_logger("main")
|
||||
|
||||
|
|
|
|||
|
|
@ -262,7 +262,6 @@ class ActionPlanner:
|
|||
# --- 知识信息字符串构建结束 ---
|
||||
|
||||
# 获取聊天历史记录 (chat_history_text)
|
||||
chat_history_text = ""
|
||||
try:
|
||||
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||
chat_history_text = observation_info.chat_history_str
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ from maim_message import UserInfo
|
|||
from ...config.config import global_config
|
||||
from .chat_states import NotificationManager, create_new_message_notification, create_cold_chat_notification
|
||||
from .message_storage import MongoDBMessageStorage
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_module_logger("chat_observer")
|
||||
|
||||
|
|
@ -23,6 +26,7 @@ class ChatObserver:
|
|||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊名称
|
||||
|
||||
Returns:
|
||||
ChatObserver: 观察器实例
|
||||
|
|
@ -37,6 +41,9 @@ class ChatObserver:
|
|||
Args:
|
||||
stream_id: 聊天流ID
|
||||
"""
|
||||
self.last_check_time = None
|
||||
self.last_bot_speak_time = None
|
||||
self.last_user_speak_time = None
|
||||
if stream_id in self._instances:
|
||||
raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.")
|
||||
|
||||
|
|
@ -118,11 +125,11 @@ class ChatObserver:
|
|||
self.last_cold_chat_check = current_time
|
||||
|
||||
# 判断是否冷场
|
||||
is_cold = False
|
||||
if self.last_message_time is None:
|
||||
is_cold = True
|
||||
else:
|
||||
is_cold = (current_time - self.last_message_time) > self.cold_chat_threshold
|
||||
is_cold = (
|
||||
True
|
||||
if self.last_message_time is None
|
||||
else (current_time - self.last_message_time) > self.cold_chat_threshold
|
||||
)
|
||||
|
||||
# 如果冷场状态发生变化,发送通知
|
||||
if is_cold != self.is_cold_chat_state:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ from .pfc_KnowledgeFetcher import KnowledgeFetcher
|
|||
from .waiter import Waiter
|
||||
|
||||
import traceback
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_logger("pfc")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ from src.plugins.chat.message import MessageSending, MessageSet
|
|||
from src.plugins.chat.message_sender import message_manager
|
||||
from ..storage.storage import MessageStorage
|
||||
from ...config.config import global_config
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
logger = get_module_logger("message_sender")
|
||||
|
|
|
|||
|
|
@ -51,11 +51,9 @@ class MongoDBMessageStorage(MessageStorage):
|
|||
"""MongoDB消息存储实现"""
|
||||
|
||||
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
|
||||
query = {"chat_id": chat_id}
|
||||
query = {"chat_id": chat_id, "time": {"$gt": message_time}}
|
||||
# print(f"storage_check_message: {message_time}")
|
||||
|
||||
query["time"] = {"$gt": message_time}
|
||||
|
||||
return list(db.messages.find(query).sort("time", 1))
|
||||
|
||||
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import List, Optional, Dict, Any, Set
|
||||
from maim_message import UserInfo
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from src.common.logger import get_module_logger
|
||||
from .chat_observer import ChatObserver
|
||||
from .chat_states import NotificationHandler, NotificationType, Notification
|
||||
|
|
@ -121,47 +120,69 @@ class ObservationInfoHandler(NotificationHandler):
|
|||
logger.error(traceback.format_exc()) # 打印详细堆栈信息
|
||||
|
||||
|
||||
@dataclass
|
||||
# @dataclass <-- 这个,不需要了(递黄瓜)
|
||||
class ObservationInfo:
|
||||
"""决策信息类,用于收集和管理来自chat_observer的通知信息"""
|
||||
"""决策信息类,用于收集和管理来自chat_observer的通知信息 (手动实现 __init__)"""
|
||||
|
||||
# --- 修改:添加 private_name 字段 ---
|
||||
private_name: str = field(init=True) # 让 dataclass 的 __init__ 接收 private_name
|
||||
# 类型提示保留,可用于文档和静态分析
|
||||
private_name: str
|
||||
chat_history: List[Dict[str, Any]]
|
||||
chat_history_str: str
|
||||
unprocessed_messages: List[Dict[str, Any]]
|
||||
active_users: Set[str]
|
||||
last_bot_speak_time: Optional[float]
|
||||
last_user_speak_time: Optional[float]
|
||||
last_message_time: Optional[float]
|
||||
last_message_id: Optional[str]
|
||||
last_message_content: str
|
||||
last_message_sender: Optional[str]
|
||||
bot_id: Optional[str]
|
||||
chat_history_count: int
|
||||
new_messages_count: int
|
||||
cold_chat_start_time: Optional[float]
|
||||
cold_chat_duration: float
|
||||
is_typing: bool
|
||||
is_cold_chat: bool
|
||||
changed: bool
|
||||
chat_observer: Optional[ChatObserver]
|
||||
handler: Optional[ObservationInfoHandler]
|
||||
|
||||
# data_list
|
||||
chat_history: List[Dict[str, Any]] = field(default_factory=list) # 修改:明确类型为 Dict
|
||||
chat_history_str: str = ""
|
||||
unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list) # 修改:明确类型为 Dict
|
||||
active_users: Set[str] = field(default_factory=set)
|
||||
def __init__(self, private_name: str):
|
||||
"""
|
||||
手动初始化 ObservationInfo 的所有实例变量。
|
||||
"""
|
||||
|
||||
# data
|
||||
last_bot_speak_time: Optional[float] = None
|
||||
last_user_speak_time: Optional[float] = None
|
||||
last_message_time: Optional[float] = None
|
||||
# 添加 last_message_id
|
||||
last_message_id: Optional[str] = None
|
||||
last_message_content: str = ""
|
||||
last_message_sender: Optional[str] = None
|
||||
bot_id: Optional[str] = None
|
||||
chat_history_count: int = 0
|
||||
new_messages_count: int = 0
|
||||
cold_chat_start_time: Optional[float] = None # 用于计算冷场持续时间
|
||||
cold_chat_duration: float = 0.0 # 缓存计算结果
|
||||
# 接收的参数
|
||||
self.private_name: str = private_name
|
||||
|
||||
# state
|
||||
is_typing: bool = False # 可能表示对方正在输入
|
||||
# has_unread_messages: bool = False # 这个状态可以通过 new_messages_count > 0 判断
|
||||
is_cold_chat: bool = False
|
||||
changed: bool = False # 用于标记状态是否有变化,以便外部模块决定是否重新规划
|
||||
# data_list
|
||||
self.chat_history: List[Dict[str, Any]] = []
|
||||
self.chat_history_str: str = ""
|
||||
self.unprocessed_messages: List[Dict[str, Any]] = []
|
||||
self.active_users: Set[str] = set()
|
||||
|
||||
# #spec (暂时注释掉,如果不需要)
|
||||
# meta_plan_trigger: bool = False
|
||||
# data
|
||||
self.last_bot_speak_time: Optional[float] = None
|
||||
self.last_user_speak_time: Optional[float] = None
|
||||
self.last_message_time: Optional[float] = None
|
||||
self.last_message_id: Optional[str] = None
|
||||
self.last_message_content: str = ""
|
||||
self.last_message_sender: Optional[str] = None
|
||||
self.bot_id: Optional[str] = None
|
||||
self.chat_history_count: int = 0
|
||||
self.new_messages_count: int = 0
|
||||
self.cold_chat_start_time: Optional[float] = None
|
||||
self.cold_chat_duration: float = 0.0
|
||||
|
||||
# --- 修改:移除 __post_init__ 的参数 ---
|
||||
def __post_init__(self):
|
||||
"""初始化后创建handler并进行必要的设置"""
|
||||
self.chat_observer: Optional[ChatObserver] = None # 添加类型提示
|
||||
self.handler = ObservationInfoHandler(self, self.private_name)
|
||||
# state
|
||||
self.is_typing: bool = False
|
||||
self.is_cold_chat: bool = False
|
||||
self.changed: bool = False
|
||||
|
||||
# 关联对象
|
||||
self.chat_observer: Optional[ChatObserver] = None
|
||||
|
||||
self.handler: ObservationInfoHandler = ObservationInfoHandler(self, self.private_name)
|
||||
|
||||
def bind_to_chat_observer(self, chat_observer: ChatObserver):
|
||||
"""绑定到指定的chat_observer
|
||||
|
|
@ -175,6 +196,11 @@ class ObservationInfo:
|
|||
|
||||
self.chat_observer = chat_observer
|
||||
try:
|
||||
if not self.handler: # 确保 handler 已经被创建
|
||||
logger.error(f"[私聊][{self.private_name}] 尝试绑定时 handler 未初始化!")
|
||||
self.chat_observer = None # 重置,防止后续错误
|
||||
return
|
||||
|
||||
# 注册关心的通知类型
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
|
||||
|
|
@ -193,7 +219,9 @@ class ObservationInfo:
|
|||
|
||||
def unbind_from_chat_observer(self):
|
||||
"""解除与chat_observer的绑定"""
|
||||
if self.chat_observer and hasattr(self.chat_observer, "notification_manager"): # 增加检查
|
||||
if (
|
||||
self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler
|
||||
): # 增加 handler 检查
|
||||
try:
|
||||
self.chat_observer.notification_manager.unregister_handler(
|
||||
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
|
||||
|
|
@ -211,7 +239,7 @@ class ObservationInfo:
|
|||
finally: # 确保 chat_observer 被重置
|
||||
self.chat_observer = None
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在或无效")
|
||||
logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在、无效或 handler 未设置")
|
||||
|
||||
# 修改:update_from_message 接收 UserInfo 对象
|
||||
async def update_from_message(self, message: Dict[str, Any], user_info: Optional[UserInfo]):
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ from src.individuality.individuality import Individuality
|
|||
from .conversation_info import ConversationInfo
|
||||
from .observation_info import ObservationInfo
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
|
@ -15,6 +18,26 @@ if TYPE_CHECKING:
|
|||
logger = get_module_logger("pfc")
|
||||
|
||||
|
||||
def _calculate_similarity(goal1: str, goal2: str) -> float:
|
||||
"""简单计算两个目标之间的相似度
|
||||
|
||||
这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法
|
||||
|
||||
Args:
|
||||
goal1: 第一个目标
|
||||
goal2: 第二个目标
|
||||
|
||||
Returns:
|
||||
float: 相似度得分 (0-1)
|
||||
"""
|
||||
# 简单实现:检查重叠字数比例
|
||||
words1 = set(goal1)
|
||||
words2 = set(goal2)
|
||||
overlap = len(words1.intersection(words2))
|
||||
total = len(words1.union(words2))
|
||||
return overlap / total if total > 0 else 0
|
||||
|
||||
|
||||
class GoalAnalyzer:
|
||||
"""对话目标分析器"""
|
||||
|
||||
|
|
@ -147,14 +170,14 @@ class GoalAnalyzer:
|
|||
# 返回第一个目标作为当前主要目标(如果有)
|
||||
if result:
|
||||
first_goal = result[0]
|
||||
return (first_goal.get("goal", ""), "", first_goal.get("reasoning", ""))
|
||||
return first_goal.get("goal", ""), "", first_goal.get("reasoning", "")
|
||||
else:
|
||||
# 单个目标的情况
|
||||
conversation_info.goal_list.append(result)
|
||||
return (goal, "", reasoning)
|
||||
return goal, "", reasoning
|
||||
|
||||
# 如果解析失败,返回默认值
|
||||
return ("", "", "")
|
||||
return "", "", ""
|
||||
|
||||
async def _update_goals(self, new_goal: str, method: str, reasoning: str):
|
||||
"""更新目标列表
|
||||
|
|
@ -166,7 +189,7 @@ class GoalAnalyzer:
|
|||
"""
|
||||
# 检查新目标是否与现有目标相似
|
||||
for i, (existing_goal, _, _) in enumerate(self.goals):
|
||||
if self._calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值
|
||||
if _calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值
|
||||
# 更新现有目标
|
||||
self.goals[i] = (new_goal, method, reasoning)
|
||||
# 将此目标移到列表前面(最主要的位置)
|
||||
|
|
@ -180,25 +203,6 @@ class GoalAnalyzer:
|
|||
if len(self.goals) > self.max_goals:
|
||||
self.goals.pop() # 移除最老的目标
|
||||
|
||||
def _calculate_similarity(self, goal1: str, goal2: str) -> float:
|
||||
"""简单计算两个目标之间的相似度
|
||||
|
||||
这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法
|
||||
|
||||
Args:
|
||||
goal1: 第一个目标
|
||||
goal2: 第二个目标
|
||||
|
||||
Returns:
|
||||
float: 相似度得分 (0-1)
|
||||
"""
|
||||
# 简单实现:检查重叠字数比例
|
||||
words1 = set(goal1)
|
||||
words2 = set(goal2)
|
||||
overlap = len(words1.intersection(words2))
|
||||
total = len(words1.union(words2))
|
||||
return overlap / total if total > 0 else 0
|
||||
|
||||
async def get_all_goals(self) -> List[Tuple[str, str, str]]:
|
||||
"""获取所有当前目标
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class PFCManager:
|
|||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊名称
|
||||
|
||||
Returns:
|
||||
Optional[Conversation]: 对话实例,创建失败则返回None
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ def get_items_from_json(
|
|||
|
||||
Args:
|
||||
content: 包含JSON的文本
|
||||
private_name: 私聊名称
|
||||
*items: 要提取的字段名
|
||||
default_values: 字段的默认值,格式为 {字段名: 默认值}
|
||||
required_types: 字段的必需类型,格式为 {字段名: 类型}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ class ReplyChecker:
|
|||
Args:
|
||||
reply: 生成的回复
|
||||
goal: 对话目标
|
||||
chat_history: 对话历史记录
|
||||
chat_history_text: 对话历史记录文本
|
||||
retry_count: 当前重试次数
|
||||
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Dict, Any
|
||||
|
||||
from ..moods.moods import MoodManager # 导入情绪管理器
|
||||
from ...config.config import global_config
|
||||
from .message import MessageRecv
|
||||
|
|
@ -46,7 +48,7 @@ class ChatBot:
|
|||
except Exception as e:
|
||||
logger.error(f"创建PFC聊天失败: {e}")
|
||||
|
||||
async def message_process(self, message_data: str) -> None:
|
||||
async def message_process(self, message_data: Dict[str, Any]) -> None:
|
||||
"""处理转化后的统一格式消息
|
||||
这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中
|
||||
heart_flow模式:使用思维流系统进行回复
|
||||
|
|
@ -81,8 +83,15 @@ class ChatBot:
|
|||
logger.debug(f"用户{userinfo.user_id}被禁止回复")
|
||||
return
|
||||
|
||||
if groupinfo is None:
|
||||
logger.trace("检测到私聊消息,检查")
|
||||
# 好友黑名单拦截
|
||||
if userinfo.user_id not in global_config.talk_allowed_private:
|
||||
logger.debug(f"用户{userinfo.user_id}没有私聊权限")
|
||||
return
|
||||
|
||||
# 群聊黑名单拦截
|
||||
if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups:
|
||||
if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups:
|
||||
logger.trace(f"群{groupinfo.group_id}被禁止回复")
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ from ...common.database import db
|
|||
from maim_message import GroupInfo, UserInfo
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
logger = get_logger("chat_stream")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import time
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Optional, Any
|
||||
|
||||
import urllib3
|
||||
|
||||
|
|
@ -8,6 +9,9 @@ from src.common.logger_manager import get_logger
|
|||
from .chat_stream import ChatStream
|
||||
from .utils_image import image_manager
|
||||
from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_logger("chat_message")
|
||||
|
||||
|
|
@ -30,19 +34,21 @@ class Message(MessageBase):
|
|||
def __init__(
|
||||
self,
|
||||
message_id: str,
|
||||
timestamp: float,
|
||||
chat_stream: ChatStream,
|
||||
user_info: UserInfo,
|
||||
message_segment: Optional[Seg] = None,
|
||||
timestamp: Optional[float] = None,
|
||||
reply: Optional["MessageRecv"] = None,
|
||||
detailed_plain_text: str = "",
|
||||
processed_plain_text: str = "",
|
||||
):
|
||||
# 使用传入的时间戳或当前时间
|
||||
current_timestamp = timestamp if timestamp is not None else round(time.time(), 3)
|
||||
# 构造基础消息信息
|
||||
message_info = BaseMessageInfo(
|
||||
platform=chat_stream.platform,
|
||||
message_id=message_id,
|
||||
time=timestamp,
|
||||
time=current_timestamp,
|
||||
group_info=chat_stream.group_info,
|
||||
user_info=user_info,
|
||||
)
|
||||
|
|
@ -58,12 +64,37 @@ class Message(MessageBase):
|
|||
# 回复消息
|
||||
self.reply = reply
|
||||
|
||||
async def _process_message_segments(self, segment: Seg) -> str:
|
||||
"""递归处理消息段,转换为文字描述
|
||||
|
||||
Args:
|
||||
segment: 要处理的消息段
|
||||
|
||||
Returns:
|
||||
str: 处理后的文本
|
||||
"""
|
||||
if segment.type == "seglist":
|
||||
# 处理消息段列表
|
||||
segments_text = []
|
||||
for seg in segment.data:
|
||||
processed = await self._process_message_segments(seg)
|
||||
if processed:
|
||||
segments_text.append(processed)
|
||||
return " ".join(segments_text)
|
||||
else:
|
||||
# 处理单个消息段
|
||||
return await self._process_single_segment(segment)
|
||||
|
||||
@abstractmethod
|
||||
async def _process_single_segment(self, segment):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageRecv(Message):
|
||||
"""接收消息类,用于处理从MessageCQ序列化的消息"""
|
||||
|
||||
def __init__(self, message_dict: Dict):
|
||||
def __init__(self, message_dict: dict[str, Any]):
|
||||
"""从MessageCQ的字典初始化
|
||||
|
||||
Args:
|
||||
|
|
@ -90,27 +121,6 @@ class MessageRecv(Message):
|
|||
self.processed_plain_text = await self._process_message_segments(self.message_segment)
|
||||
self.detailed_plain_text = self._generate_detailed_text()
|
||||
|
||||
async def _process_message_segments(self, segment: Seg) -> str:
|
||||
"""递归处理消息段,转换为文字描述
|
||||
|
||||
Args:
|
||||
segment: 要处理的消息段
|
||||
|
||||
Returns:
|
||||
str: 处理后的文本
|
||||
"""
|
||||
if segment.type == "seglist":
|
||||
# 处理消息段列表
|
||||
segments_text = []
|
||||
for seg in segment.data:
|
||||
processed = await self._process_message_segments(seg)
|
||||
if processed:
|
||||
segments_text.append(processed)
|
||||
return " ".join(segments_text)
|
||||
else:
|
||||
# 处理单个消息段
|
||||
return await self._process_single_segment(segment)
|
||||
|
||||
async def _process_single_segment(self, seg: Seg) -> str:
|
||||
"""处理单个消息段
|
||||
|
||||
|
|
@ -159,11 +169,12 @@ class MessageProcessBase(Message):
|
|||
message_segment: Optional[Seg] = None,
|
||||
reply: Optional["MessageRecv"] = None,
|
||||
thinking_start_time: float = 0,
|
||||
timestamp: Optional[float] = None,
|
||||
):
|
||||
# 调用父类初始化
|
||||
# 调用父类初始化,传递时间戳
|
||||
super().__init__(
|
||||
message_id=message_id,
|
||||
timestamp=round(time.time(), 3), # 保留3位小数
|
||||
timestamp=timestamp,
|
||||
chat_stream=chat_stream,
|
||||
user_info=bot_user_info,
|
||||
message_segment=message_segment,
|
||||
|
|
@ -179,28 +190,7 @@ class MessageProcessBase(Message):
|
|||
self.thinking_time = round(time.time() - self.thinking_start_time, 2)
|
||||
return self.thinking_time
|
||||
|
||||
async def _process_message_segments(self, segment: Seg) -> str:
|
||||
"""递归处理消息段,转换为文字描述
|
||||
|
||||
Args:
|
||||
segment: 要处理的消息段
|
||||
|
||||
Returns:
|
||||
str: 处理后的文本
|
||||
"""
|
||||
if segment.type == "seglist":
|
||||
# 处理消息段列表
|
||||
segments_text = []
|
||||
for seg in segment.data:
|
||||
processed = await self._process_message_segments(seg)
|
||||
if processed:
|
||||
segments_text.append(processed)
|
||||
return " ".join(segments_text)
|
||||
else:
|
||||
# 处理单个消息段
|
||||
return await self._process_single_segment(segment)
|
||||
|
||||
async def _process_single_segment(self, seg: Seg) -> Union[str, None]:
|
||||
async def _process_single_segment(self, seg: Seg) -> str | None:
|
||||
"""处理单个消息段
|
||||
|
||||
Args:
|
||||
|
|
@ -254,8 +244,9 @@ class MessageThinking(MessageProcessBase):
|
|||
bot_user_info: UserInfo,
|
||||
reply: Optional["MessageRecv"] = None,
|
||||
thinking_start_time: float = 0,
|
||||
timestamp: Optional[float] = None,
|
||||
):
|
||||
# 调用父类初始化
|
||||
# 调用父类初始化,传递时间戳
|
||||
super().__init__(
|
||||
message_id=message_id,
|
||||
chat_stream=chat_stream,
|
||||
|
|
@ -263,6 +254,7 @@ class MessageThinking(MessageProcessBase):
|
|||
message_segment=None, # 思考状态不需要消息段
|
||||
reply=reply,
|
||||
thinking_start_time=thinking_start_time,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
# 思考状态特有属性
|
||||
|
|
@ -278,7 +270,7 @@ class MessageSending(MessageProcessBase):
|
|||
message_id: str,
|
||||
chat_stream: ChatStream,
|
||||
bot_user_info: UserInfo,
|
||||
sender_info: UserInfo, # 用来记录发送者信息,用于私聊回复
|
||||
sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复
|
||||
message_segment: Seg,
|
||||
reply: Optional["MessageRecv"] = None,
|
||||
is_head: bool = False,
|
||||
|
|
@ -303,9 +295,11 @@ class MessageSending(MessageProcessBase):
|
|||
self.is_emoji = is_emoji
|
||||
self.apply_set_reply_logic = apply_set_reply_logic
|
||||
|
||||
def set_reply(self, reply: Optional["MessageRecv"] = None) -> None:
|
||||
def set_reply(self, reply: Optional["MessageRecv"] = None):
|
||||
"""设置回复消息"""
|
||||
if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format:
|
||||
# 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
|
||||
if self.reply:
|
||||
|
|
@ -317,7 +311,6 @@ class MessageSending(MessageProcessBase):
|
|||
self.message_segment,
|
||||
],
|
||||
)
|
||||
return self
|
||||
|
||||
async def process(self) -> None:
|
||||
"""处理消息内容,生成纯文本和详细文本"""
|
||||
|
|
@ -342,6 +335,7 @@ class MessageSending(MessageProcessBase):
|
|||
reply=thinking.reply,
|
||||
is_head=is_head,
|
||||
is_emoji=is_emoji,
|
||||
sender_info=None,
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
|
|
@ -361,7 +355,7 @@ class MessageSet:
|
|||
def __init__(self, chat_stream: ChatStream, message_id: str):
|
||||
self.chat_stream = chat_stream
|
||||
self.message_id = message_id
|
||||
self.messages: List[MessageSending] = []
|
||||
self.messages: list[MessageSending] = []
|
||||
self.time = round(time.time(), 3) # 保留3位小数
|
||||
|
||||
def add_message(self, message: MessageSending) -> None:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# src/plugins/chat/message_sender.py
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, List, Optional, Union
|
||||
from asyncio import Task
|
||||
from typing import Union
|
||||
from src.plugins.message.api import global_api
|
||||
|
||||
# from ...common.database import db # 数据库依赖似乎不需要了,注释掉
|
||||
from ..message.api import global_api
|
||||
from .message import MessageSending, MessageThinking, MessageSet
|
||||
|
||||
from ..storage.storage import MessageStorage
|
||||
|
|
@ -12,11 +13,48 @@ from ...config.config import global_config
|
|||
from .utils import truncate_message, calculate_typing_time, count_messages_between
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
logger = get_logger("sender")
|
||||
|
||||
|
||||
async def send_via_ws(message: MessageSending) -> None:
|
||||
"""通过 WebSocket 发送消息"""
|
||||
try:
|
||||
await global_api.send_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"WS发送失败: {e}")
|
||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
|
||||
|
||||
async def send_message(
|
||||
message: MessageSending,
|
||||
) -> None:
|
||||
"""发送消息(核心发送逻辑)"""
|
||||
|
||||
# --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) ---
|
||||
typing_time = calculate_typing_time(
|
||||
input_string=message.processed_plain_text,
|
||||
thinking_start_time=message.thinking_start_time,
|
||||
is_emoji=message.is_emoji,
|
||||
)
|
||||
# logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志
|
||||
await asyncio.sleep(typing_time)
|
||||
# logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志
|
||||
# --- 结束打字延迟 ---
|
||||
|
||||
message_preview = truncate_message(message.processed_plain_text)
|
||||
|
||||
try:
|
||||
await send_via_ws(message)
|
||||
logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
||||
|
||||
|
||||
class MessageSender:
|
||||
"""发送器 (不再是单例)"""
|
||||
|
||||
|
|
@ -29,39 +67,6 @@ class MessageSender:
|
|||
"""设置当前bot实例"""
|
||||
pass
|
||||
|
||||
async def send_via_ws(self, message: MessageSending) -> None:
|
||||
"""通过 WebSocket 发送消息"""
|
||||
try:
|
||||
await global_api.send_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"WS发送失败: {e}")
|
||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
message: MessageSending,
|
||||
) -> None:
|
||||
"""发送消息(核心发送逻辑)"""
|
||||
|
||||
# --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) ---
|
||||
typing_time = calculate_typing_time(
|
||||
input_string=message.processed_plain_text,
|
||||
thinking_start_time=message.thinking_start_time,
|
||||
is_emoji=message.is_emoji,
|
||||
)
|
||||
# logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志
|
||||
await asyncio.sleep(typing_time)
|
||||
# logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志
|
||||
# --- 结束打字延迟 ---
|
||||
|
||||
message_preview = truncate_message(message.processed_plain_text)
|
||||
|
||||
try:
|
||||
await self.send_via_ws(message)
|
||||
logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
||||
|
||||
|
||||
class MessageContainer:
|
||||
"""单个聊天流的发送/思考消息容器"""
|
||||
|
|
@ -69,7 +74,7 @@ class MessageContainer:
|
|||
def __init__(self, chat_id: str, max_size: int = 100):
|
||||
self.chat_id = chat_id
|
||||
self.max_size = max_size
|
||||
self.messages: List[Union[MessageThinking, MessageSending]] = [] # 明确类型
|
||||
self.messages: list[MessageThinking | MessageSending] = [] # 明确类型
|
||||
self.last_send_time = 0
|
||||
self.thinking_wait_timeout = 20 # 思考等待超时时间(秒) - 从旧 sender 合并
|
||||
|
||||
|
|
@ -77,7 +82,7 @@ class MessageContainer:
|
|||
"""计算当前容器中思考消息的数量"""
|
||||
return sum(1 for msg in self.messages if isinstance(msg, MessageThinking))
|
||||
|
||||
def get_timeout_sending_messages(self) -> List[MessageSending]:
|
||||
def get_timeout_sending_messages(self) -> list[MessageSending]:
|
||||
"""获取所有超时的MessageSending对象(思考时间超过20秒),按thinking_start_time排序 - 从旧 sender 合并"""
|
||||
current_time = time.time()
|
||||
timeout_messages = []
|
||||
|
|
@ -93,7 +98,7 @@ class MessageContainer:
|
|||
timeout_messages.sort(key=lambda x: x.thinking_start_time)
|
||||
return timeout_messages
|
||||
|
||||
def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]:
|
||||
def get_earliest_message(self):
|
||||
"""获取thinking_start_time最早的消息对象"""
|
||||
if not self.messages:
|
||||
return None
|
||||
|
|
@ -107,7 +112,7 @@ class MessageContainer:
|
|||
earliest_message = msg
|
||||
return earliest_message
|
||||
|
||||
def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None:
|
||||
def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]):
|
||||
"""添加消息到队列"""
|
||||
if isinstance(message, MessageSet):
|
||||
for single_message in message.messages:
|
||||
|
|
@ -115,11 +120,11 @@ class MessageContainer:
|
|||
else:
|
||||
self.messages.append(message)
|
||||
|
||||
def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]) -> bool:
|
||||
def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]):
|
||||
"""移除指定的消息对象,如果消息存在则返回True,否则返回False"""
|
||||
try:
|
||||
_initial_len = len(self.messages)
|
||||
# 使用列表推导式或 filter 创建新列表,排除要删除的元素
|
||||
# 使用列表推导式或 message_filter 创建新列表,排除要删除的元素
|
||||
# self.messages = [msg for msg in self.messages if msg is not message_to_remove]
|
||||
# 或者直接 remove (如果确定对象唯一性)
|
||||
if message_to_remove in self.messages:
|
||||
|
|
@ -137,7 +142,7 @@ class MessageContainer:
|
|||
"""检查是否有待发送的消息"""
|
||||
return bool(self.messages)
|
||||
|
||||
def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]:
|
||||
def get_all_messages(self) -> list[MessageThinking | MessageSending]:
|
||||
"""获取所有消息"""
|
||||
return list(self.messages) # 返回副本
|
||||
|
||||
|
|
@ -146,7 +151,8 @@ class MessageManager:
|
|||
"""管理所有聊天流的消息容器 (不再是单例)"""
|
||||
|
||||
def __init__(self):
|
||||
self.containers: Dict[str, MessageContainer] = {}
|
||||
self._processor_task: Task | None = None
|
||||
self.containers: dict[str, MessageContainer] = {}
|
||||
self.storage = MessageStorage() # 添加 storage 实例
|
||||
self._running = True # 处理器运行状态
|
||||
self._container_lock = asyncio.Lock() # 保护 containers 字典的锁
|
||||
|
|
@ -155,7 +161,7 @@ class MessageManager:
|
|||
async def start(self):
|
||||
"""启动后台处理器任务。"""
|
||||
# 检查是否已有任务在运行,避免重复启动
|
||||
if hasattr(self, "_processor_task") and not self._processor_task.done():
|
||||
if self._processor_task is not None and not self._processor_task.done():
|
||||
logger.warning("Processor task already running.")
|
||||
return
|
||||
self._processor_task = asyncio.create_task(self._start_processor_loop())
|
||||
|
|
@ -164,7 +170,7 @@ class MessageManager:
|
|||
def stop(self):
|
||||
"""停止后台处理器任务。"""
|
||||
self._running = False
|
||||
if hasattr(self, "_processor_task") and not self._processor_task.done():
|
||||
if self._processor_task is not None and not self._processor_task.done():
|
||||
self._processor_task.cancel()
|
||||
logger.debug("MessageManager processor task stopping.")
|
||||
else:
|
||||
|
|
@ -206,27 +212,34 @@ class MessageManager:
|
|||
_ = message.update_thinking_time() # 更新思考时间
|
||||
thinking_start_time = message.thinking_start_time
|
||||
now_time = time.time()
|
||||
logger.debug(f"thinking_start_time:{thinking_start_time},now_time:{now_time}")
|
||||
thinking_messages_count, thinking_messages_length = count_messages_between(
|
||||
start_time=thinking_start_time, end_time=now_time, stream_id=message.chat_stream.stream_id
|
||||
)
|
||||
# print(f"message.reply:{message.reply}")
|
||||
|
||||
# --- 条件应用 set_reply 逻辑 ---
|
||||
logger.debug(
|
||||
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
|
||||
and (thinking_messages_count > 4 or thinking_messages_length > 250)
|
||||
and (thinking_messages_count > 3 or thinking_messages_length > 200)
|
||||
and not message.is_private_message()
|
||||
):
|
||||
logger.debug(
|
||||
f"[{message.chat_stream.stream_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}..."
|
||||
)
|
||||
message.set_reply()
|
||||
message.set_reply(message.reply)
|
||||
# --- 结束条件 set_reply ---
|
||||
|
||||
await message.process() # 预处理消息内容
|
||||
|
||||
logger.debug(f"{message}")
|
||||
|
||||
# 使用全局 message_sender 实例
|
||||
await message_sender.send_message(message)
|
||||
await send_message(message)
|
||||
await self.storage.store_message(message, message.chat_stream)
|
||||
|
||||
# 移除消息要在发送 *之后*
|
||||
|
|
|
|||
|
|
@ -2,18 +2,17 @@ import random
|
|||
import time
|
||||
import re
|
||||
from collections import Counter
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import jieba
|
||||
import numpy as np
|
||||
from src.common.logger import get_module_logger
|
||||
from pymongo.errors import PyMongoError
|
||||
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ..utils.typo_generator import ChineseTypoGenerator
|
||||
from ...config.config import global_config
|
||||
from .message import MessageRecv, Message
|
||||
from .message import MessageRecv
|
||||
from maim_message import UserInfo
|
||||
from .chat_stream import ChatStream
|
||||
from ..moods.moods import MoodManager
|
||||
from ...common.database import db
|
||||
|
||||
|
|
@ -26,7 +25,7 @@ def is_english_letter(char: str) -> bool:
|
|||
return "a" <= char.lower() <= "z"
|
||||
|
||||
|
||||
def db_message_to_str(message_dict: Dict) -> str:
|
||||
def db_message_to_str(message_dict: dict) -> str:
|
||||
logger.debug(f"message_dict: {message_dict}")
|
||||
time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message_dict["time"]))
|
||||
try:
|
||||
|
|
@ -77,13 +76,13 @@ 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)}\):[\s\S]*?\],说:", message.processed_plain_text
|
||||
f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:", 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"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?],说:", "", message_content)
|
||||
for keyword in keywords:
|
||||
if keyword in message_content:
|
||||
is_mentioned = True
|
||||
|
|
@ -108,56 +107,7 @@ async def get_embedding(text, request_type="embedding"):
|
|||
return embedding
|
||||
|
||||
|
||||
async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list:
|
||||
"""从数据库获取群组最近的消息记录
|
||||
|
||||
Args:
|
||||
chat_id: 群组ID
|
||||
limit: 获取消息数量,默认12条
|
||||
|
||||
Returns:
|
||||
list: Message对象列表,按时间正序排列
|
||||
"""
|
||||
|
||||
# 从数据库获取最近消息
|
||||
recent_messages = list(
|
||||
db.messages.find(
|
||||
{"chat_id": chat_id},
|
||||
)
|
||||
.sort("time", -1)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
if not recent_messages:
|
||||
return []
|
||||
|
||||
# 转换为 Message对象列表
|
||||
message_objects = []
|
||||
for msg_data in recent_messages:
|
||||
try:
|
||||
chat_info = msg_data.get("chat_info", {})
|
||||
chat_stream = ChatStream.from_dict(chat_info)
|
||||
user_info = msg_data.get("user_info", {})
|
||||
user_info = UserInfo.from_dict(user_info)
|
||||
msg = Message(
|
||||
message_id=msg_data["message_id"],
|
||||
chat_stream=chat_stream,
|
||||
timestamp=msg_data["time"],
|
||||
user_info=user_info,
|
||||
processed_plain_text=msg_data.get("processed_text", ""),
|
||||
detailed_plain_text=msg_data.get("detailed_plain_text", ""),
|
||||
)
|
||||
message_objects.append(msg)
|
||||
except KeyError:
|
||||
logger.warning("数据库中存在无效的消息")
|
||||
continue
|
||||
|
||||
# 按时间正序排列
|
||||
message_objects.reverse()
|
||||
return message_objects
|
||||
|
||||
|
||||
def get_recent_group_detailed_plain_text(chat_stream_id: int, limit: int = 12, combine=False):
|
||||
def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False):
|
||||
recent_messages = list(
|
||||
db.messages.find(
|
||||
{"chat_id": chat_stream_id},
|
||||
|
|
@ -223,7 +173,7 @@ def get_recent_group_speaker(chat_stream_id: int, sender, limit: int = 12) -> li
|
|||
return who_chat_in_group
|
||||
|
||||
|
||||
def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
|
||||
def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
|
||||
"""将文本分割成句子,并根据概率合并
|
||||
1. 识别分割点(, , 。 ; 空格),但如果分割点左右都是英文字母则不分割。
|
||||
2. 将文本分割成 (内容, 分隔符) 的元组。
|
||||
|
|
@ -263,7 +213,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
|
|||
if char in separators:
|
||||
# 检查分割条件:如果分隔符左右都是英文字母,则不分割
|
||||
can_split = True
|
||||
if i > 0 and i < len(text) - 1:
|
||||
if 0 < i < len(text) - 1:
|
||||
prev_char = text[i - 1]
|
||||
next_char = text[i + 1]
|
||||
# if is_english_letter(prev_char) and is_english_letter(next_char) and char == ' ': # 原计划只对空格应用此规则,现应用于所有分隔符
|
||||
|
|
@ -370,7 +320,7 @@ def random_remove_punctuation(text: str) -> str:
|
|||
return result
|
||||
|
||||
|
||||
def process_llm_response(text: str) -> List[str]:
|
||||
def process_llm_response(text: str) -> list[str]:
|
||||
# 先保护颜文字
|
||||
if global_config.enable_kaomoji_protection:
|
||||
protected_text, kaomoji_mapping = protect_kaomoji(text)
|
||||
|
|
@ -379,7 +329,7 @@ def process_llm_response(text: str) -> List[str]:
|
|||
protected_text = text
|
||||
kaomoji_mapping = {}
|
||||
# 提取被 () 或 [] 包裹且包含中文的内容
|
||||
pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]")
|
||||
pattern = re.compile(r"[(\[(](?=.*[一-鿿]).*?[)\])]")
|
||||
# _extracted_contents = pattern.findall(text)
|
||||
_extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
|
||||
# 去除 () 和 [] 及其包裹的内容
|
||||
|
|
@ -554,7 +504,7 @@ def protect_kaomoji(sentence):
|
|||
r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配)
|
||||
r"[^一-龥a-zA-Z0-9\s]" # 非中文、非英文、非数字、非空格字符(必须包含至少一个)
|
||||
r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配)
|
||||
r"[\)\])】" # 右括号
|
||||
r"[)\])】" # 右括号
|
||||
r"]"
|
||||
r")"
|
||||
r"|"
|
||||
|
|
@ -614,97 +564,49 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) -
|
|||
"""计算两个时间点之间的消息数量和文本总长度
|
||||
|
||||
Args:
|
||||
start_time (float): 起始时间戳
|
||||
end_time (float): 结束时间戳
|
||||
start_time (float): 起始时间戳 (不包含)
|
||||
end_time (float): 结束时间戳 (包含)
|
||||
stream_id (str): 聊天流ID
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: (消息数量, 文本总长度)
|
||||
- 消息数量:包含起始时间的消息,不包含结束时间的消息
|
||||
- 文本总长度:所有消息的processed_plain_text长度之和
|
||||
"""
|
||||
count = 0
|
||||
total_length = 0
|
||||
|
||||
# 参数校验 (可选但推荐)
|
||||
if start_time >= end_time:
|
||||
# logger.debug(f"开始时间 {start_time} 大于或等于结束时间 {end_time},返回 0, 0")
|
||||
return 0, 0
|
||||
if not stream_id:
|
||||
logger.error("stream_id 不能为空")
|
||||
return 0, 0
|
||||
|
||||
# 直接查询时间范围内的消息
|
||||
# time > start_time AND time <= end_time
|
||||
query = {"chat_id": stream_id, "time": {"$gt": start_time, "$lte": end_time}}
|
||||
|
||||
try:
|
||||
# 获取开始时间之前最新的一条消息
|
||||
start_message = db.messages.find_one(
|
||||
{"chat_id": stream_id, "time": {"$lte": start_time}},
|
||||
sort=[("time", -1), ("_id", -1)], # 按时间倒序,_id倒序(最后插入的在前)
|
||||
)
|
||||
# 执行查询
|
||||
messages_cursor = db.messages.find(query)
|
||||
|
||||
# 获取结束时间最近的一条消息
|
||||
# 先找到结束时间点的所有消息
|
||||
end_time_messages = list(
|
||||
db.messages.find(
|
||||
{"chat_id": stream_id, "time": {"$lte": end_time}},
|
||||
sort=[("time", -1)], # 先按时间倒序
|
||||
).limit(10)
|
||||
) # 限制查询数量,避免性能问题
|
||||
|
||||
if not end_time_messages:
|
||||
logger.warning(f"未找到结束时间 {end_time} 之前的消息")
|
||||
return 0, 0
|
||||
|
||||
# 找到最大时间
|
||||
max_time = end_time_messages[0]["time"]
|
||||
# 在最大时间的消息中找最后插入的(_id最大的)
|
||||
end_message = max([msg for msg in end_time_messages if msg["time"] == max_time], key=lambda x: x["_id"])
|
||||
|
||||
if not start_message:
|
||||
logger.warning(f"未找到开始时间 {start_time} 之前的消息")
|
||||
return 0, 0
|
||||
|
||||
# 调试输出
|
||||
# print("\n=== 消息范围信息 ===")
|
||||
# print("Start message:", {
|
||||
# "message_id": start_message.get("message_id"),
|
||||
# "time": start_message.get("time"),
|
||||
# "text": start_message.get("processed_plain_text", ""),
|
||||
# "_id": str(start_message.get("_id"))
|
||||
# })
|
||||
# print("End message:", {
|
||||
# "message_id": end_message.get("message_id"),
|
||||
# "time": end_message.get("time"),
|
||||
# "text": end_message.get("processed_plain_text", ""),
|
||||
# "_id": str(end_message.get("_id"))
|
||||
# })
|
||||
# print("Stream ID:", stream_id)
|
||||
|
||||
# 如果结束消息的时间等于开始时间,返回0
|
||||
if end_message["time"] == start_message["time"]:
|
||||
return 0, 0
|
||||
|
||||
# 获取并打印这个时间范围内的所有消息
|
||||
# print("\n=== 时间范围内的所有消息 ===")
|
||||
all_messages = list(
|
||||
db.messages.find(
|
||||
{"chat_id": stream_id, "time": {"$gte": start_message["time"], "$lte": end_message["time"]}},
|
||||
sort=[("time", 1), ("_id", 1)], # 按时间正序,_id正序
|
||||
)
|
||||
)
|
||||
|
||||
count = 0
|
||||
total_length = 0
|
||||
for msg in all_messages:
|
||||
# 遍历结果计算数量和长度
|
||||
for msg in messages_cursor:
|
||||
count += 1
|
||||
text_length = len(msg.get("processed_plain_text", ""))
|
||||
total_length += text_length
|
||||
# print(f"\n消息 {count}:")
|
||||
# print({
|
||||
# "message_id": msg.get("message_id"),
|
||||
# "time": msg.get("time"),
|
||||
# "text": msg.get("processed_plain_text", ""),
|
||||
# "text_length": text_length,
|
||||
# "_id": str(msg.get("_id"))
|
||||
# })
|
||||
total_length += len(msg.get("processed_plain_text", ""))
|
||||
|
||||
# 如果时间不同,需要把end_message本身也计入
|
||||
return count - 1, total_length
|
||||
# logger.debug(f"查询范围 ({start_time}, {end_time}] 内找到 {count} 条消息,总长度 {total_length}")
|
||||
return count, total_length
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"计算消息数量时出错: {str(e)}")
|
||||
except PyMongoError as e:
|
||||
logger.error(f"查询 stream_id={stream_id} 在 ({start_time}, {end_time}] 范围内的消息时出错: {e}")
|
||||
return 0, 0
|
||||
except Exception as e: # 保留一个通用异常捕获以防万一
|
||||
logger.error(f"计算消息数量时发生意外错误: {e}")
|
||||
return 0, 0
|
||||
|
||||
|
||||
def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> Optional[str]:
|
||||
def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> str:
|
||||
"""将时间戳转换为人类可读的时间格式
|
||||
|
||||
Args:
|
||||
|
|
@ -732,10 +634,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
|
|||
return f"{int(diff / 86400)}天前:\n"
|
||||
else:
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n"
|
||||
elif mode == "lite":
|
||||
else: # mode = "lite" or unknown
|
||||
# 只返回时分秒格式,喵~
|
||||
return time.strftime("%H:%M:%S", time.localtime(timestamp))
|
||||
return None
|
||||
|
||||
|
||||
def parse_text_timestamps(text: str, mode: str = "normal") -> str:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ from ...config.config import global_config
|
|||
from ..models.utils_model import LLMRequest
|
||||
|
||||
from src.common.logger_manager import get_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_logger("chat_image")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
# 创建APIRouter而不是FastAPI实例
|
||||
router = APIRouter()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ from ...config.config import global_config
|
|||
from ..chat.utils_image import image_path_to_base64, image_manager
|
||||
from ..models.utils_model import LLMRequest
|
||||
from src.common.logger_manager import get_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_logger("emoji")
|
||||
|
||||
|
|
@ -24,7 +26,6 @@ EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录
|
|||
EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录
|
||||
MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换的 prompt 中
|
||||
|
||||
|
||||
"""
|
||||
还没经过测试,有些地方数据库和内存数据同步可能不完全
|
||||
|
||||
|
|
@ -52,8 +53,6 @@ class MaiEmoji:
|
|||
|
||||
async def initialize_hash_format(self):
|
||||
"""从文件创建表情包实例, 计算哈希值和格式"""
|
||||
image_base64 = None
|
||||
image_bytes = None
|
||||
try:
|
||||
# 使用 full_path 检查文件是否存在
|
||||
if not os.path.exists(self.full_path):
|
||||
|
|
@ -225,6 +224,140 @@ class MaiEmoji:
|
|||
return False
|
||||
|
||||
|
||||
def _emoji_objects_to_readable_list(emoji_objects):
|
||||
"""将表情包对象列表转换为可读的字符串列表
|
||||
|
||||
参数:
|
||||
emoji_objects: MaiEmoji对象列表
|
||||
|
||||
返回:
|
||||
list[str]: 可读的表情包信息字符串列表
|
||||
"""
|
||||
emoji_info_list = []
|
||||
for i, emoji in enumerate(emoji_objects):
|
||||
# 转换时间戳为可读时间
|
||||
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time))
|
||||
# 构建每个表情包的信息字符串
|
||||
emoji_info = f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n"
|
||||
emoji_info_list.append(emoji_info)
|
||||
return emoji_info_list
|
||||
|
||||
|
||||
def _to_emoji_objects(data):
|
||||
emoji_objects = []
|
||||
load_errors = 0
|
||||
emoji_data_list = list(data)
|
||||
|
||||
for emoji_data in emoji_data_list:
|
||||
full_path = emoji_data.get("full_path")
|
||||
if not full_path:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
|
||||
load_errors += 1
|
||||
continue # 跳过缺少 full_path 的记录
|
||||
|
||||
try:
|
||||
# 使用 full_path 初始化 MaiEmoji 对象
|
||||
emoji = MaiEmoji(full_path=full_path)
|
||||
|
||||
# 设置从数据库加载的属性
|
||||
emoji.hash = emoji_data.get("hash", "")
|
||||
# 如果 hash 为空,也跳过?取决于业务逻辑
|
||||
if not emoji.hash:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", [])
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
# 优先使用 last_used_time,否则用 timestamp,最后用当前时间
|
||||
last_used = emoji_data.get("last_used_time")
|
||||
timestamp = emoji_data.get("timestamp")
|
||||
emoji.last_used_time = (
|
||||
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
|
||||
)
|
||||
emoji.register_time = timestamp if timestamp is not None else time.time()
|
||||
emoji.format = emoji_data.get("format", "") # 加载格式
|
||||
|
||||
# 不需要再手动设置 path 和 filename,__init__ 会自动处理
|
||||
|
||||
emoji_objects.append(emoji)
|
||||
|
||||
except ValueError as ve: # 捕获 __init__ 可能的错误
|
||||
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
|
||||
load_errors += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
|
||||
load_errors += 1
|
||||
return emoji_objects, load_errors
|
||||
return emoji_objects, load_errors
|
||||
|
||||
|
||||
def _ensure_emoji_dir():
|
||||
"""确保表情存储目录存在"""
|
||||
os.makedirs(EMOJI_DIR, exist_ok=True)
|
||||
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
|
||||
|
||||
|
||||
async def clear_temp_emoji():
|
||||
"""清理临时表情包
|
||||
清理/data/emoji和/data/image目录下的所有文件
|
||||
当目录中文件数超过100时,会全部删除
|
||||
"""
|
||||
|
||||
logger.info("[清理] 开始清理缓存...")
|
||||
|
||||
for need_clear in (os.path.join(BASE_DIR, "emoji"), os.path.join(BASE_DIR, "image")):
|
||||
if os.path.exists(need_clear):
|
||||
files = os.listdir(need_clear)
|
||||
# 如果文件数超过50就全部删除
|
||||
if len(files) > 100:
|
||||
for filename in files:
|
||||
file_path = os.path.join(need_clear, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
logger.debug(f"[清理] 删除: {filename}")
|
||||
|
||||
logger.success("[清理] 完成")
|
||||
|
||||
|
||||
async def clean_unused_emojis(emoji_dir, emoji_objects):
|
||||
"""清理指定目录中未被 emoji_objects 追踪的表情包文件"""
|
||||
if not os.path.exists(emoji_dir):
|
||||
logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取内存中所有有效表情包的完整路径集合
|
||||
tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
|
||||
cleaned_count = 0
|
||||
|
||||
# 遍历指定目录中的所有文件
|
||||
for file_name in os.listdir(emoji_dir):
|
||||
file_full_path = os.path.join(emoji_dir, file_name)
|
||||
|
||||
# 确保处理的是文件而不是子目录
|
||||
if not os.path.isfile(file_full_path):
|
||||
continue
|
||||
|
||||
# 如果文件不在被追踪的集合中,则删除
|
||||
if file_full_path not in tracked_full_paths:
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
|
||||
cleaned_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
|
||||
|
||||
if cleaned_count > 0:
|
||||
logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
|
||||
else:
|
||||
logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
|
||||
|
||||
|
||||
class EmojiManager:
|
||||
_instance = None
|
||||
|
||||
|
|
@ -235,6 +368,7 @@ class EmojiManager:
|
|||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self._initialized = None
|
||||
self._scan_task = None
|
||||
self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
|
||||
self.llm_emotion_judge = LLMRequest(
|
||||
|
|
@ -248,23 +382,18 @@ class EmojiManager:
|
|||
|
||||
logger.info("启动表情包管理器")
|
||||
|
||||
def _ensure_emoji_dir(self):
|
||||
"""确保表情存储目录存在"""
|
||||
os.makedirs(EMOJI_DIR, exist_ok=True)
|
||||
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
|
||||
|
||||
def initialize(self):
|
||||
"""初始化数据库连接和表情目录"""
|
||||
if not self._initialized:
|
||||
try:
|
||||
self._ensure_emoji_collection()
|
||||
self._ensure_emoji_dir()
|
||||
_ensure_emoji_dir()
|
||||
self._initialized = True
|
||||
# 更新表情包数量
|
||||
# 启动时执行一次完整性检查
|
||||
# await self.check_emoji_file_integrity()
|
||||
except Exception:
|
||||
logger.exception("初始化表情管理器失败")
|
||||
except Exception as e:
|
||||
logger.exception(f"初始化表情管理器失败: {e}")
|
||||
|
||||
def _ensure_db(self):
|
||||
"""确保数据库已初始化"""
|
||||
|
|
@ -291,12 +420,12 @@ class EmojiManager:
|
|||
db.emoji.create_index([("embedding", "2dsphere")])
|
||||
db.emoji.create_index([("filename", 1)], unique=True)
|
||||
|
||||
def record_usage(self, hash: str):
|
||||
def record_usage(self, emoji_hash: str):
|
||||
"""记录表情使用次数"""
|
||||
try:
|
||||
db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}})
|
||||
db.emoji.update_one({"hash": emoji_hash}, {"$inc": {"usage_count": 1}})
|
||||
for emoji in self.emoji_objects:
|
||||
if emoji.hash == hash:
|
||||
if emoji.hash == emoji_hash:
|
||||
emoji.usage_count += 1
|
||||
break
|
||||
|
||||
|
|
@ -458,7 +587,7 @@ class EmojiManager:
|
|||
self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove]
|
||||
|
||||
# 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件
|
||||
await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
|
||||
await clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
|
||||
|
||||
# 输出清理结果
|
||||
if removed_count > 0:
|
||||
|
|
@ -477,7 +606,7 @@ class EmojiManager:
|
|||
while True:
|
||||
logger.info("[扫描] 开始检查表情包完整性...")
|
||||
await self.check_emoji_file_integrity()
|
||||
await self.clear_temp_emoji()
|
||||
await clear_temp_emoji()
|
||||
logger.info("[扫描] 开始扫描新表情包...")
|
||||
|
||||
# 检查表情包目录是否存在
|
||||
|
|
@ -531,51 +660,7 @@ class EmojiManager:
|
|||
self._ensure_db()
|
||||
logger.info("[数据库] 开始加载所有表情包记录...")
|
||||
|
||||
all_emoji_data = list(db.emoji.find())
|
||||
emoji_objects = []
|
||||
load_errors = 0
|
||||
|
||||
for emoji_data in all_emoji_data:
|
||||
full_path = emoji_data.get("full_path")
|
||||
if not full_path:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
|
||||
load_errors += 1
|
||||
continue # 跳过缺少 full_path 的记录
|
||||
|
||||
try:
|
||||
# 使用 full_path 初始化 MaiEmoji 对象
|
||||
emoji = MaiEmoji(full_path=full_path)
|
||||
|
||||
# 设置从数据库加载的属性
|
||||
emoji.hash = emoji_data.get("hash", "")
|
||||
# 如果 hash 为空,也跳过?取决于业务逻辑
|
||||
if not emoji.hash:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", [])
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
# 优先使用 last_used_time,否则用 timestamp,最后用当前时间
|
||||
last_used = emoji_data.get("last_used_time")
|
||||
timestamp = emoji_data.get("timestamp")
|
||||
emoji.last_used_time = (
|
||||
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
|
||||
)
|
||||
emoji.register_time = timestamp if timestamp is not None else time.time()
|
||||
emoji.format = emoji_data.get("format", "") # 加载格式
|
||||
|
||||
# 不需要再手动设置 path 和 filename,__init__ 会自动处理
|
||||
|
||||
emoji_objects.append(emoji)
|
||||
|
||||
except ValueError as ve: # 捕获 __init__ 可能的错误
|
||||
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
|
||||
load_errors += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
|
||||
load_errors += 1
|
||||
emoji_objects, load_errors = _to_emoji_objects(db.emoji.find())
|
||||
|
||||
# 更新内存中的列表和数量
|
||||
self.emoji_objects = emoji_objects
|
||||
|
|
@ -590,11 +675,11 @@ class EmojiManager:
|
|||
self.emoji_objects = [] # 加载失败则清空列表
|
||||
self.emoji_num = 0
|
||||
|
||||
async def get_emoji_from_db(self, hash=None):
|
||||
async def get_emoji_from_db(self, emoji_hash=None):
|
||||
"""获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找)
|
||||
|
||||
参数:
|
||||
hash: 可选,如果提供则只返回指定哈希值的表情包
|
||||
emoji_hash: 可选,如果提供则只返回指定哈希值的表情包
|
||||
|
||||
返回:
|
||||
list[MaiEmoji]: 表情包对象列表
|
||||
|
|
@ -603,49 +688,14 @@ class EmojiManager:
|
|||
self._ensure_db()
|
||||
|
||||
query = {}
|
||||
if hash:
|
||||
query = {"hash": hash}
|
||||
if emoji_hash:
|
||||
query = {"hash": emoji_hash}
|
||||
else:
|
||||
logger.warning(
|
||||
"[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。"
|
||||
)
|
||||
|
||||
emoji_data_list = list(db.emoji.find(query))
|
||||
emoji_objects = []
|
||||
load_errors = 0
|
||||
|
||||
for emoji_data in emoji_data_list:
|
||||
full_path = emoji_data.get("full_path")
|
||||
if not full_path:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
emoji = MaiEmoji(full_path=full_path)
|
||||
emoji.hash = emoji_data.get("hash", "")
|
||||
if not emoji.hash:
|
||||
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
|
||||
load_errors += 1
|
||||
continue
|
||||
|
||||
emoji.description = emoji_data.get("description", "")
|
||||
emoji.emotion = emoji_data.get("emotion", [])
|
||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||
last_used = emoji_data.get("last_used_time")
|
||||
timestamp = emoji_data.get("timestamp")
|
||||
emoji.last_used_time = (
|
||||
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
|
||||
)
|
||||
emoji.register_time = timestamp if timestamp is not None else time.time()
|
||||
emoji.format = emoji_data.get("format", "")
|
||||
emoji_objects.append(emoji)
|
||||
except ValueError as ve:
|
||||
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
|
||||
load_errors += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
|
||||
load_errors += 1
|
||||
emoji_objects, load_errors = _to_emoji_objects(db.emoji.find(query))
|
||||
|
||||
if load_errors > 0:
|
||||
logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。")
|
||||
|
|
@ -656,17 +706,17 @@ class EmojiManager:
|
|||
logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}")
|
||||
return []
|
||||
|
||||
async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]:
|
||||
async def get_emoji_from_manager(self, emoji_hash) -> Optional[MaiEmoji]:
|
||||
"""从内存中的 emoji_objects 列表获取表情包
|
||||
|
||||
参数:
|
||||
hash: 要查找的表情包哈希值
|
||||
emoji_hash: 要查找的表情包哈希值
|
||||
返回:
|
||||
MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None
|
||||
"""
|
||||
for emoji in self.emoji_objects:
|
||||
# 确保对象未被标记为删除且哈希值匹配
|
||||
if not emoji.is_deleted and emoji.hash == hash:
|
||||
if not emoji.is_deleted and emoji.hash == emoji_hash:
|
||||
return emoji
|
||||
return None # 如果循环结束还没找到,则返回 None
|
||||
|
||||
|
|
@ -709,26 +759,6 @@ class EmojiManager:
|
|||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def _emoji_objects_to_readable_list(self, emoji_objects):
|
||||
"""将表情包对象列表转换为可读的字符串列表
|
||||
|
||||
参数:
|
||||
emoji_objects: MaiEmoji对象列表
|
||||
|
||||
返回:
|
||||
list[str]: 可读的表情包信息字符串列表
|
||||
"""
|
||||
emoji_info_list = []
|
||||
for i, emoji in enumerate(emoji_objects):
|
||||
# 转换时间戳为可读时间
|
||||
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time))
|
||||
# 构建每个表情包的信息字符串
|
||||
emoji_info = (
|
||||
f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n"
|
||||
)
|
||||
emoji_info_list.append(emoji_info)
|
||||
return emoji_info_list
|
||||
|
||||
async def replace_a_emoji(self, new_emoji: MaiEmoji):
|
||||
"""替换一个表情包
|
||||
|
||||
|
|
@ -755,7 +785,7 @@ class EmojiManager:
|
|||
)
|
||||
|
||||
# 将表情包信息转换为可读的字符串
|
||||
emoji_info_list = self._emoji_objects_to_readable_list(selected_emojis)
|
||||
emoji_info_list = _emoji_objects_to_readable_list(selected_emojis)
|
||||
|
||||
# 构建提示词
|
||||
prompt = (
|
||||
|
|
@ -853,7 +883,7 @@ class EmojiManager:
|
|||
'''
|
||||
content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
|
||||
if content == "否":
|
||||
return None, []
|
||||
return "", []
|
||||
|
||||
# 分析情感含义
|
||||
emotion_prompt = f"""
|
||||
|
|
@ -989,76 +1019,6 @@ class EmojiManager:
|
|||
logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}")
|
||||
return False
|
||||
|
||||
async def clear_temp_emoji(self):
|
||||
"""清理临时表情包
|
||||
清理/data/emoji和/data/image目录下的所有文件
|
||||
当目录中文件数超过100时,会全部删除
|
||||
"""
|
||||
|
||||
logger.info("[清理] 开始清理缓存...")
|
||||
|
||||
# 清理emoji目录
|
||||
emoji_dir = os.path.join(BASE_DIR, "emoji")
|
||||
if os.path.exists(emoji_dir):
|
||||
files = os.listdir(emoji_dir)
|
||||
# 如果文件数超过50就全部删除
|
||||
if len(files) > 100:
|
||||
for filename in files:
|
||||
file_path = os.path.join(emoji_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
logger.debug(f"[清理] 删除: {filename}")
|
||||
|
||||
# 清理image目录
|
||||
image_dir = os.path.join(BASE_DIR, "image")
|
||||
if os.path.exists(image_dir):
|
||||
files = os.listdir(image_dir)
|
||||
# 如果文件数超过50就全部删除
|
||||
if len(files) > 100:
|
||||
for filename in files:
|
||||
file_path = os.path.join(image_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
logger.debug(f"[清理] 删除图片: {filename}")
|
||||
|
||||
logger.success("[清理] 完成")
|
||||
|
||||
async def clean_unused_emojis(self, emoji_dir, emoji_objects):
|
||||
"""清理指定目录中未被 emoji_objects 追踪的表情包文件"""
|
||||
if not os.path.exists(emoji_dir):
|
||||
logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
|
||||
return
|
||||
|
||||
try:
|
||||
# 获取内存中所有有效表情包的完整路径集合
|
||||
tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
|
||||
cleaned_count = 0
|
||||
|
||||
# 遍历指定目录中的所有文件
|
||||
for file_name in os.listdir(emoji_dir):
|
||||
file_full_path = os.path.join(emoji_dir, file_name)
|
||||
|
||||
# 确保处理的是文件而不是子目录
|
||||
if not os.path.isfile(file_full_path):
|
||||
continue
|
||||
|
||||
# 如果文件不在被追踪的集合中,则删除
|
||||
if file_full_path not in tracked_full_paths:
|
||||
try:
|
||||
os.remove(file_full_path)
|
||||
logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
|
||||
cleaned_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
|
||||
|
||||
if cleaned_count > 0:
|
||||
logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
|
||||
else:
|
||||
logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
|
||||
|
||||
|
||||
# 创建全局单例
|
||||
emoji_manager = EmojiManager()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ from .heartFC_sender import HeartFCSender
|
|||
from src.plugins.chat.utils import process_llm_response
|
||||
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
|
||||
from src.plugins.moods.moods import MoodManager
|
||||
from src.individuality.individuality import Individuality
|
||||
from src.heart_flow.utils_chat import get_chat_type_and_target_info
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
|
||||
|
|
@ -144,6 +147,25 @@ class SenderError(HeartFCError):
|
|||
pass
|
||||
|
||||
|
||||
async def _handle_cycle_delay(action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
|
||||
"""处理循环延迟"""
|
||||
cycle_duration = time.monotonic() - cycle_start_time
|
||||
|
||||
try:
|
||||
sleep_duration = 0.0
|
||||
if not action_taken_this_cycle and cycle_duration < 1:
|
||||
sleep_duration = 1 - cycle_duration
|
||||
elif cycle_duration < 0.2:
|
||||
sleep_duration = 0.2
|
||||
|
||||
if sleep_duration > 0:
|
||||
await asyncio.sleep(sleep_duration)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
|
||||
raise
|
||||
|
||||
|
||||
class HeartFChatting:
|
||||
"""
|
||||
管理一个连续的Plan-Replier-Sender循环
|
||||
|
|
@ -155,7 +177,7 @@ class HeartFChatting:
|
|||
self,
|
||||
chat_id: str,
|
||||
sub_mind: SubMind,
|
||||
observations: Observation,
|
||||
observations: list[Observation],
|
||||
on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]],
|
||||
):
|
||||
"""
|
||||
|
|
@ -175,7 +197,12 @@ class HeartFChatting:
|
|||
self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback
|
||||
|
||||
# 日志前缀
|
||||
self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]"
|
||||
self.log_prefix: str = str(chat_id) # Initial default, will be updated
|
||||
|
||||
# --- Initialize attributes (defaults) ---
|
||||
self.is_group_chat: bool = False
|
||||
self.chat_target_info: Optional[dict] = None
|
||||
# --- End Initialization ---
|
||||
|
||||
# 动作管理器
|
||||
self.action_manager = ActionManager()
|
||||
|
|
@ -215,22 +242,35 @@ class HeartFChatting:
|
|||
|
||||
async def _initialize(self) -> bool:
|
||||
"""
|
||||
懒初始化以使用提供的标识符解析chat_stream。
|
||||
确保实例已准备好处理触发器。
|
||||
懒初始化,解析chat_stream, 获取聊天类型和目标信息。
|
||||
"""
|
||||
if self._initialized:
|
||||
return True
|
||||
|
||||
self.chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
if not self.chat_stream:
|
||||
logger.error(f"{self.log_prefix} 获取ChatStream失败。")
|
||||
# --- Use utility function to determine chat type and fetch info ---
|
||||
# Note: get_chat_type_and_target_info handles getting the chat_stream internally
|
||||
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id)
|
||||
|
||||
# Update log prefix based on potential stream name (if needed, or get it from chat_stream if util doesn't return it)
|
||||
# Assuming get_chat_type_and_target_info focuses only on type/target
|
||||
# We still need the chat_stream object itself for other operations
|
||||
try:
|
||||
self.chat_stream = await asyncio.to_thread(chat_manager.get_stream, self.stream_id)
|
||||
if not self.chat_stream:
|
||||
logger.error(
|
||||
f"[HFC:{self.stream_id}] 获取ChatStream失败 during _initialize, though util func might have succeeded earlier."
|
||||
)
|
||||
return False # Cannot proceed without chat_stream object
|
||||
# Update log prefix using the fetched stream object
|
||||
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
|
||||
except Exception as e:
|
||||
logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}")
|
||||
return False
|
||||
|
||||
# 更新日志前缀(以防流名称发生变化)
|
||||
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
|
||||
# --- End using utility function ---
|
||||
|
||||
self._initialized = True
|
||||
logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ")
|
||||
logger.debug(f"{self.log_prefix} 麦麦感觉到了,可以开始认真水群 ")
|
||||
return True
|
||||
|
||||
async def start(self):
|
||||
|
|
@ -327,7 +367,7 @@ class HeartFChatting:
|
|||
self._current_cycle.timers = cycle_timers
|
||||
|
||||
# 防止循环过快消耗资源
|
||||
await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix)
|
||||
await _handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix)
|
||||
|
||||
# 完成当前循环并保存历史
|
||||
self._current_cycle.complete_cycle()
|
||||
|
|
@ -612,19 +652,18 @@ class HeartFChatting:
|
|||
observation = self.observations[0] if self.observations else None
|
||||
|
||||
try:
|
||||
dang_qian_deng_dai = 0.0 # 初始化本次等待时间
|
||||
with Timer("等待新消息", cycle_timers):
|
||||
# 等待新消息、超时或关闭信号,并获取结果
|
||||
await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix)
|
||||
# 从计时器获取实际等待时间
|
||||
dang_qian_deng_dai = cycle_timers.get("等待新消息", 0.0)
|
||||
current_waiting = cycle_timers.get("等待新消息", 0.0)
|
||||
|
||||
if not self._shutting_down:
|
||||
self._lian_xu_bu_hui_fu_ci_shu += 1
|
||||
self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间
|
||||
self._lian_xu_deng_dai_shi_jian += current_waiting # 累加等待时间
|
||||
logger.debug(
|
||||
f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
|
||||
f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒"
|
||||
f"本次等待: {current_waiting:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒"
|
||||
)
|
||||
|
||||
# 检查是否同时达到次数和时间阈值
|
||||
|
|
@ -715,24 +754,6 @@ class HeartFChatting:
|
|||
if not self._shutting_down:
|
||||
logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
|
||||
|
||||
async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
|
||||
"""处理循环延迟"""
|
||||
cycle_duration = time.monotonic() - cycle_start_time
|
||||
|
||||
try:
|
||||
sleep_duration = 0.0
|
||||
if not action_taken_this_cycle and cycle_duration < 1:
|
||||
sleep_duration = 1 - cycle_duration
|
||||
elif cycle_duration < 0.2:
|
||||
sleep_duration = 0.2
|
||||
|
||||
if sleep_duration > 0:
|
||||
await asyncio.sleep(sleep_duration)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
|
||||
raise
|
||||
|
||||
async def _get_submind_thinking(self, cycle_timers: dict) -> str:
|
||||
"""
|
||||
获取子思维的思考结果
|
||||
|
|
@ -833,18 +854,15 @@ class HeartFChatting:
|
|||
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
|
||||
)
|
||||
|
||||
# --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
|
||||
# replan_prompt_str = "" # 暂时简化
|
||||
# if is_re_planned:
|
||||
# replan_prompt_str = await self._build_replan_prompt(
|
||||
# self._current_cycle.action_type, self._current_cycle.reasoning
|
||||
# )
|
||||
prompt = await self._build_planner_prompt(
|
||||
observed_messages_str,
|
||||
current_mind,
|
||||
self.sub_mind.structured_info,
|
||||
"", # replan_prompt_str,
|
||||
current_available_actions, # <--- 传入当前可用动作
|
||||
# --- 构建提示词 (调用修改后的 PromptBuilder 方法) ---
|
||||
prompt = await prompt_builder.build_planner_prompt(
|
||||
is_group_chat=self.is_group_chat, # <-- Pass HFC state
|
||||
chat_target_info=self.chat_target_info, # <-- Pass HFC state
|
||||
cycle_history=self._cycle_history, # <-- Pass HFC state
|
||||
observed_messages_str=observed_messages_str, # <-- Pass local variable
|
||||
current_mind=current_mind, # <-- Pass argument
|
||||
structured_info=self.sub_mind.structured_info_str, # <-- Pass SubMind info
|
||||
current_available_actions=current_available_actions, # <-- Pass determined actions
|
||||
)
|
||||
|
||||
# --- 调用 LLM (普通文本生成) ---
|
||||
|
|
@ -1108,217 +1126,6 @@ class HeartFChatting:
|
|||
|
||||
return prompt
|
||||
|
||||
async def _build_planner_prompt(
|
||||
self,
|
||||
observed_messages_str: str,
|
||||
current_mind: Optional[str],
|
||||
structured_info: Dict[str, Any],
|
||||
replan_prompt: str,
|
||||
current_available_actions: Dict[str, str],
|
||||
) -> str:
|
||||
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
|
||||
try:
|
||||
# 准备结构化信息块
|
||||
structured_info_block = ""
|
||||
if structured_info:
|
||||
structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n"
|
||||
|
||||
# 准备聊天内容块
|
||||
chat_content_block = ""
|
||||
if observed_messages_str:
|
||||
chat_content_block = "观察到的最新聊天内容如下:\n---\n"
|
||||
chat_content_block += observed_messages_str
|
||||
chat_content_block += "\n---"
|
||||
else:
|
||||
chat_content_block = "当前没有观察到新的聊天内容。\n"
|
||||
|
||||
# 准备当前思维块 (修改以匹配模板)
|
||||
current_mind_block = ""
|
||||
if current_mind:
|
||||
# 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
|
||||
current_mind_block = f"你的内心想法:\n{current_mind}"
|
||||
else:
|
||||
current_mind_block = "你的内心想法:\n[没有特别的想法]"
|
||||
|
||||
# 准备循环信息块 (分析最近的活动循环)
|
||||
recent_active_cycles = []
|
||||
for cycle in reversed(self._cycle_history):
|
||||
# 只关心实际执行了动作的循环
|
||||
if cycle.action_taken:
|
||||
recent_active_cycles.append(cycle)
|
||||
# 最多找最近的3个活动循环
|
||||
if len(recent_active_cycles) == 3:
|
||||
break
|
||||
|
||||
cycle_info_block = ""
|
||||
consecutive_text_replies = 0
|
||||
responses_for_prompt = []
|
||||
|
||||
# 检查这最近的活动循环中有多少是连续的文本回复 (从最近的开始看)
|
||||
for cycle in recent_active_cycles:
|
||||
if cycle.action_type == "text_reply":
|
||||
consecutive_text_replies += 1
|
||||
# 获取回复内容,如果不存在则返回'[空回复]'
|
||||
response_text = cycle.response_info.get("response_text", [])
|
||||
# 使用简单的 join 来格式化回复内容列表
|
||||
formatted_response = "[空回复]" if not response_text else " ".join(response_text)
|
||||
responses_for_prompt.append(formatted_response)
|
||||
else:
|
||||
# 一旦遇到非文本回复,连续性中断
|
||||
break
|
||||
|
||||
# 根据连续文本回复的数量构建提示信息
|
||||
# 注意: responses_for_prompt 列表是从最近到最远排序的
|
||||
if consecutive_text_replies >= 3: # 如果最近的三个活动都是文本回复
|
||||
cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意'
|
||||
elif consecutive_text_replies == 2: # 如果最近的两个活动是文本回复
|
||||
cycle_info_block = f'你已经连续回复了两条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}"),请注意'
|
||||
elif consecutive_text_replies == 1: # 如果最近的一个活动是文本回复
|
||||
cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")'
|
||||
|
||||
# 包装提示块,增加可读性,即使没有连续回复也给个标记
|
||||
if cycle_info_block:
|
||||
# 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
|
||||
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
|
||||
else:
|
||||
# 如果最近的活动循环不是文本回复,或者没有活动循环
|
||||
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
|
||||
|
||||
individuality = Individuality.get_instance()
|
||||
# 模板中占位符是 {prompt_personality}
|
||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||
|
||||
# --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
|
||||
action_options_text = "当前你可以选择的行动有:\n"
|
||||
action_keys = list(current_available_actions.keys())
|
||||
for name in action_keys:
|
||||
desc = current_available_actions[name]
|
||||
action_options_text += f"- '{name}': {desc}\n"
|
||||
|
||||
# --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
|
||||
example_action_key = action_keys[0] if action_keys else "no_reply"
|
||||
|
||||
# --- 获取提示词模板 ---
|
||||
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
|
||||
|
||||
# --- 填充模板 ---
|
||||
prompt = planner_prompt_template.format(
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
structured_info_block=structured_info_block,
|
||||
chat_content_block=chat_content_block,
|
||||
current_mind_block=current_mind_block,
|
||||
replan="", # 暂时留空 replan 信息
|
||||
cycle_info_block=cycle_info_block,
|
||||
action_options_text=action_options_text, # 传入可用动作描述
|
||||
example_action=example_action_key, # 传入示例动作键
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
|
||||
|
||||
# --- 回复器 (Replier) 的定义 --- #
|
||||
async def _replier_work(
|
||||
self,
|
||||
reason: str,
|
||||
anchor_message: MessageRecv,
|
||||
thinking_id: str,
|
||||
) -> Optional[List[str]]:
|
||||
"""
|
||||
回复器 (Replier): 核心逻辑,负责生成回复文本。
|
||||
(已整合原 HeartFCGenerator 的功能)
|
||||
"""
|
||||
try:
|
||||
# 1. 获取情绪影响因子并调整模型温度
|
||||
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
|
||||
current_temp = global_config.llm_normal["temp"] * arousal_multiplier
|
||||
self.model_normal.temperature = current_temp # 动态调整温度
|
||||
|
||||
# 2. 获取信息捕捉器
|
||||
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
|
||||
|
||||
# 3. 构建 Prompt
|
||||
with Timer("构建Prompt", {}): # 内部计时器,可选保留
|
||||
prompt = await prompt_builder.build_prompt(
|
||||
build_mode="focus",
|
||||
reason=reason,
|
||||
current_mind_info=self.sub_mind.current_mind,
|
||||
structured_info=self.sub_mind.structured_info,
|
||||
message_txt="", # 似乎是固定的空字符串
|
||||
sender_name="", # 似乎是固定的空字符串
|
||||
chat_stream=anchor_message.chat_stream,
|
||||
)
|
||||
|
||||
# 4. 调用 LLM 生成回复
|
||||
content = None
|
||||
reasoning_content = None
|
||||
model_name = "unknown_model"
|
||||
try:
|
||||
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
||||
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
|
||||
# logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
|
||||
# 捕捉 LLM 输出信息
|
||||
info_catcher.catch_after_llm_generated(
|
||||
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
|
||||
)
|
||||
|
||||
except Exception as llm_e:
|
||||
# 精简报错信息
|
||||
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}")
|
||||
return None # LLM 调用失败则无法生成回复
|
||||
|
||||
# 5. 处理 LLM 响应
|
||||
if not content:
|
||||
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。")
|
||||
return None
|
||||
|
||||
with Timer("处理响应", {}): # 内部计时器,可选保留
|
||||
processed_response = process_llm_response(content)
|
||||
|
||||
if not processed_response:
|
||||
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。")
|
||||
return None
|
||||
|
||||
return processed_response
|
||||
|
||||
except Exception as e:
|
||||
# 更通用的错误处理,精简信息
|
||||
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}")
|
||||
# logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈
|
||||
return None
|
||||
|
||||
# --- Methods moved from HeartFCController start ---
|
||||
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]:
|
||||
"""创建思考消息 (尝试锚定到 anchor_message)"""
|
||||
if not anchor_message or not anchor_message.chat_stream:
|
||||
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
|
||||
return None
|
||||
|
||||
chat = anchor_message.chat_stream
|
||||
messageinfo = anchor_message.message_info
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.BOT_QQ,
|
||||
user_nickname=global_config.BOT_NICKNAME,
|
||||
platform=messageinfo.platform,
|
||||
)
|
||||
|
||||
thinking_time_point = round(time.time(), 2)
|
||||
thinking_id = "mt" + str(thinking_time_point)
|
||||
thinking_message = MessageThinking(
|
||||
message_id=thinking_id,
|
||||
chat_stream=chat,
|
||||
bot_user_info=bot_user_info,
|
||||
reply=anchor_message, # 回复的是锚点消息
|
||||
thinking_start_time=thinking_time_point,
|
||||
)
|
||||
# Access MessageManager directly
|
||||
await self.heart_fc_sender.register_thinking(thinking_message)
|
||||
return thinking_id
|
||||
|
||||
async def _send_response_messages(
|
||||
self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str
|
||||
) -> Optional[MessageSending]:
|
||||
|
|
@ -1371,9 +1178,9 @@ class HeartFChatting:
|
|||
if not mark_head:
|
||||
mark_head = True
|
||||
first_bot_msg = bot_message # 保存第一个成功发送的消息对象
|
||||
await self.heart_fc_sender.type_and_send_message(bot_message, type=False)
|
||||
await self.heart_fc_sender.type_and_send_message(bot_message, typing=False)
|
||||
else:
|
||||
await self.heart_fc_sender.type_and_send_message(bot_message, type=True)
|
||||
await self.heart_fc_sender.type_and_send_message(bot_message, typing=True)
|
||||
|
||||
reply_message_ids.append(part_message_id) # 记录我们生成的ID
|
||||
|
||||
|
|
@ -1454,3 +1261,118 @@ class HeartFChatting:
|
|||
if self._cycle_history:
|
||||
return self._cycle_history[-1].to_dict()
|
||||
return None
|
||||
|
||||
# --- 回复器 (Replier) 的定义 --- #
|
||||
async def _replier_work(
|
||||
self,
|
||||
reason: str,
|
||||
anchor_message: MessageRecv,
|
||||
thinking_id: str,
|
||||
) -> Optional[List[str]]:
|
||||
"""
|
||||
回复器 (Replier): 核心逻辑,负责生成回复文本。
|
||||
(已整合原 HeartFCGenerator 的功能)
|
||||
"""
|
||||
try:
|
||||
# 1. 获取情绪影响因子并调整模型温度
|
||||
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
|
||||
current_temp = global_config.llm_normal["temp"] * arousal_multiplier
|
||||
self.model_normal.temperature = current_temp # 动态调整温度
|
||||
|
||||
# 2. 获取信息捕捉器
|
||||
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
|
||||
|
||||
# --- Determine sender_name for private chat ---
|
||||
sender_name_for_prompt = "某人" # Default for group or if info unavailable
|
||||
if not self.is_group_chat and self.chat_target_info:
|
||||
# Prioritize person_name, then nickname
|
||||
sender_name_for_prompt = (
|
||||
self.chat_target_info.get("person_name")
|
||||
or self.chat_target_info.get("user_nickname")
|
||||
or sender_name_for_prompt
|
||||
)
|
||||
# --- End determining sender_name ---
|
||||
|
||||
# 3. 构建 Prompt
|
||||
with Timer("构建Prompt", {}): # 内部计时器,可选保留
|
||||
prompt = await prompt_builder.build_prompt(
|
||||
build_mode="focus",
|
||||
chat_stream=self.chat_stream, # Pass the stream object
|
||||
# Focus specific args:
|
||||
reason=reason,
|
||||
current_mind_info=self.sub_mind.current_mind,
|
||||
structured_info=self.sub_mind.structured_info_str,
|
||||
sender_name=sender_name_for_prompt, # Pass determined name
|
||||
# Normal specific args (not used in focus mode):
|
||||
# message_txt="",
|
||||
)
|
||||
|
||||
# 4. 调用 LLM 生成回复
|
||||
content = None
|
||||
reasoning_content = None
|
||||
model_name = "unknown_model"
|
||||
if not prompt:
|
||||
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。")
|
||||
return None
|
||||
|
||||
try:
|
||||
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
||||
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
|
||||
# logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\nPrompt:\n{prompt}\n生成回复: {content}\n")
|
||||
# 捕捉 LLM 输出信息
|
||||
info_catcher.catch_after_llm_generated(
|
||||
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
|
||||
)
|
||||
|
||||
except Exception as llm_e:
|
||||
# 精简报错信息
|
||||
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}")
|
||||
return None # LLM 调用失败则无法生成回复
|
||||
|
||||
# 5. 处理 LLM 响应
|
||||
if not content:
|
||||
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。")
|
||||
return None
|
||||
|
||||
with Timer("处理响应", {}): # 内部计时器,可选保留
|
||||
processed_response = process_llm_response(content)
|
||||
|
||||
if not processed_response:
|
||||
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。")
|
||||
return None
|
||||
|
||||
return processed_response
|
||||
|
||||
except Exception as e:
|
||||
# 更通用的错误处理,精简信息
|
||||
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}")
|
||||
# logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈
|
||||
return None
|
||||
|
||||
# --- Methods moved from HeartFCController start ---
|
||||
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]:
|
||||
"""创建思考消息 (尝试锚定到 anchor_message)"""
|
||||
if not anchor_message or not anchor_message.chat_stream:
|
||||
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
|
||||
return None
|
||||
|
||||
chat = anchor_message.chat_stream
|
||||
messageinfo = anchor_message.message_info
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.BOT_QQ,
|
||||
user_nickname=global_config.BOT_NICKNAME,
|
||||
platform=messageinfo.platform,
|
||||
)
|
||||
|
||||
thinking_time_point = round(time.time(), 2)
|
||||
thinking_id = "mt" + str(thinking_time_point)
|
||||
thinking_message = MessageThinking(
|
||||
message_id=thinking_id,
|
||||
chat_stream=chat,
|
||||
bot_user_info=bot_user_info,
|
||||
reply=anchor_message, # 回复的是锚点消息
|
||||
thinking_start_time=thinking_time_point,
|
||||
)
|
||||
# Access MessageManager directly (using heart_fc_sender)
|
||||
await self.heart_fc_sender.register_thinking(thinking_message)
|
||||
return thinking_id
|
||||
|
|
|
|||
|
|
@ -1,17 +1,38 @@
|
|||
# src/plugins/heartFC_chat/heartFC_sender.py
|
||||
import asyncio # 重新导入 asyncio
|
||||
from typing import Dict, Optional # 重新导入类型
|
||||
from ..message.api import global_api
|
||||
from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking
|
||||
|
||||
# from ..message import global_api
|
||||
from src.plugins.message.api import global_api
|
||||
from ..storage.storage import MessageStorage
|
||||
from ..chat.utils import truncate_message
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.plugins.chat.utils import calculate_typing_time
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
logger = get_logger("sender")
|
||||
|
||||
|
||||
async def send_message(message: MessageSending) -> None:
|
||||
"""合并后的消息发送函数,包含WS发送和日志记录"""
|
||||
message_preview = truncate_message(message.processed_plain_text)
|
||||
|
||||
try:
|
||||
# 直接调用API发送消息
|
||||
await global_api.send_message(message)
|
||||
logger.success(f"发送消息 '{message_preview}' 成功")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
||||
if not message.message_info.platform:
|
||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
raise e # 重新抛出其他异常
|
||||
|
||||
|
||||
class HeartFCSender:
|
||||
"""管理消息的注册、即时处理、发送和存储,并跟踪思考状态。"""
|
||||
|
||||
|
|
@ -21,21 +42,6 @@ class HeartFCSender:
|
|||
self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {}
|
||||
self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁
|
||||
|
||||
async def send_message(self, message: MessageSending) -> None:
|
||||
"""合并后的消息发送函数,包含WS发送和日志记录"""
|
||||
message_preview = truncate_message(message.processed_plain_text)
|
||||
|
||||
try:
|
||||
# 直接调用API发送消息
|
||||
await global_api.send_message(message)
|
||||
logger.success(f"发送消息 '{message_preview}' 成功")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
||||
if not message.message_info.platform:
|
||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
raise e # 重新抛出其他异常
|
||||
|
||||
async def register_thinking(self, thinking_message: MessageThinking):
|
||||
"""注册一个思考中的消息。"""
|
||||
if not thinking_message.chat_stream or not thinking_message.message_info.message_id:
|
||||
|
|
@ -73,7 +79,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 type_and_send_message(self, message: MessageSending, type=False):
|
||||
async def type_and_send_message(self, message: MessageSending, typing=False):
|
||||
"""
|
||||
立即处理、发送并存储单个 MessageSending 消息。
|
||||
调用此方法前,应先调用 register_thinking 注册对应的思考消息。
|
||||
|
|
@ -100,7 +106,7 @@ class HeartFCSender:
|
|||
|
||||
await message.process()
|
||||
|
||||
if type:
|
||||
if typing:
|
||||
typing_time = calculate_typing_time(
|
||||
input_string=message.processed_plain_text,
|
||||
thinking_start_time=message.thinking_start_time,
|
||||
|
|
@ -108,7 +114,7 @@ class HeartFCSender:
|
|||
)
|
||||
await asyncio.sleep(typing_time)
|
||||
|
||||
await self.send_message(message)
|
||||
await send_message(message)
|
||||
await self.storage.store_message(message, message.chat_stream)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -136,7 +142,7 @@ class HeartFCSender:
|
|||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
await self.send_message(message) # 使用现有的发送方法
|
||||
await send_message(message) # 使用现有的发送方法
|
||||
await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -12,11 +12,134 @@ from ..chat.chat_stream import chat_manager
|
|||
from ..chat.message_buffer import message_buffer
|
||||
from ..utils.timer_calculator import Timer
|
||||
from src.plugins.person_info.relationship_manager import relationship_manager
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Tuple, Dict, Any
|
||||
|
||||
logger = get_logger("chat")
|
||||
|
||||
|
||||
async def _handle_error(error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
|
||||
"""统一的错误处理函数
|
||||
|
||||
Args:
|
||||
error: 捕获到的异常
|
||||
context: 错误发生的上下文描述
|
||||
message: 可选的消息对象,用于记录相关消息内容
|
||||
"""
|
||||
logger.error(f"{context}: {error}")
|
||||
logger.error(traceback.format_exc())
|
||||
if message and hasattr(message, "raw_message"):
|
||||
logger.error(f"相关消息原始内容: {message.raw_message}")
|
||||
|
||||
|
||||
async def _process_relationship(message: MessageRecv) -> None:
|
||||
"""处理用户关系逻辑
|
||||
|
||||
Args:
|
||||
message: 消息对象,包含用户信息
|
||||
"""
|
||||
platform = message.message_info.platform
|
||||
user_id = message.message_info.user_info.user_id
|
||||
nickname = message.message_info.user_info.user_nickname
|
||||
cardname = message.message_info.user_info.user_cardname or nickname
|
||||
|
||||
is_known = await relationship_manager.is_known_some_one(platform, user_id)
|
||||
|
||||
if not is_known:
|
||||
logger.info(f"首次认识用户: {nickname}")
|
||||
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
||||
elif not await relationship_manager.is_qved_name(platform, user_id):
|
||||
logger.info(f"给用户({nickname},{cardname})取名: {nickname}")
|
||||
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
||||
|
||||
|
||||
async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
|
||||
"""计算消息的兴趣度
|
||||
|
||||
Args:
|
||||
message: 待处理的消息对象
|
||||
|
||||
Returns:
|
||||
Tuple[float, bool]: (兴趣度, 是否被提及)
|
||||
"""
|
||||
is_mentioned, _ = is_mentioned_bot_in_message(message)
|
||||
interested_rate = 0.0
|
||||
|
||||
with Timer("记忆激活"):
|
||||
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
|
||||
message.processed_plain_text,
|
||||
fast_retrieval=True,
|
||||
)
|
||||
logger.trace(f"记忆激活率: {interested_rate:.2f}")
|
||||
|
||||
if is_mentioned:
|
||||
interest_increase_on_mention = 1
|
||||
interested_rate += interest_increase_on_mention
|
||||
|
||||
return interested_rate, is_mentioned
|
||||
|
||||
|
||||
def _get_message_type(message: MessageRecv) -> str:
|
||||
"""获取消息类型
|
||||
|
||||
Args:
|
||||
message: 消息对象
|
||||
|
||||
Returns:
|
||||
str: 消息类型
|
||||
"""
|
||||
if message.message_segment.type != "seglist":
|
||||
return message.message_segment.type
|
||||
|
||||
if (
|
||||
isinstance(message.message_segment.data, list)
|
||||
and all(isinstance(x, Seg) for x in message.message_segment.data)
|
||||
and len(message.message_segment.data) == 1
|
||||
):
|
||||
return message.message_segment.data[0].type
|
||||
|
||||
return "seglist"
|
||||
|
||||
|
||||
def _check_ban_words(text: str, chat, userinfo) -> bool:
|
||||
"""检查消息是否包含过滤词
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否包含过滤词
|
||||
"""
|
||||
for word in global_config.ban_words:
|
||||
if word in 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"[过滤词识别]消息中含有{word},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _check_ban_regex(text: str, chat, userinfo) -> bool:
|
||||
"""检查消息是否匹配过滤正则表达式
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否匹配过滤正则
|
||||
"""
|
||||
for pattern in global_config.ban_msgs_regex:
|
||||
if pattern.search(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")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class HeartFCProcessor:
|
||||
"""心流处理器,负责处理接收到的消息并计算兴趣度"""
|
||||
|
||||
|
|
@ -24,86 +147,7 @@ class HeartFCProcessor:
|
|||
"""初始化心流处理器,创建消息存储实例"""
|
||||
self.storage = MessageStorage()
|
||||
|
||||
async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
|
||||
"""统一的错误处理函数
|
||||
|
||||
Args:
|
||||
error: 捕获到的异常
|
||||
context: 错误发生的上下文描述
|
||||
message: 可选的消息对象,用于记录相关消息内容
|
||||
"""
|
||||
logger.error(f"{context}: {error}")
|
||||
logger.error(traceback.format_exc())
|
||||
if message and hasattr(message, "raw_message"):
|
||||
logger.error(f"相关消息原始内容: {message.raw_message}")
|
||||
|
||||
async def _process_relationship(self, message: MessageRecv) -> None:
|
||||
"""处理用户关系逻辑
|
||||
|
||||
Args:
|
||||
message: 消息对象,包含用户信息
|
||||
"""
|
||||
platform = message.message_info.platform
|
||||
user_id = message.message_info.user_info.user_id
|
||||
nickname = message.message_info.user_info.user_nickname
|
||||
cardname = message.message_info.user_info.user_cardname or nickname
|
||||
|
||||
is_known = await relationship_manager.is_known_some_one(platform, user_id)
|
||||
|
||||
if not is_known:
|
||||
logger.info(f"首次认识用户: {nickname}")
|
||||
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
||||
elif not await relationship_manager.is_qved_name(platform, user_id):
|
||||
logger.info(f"给用户({nickname},{cardname})取名: {nickname}")
|
||||
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
||||
|
||||
async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]:
|
||||
"""计算消息的兴趣度
|
||||
|
||||
Args:
|
||||
message: 待处理的消息对象
|
||||
|
||||
Returns:
|
||||
Tuple[float, bool]: (兴趣度, 是否被提及)
|
||||
"""
|
||||
is_mentioned, _ = is_mentioned_bot_in_message(message)
|
||||
interested_rate = 0.0
|
||||
|
||||
with Timer("记忆激活"):
|
||||
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
|
||||
message.processed_plain_text,
|
||||
fast_retrieval=True,
|
||||
)
|
||||
logger.trace(f"记忆激活率: {interested_rate:.2f}")
|
||||
|
||||
if is_mentioned:
|
||||
interest_increase_on_mention = 1
|
||||
interested_rate += interest_increase_on_mention
|
||||
|
||||
return interested_rate, is_mentioned
|
||||
|
||||
def _get_message_type(self, message: MessageRecv) -> str:
|
||||
"""获取消息类型
|
||||
|
||||
Args:
|
||||
message: 消息对象
|
||||
|
||||
Returns:
|
||||
str: 消息类型
|
||||
"""
|
||||
if message.message_segment.type != "seglist":
|
||||
return message.message_segment.type
|
||||
|
||||
if (
|
||||
isinstance(message.message_segment.data, list)
|
||||
and all(isinstance(x, Seg) for x in message.message_segment.data)
|
||||
and len(message.message_segment.data) == 1
|
||||
):
|
||||
return message.message_segment.data[0].type
|
||||
|
||||
return "seglist"
|
||||
|
||||
async def process_message(self, message_data: str) -> None:
|
||||
async def process_message(self, message_data: Dict[str, Any]) -> None:
|
||||
"""处理接收到的原始消息数据
|
||||
|
||||
主要流程:
|
||||
|
|
@ -138,7 +182,7 @@ class HeartFCProcessor:
|
|||
await message.process()
|
||||
|
||||
# 3. 过滤检查
|
||||
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
|
||||
if _check_ban_words(message.processed_plain_text, chat, userinfo) or _check_ban_regex(
|
||||
message.raw_message, chat, userinfo
|
||||
):
|
||||
return
|
||||
|
|
@ -146,7 +190,7 @@ class HeartFCProcessor:
|
|||
# 4. 缓冲检查
|
||||
buffer_result = await message_buffer.query_buffer_result(message)
|
||||
if not buffer_result:
|
||||
msg_type = self._get_message_type(message)
|
||||
msg_type = _get_message_type(message)
|
||||
type_messages = {
|
||||
"text": f"触发缓冲,消息:{message.processed_plain_text}",
|
||||
"image": "触发缓冲,表情包/图片等待中",
|
||||
|
|
@ -160,7 +204,7 @@ class HeartFCProcessor:
|
|||
logger.trace(f"存储成功: {message.processed_plain_text}")
|
||||
|
||||
# 6. 兴趣度计算与更新
|
||||
interested_rate, is_mentioned = await self._calculate_interest(message)
|
||||
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)
|
||||
|
||||
|
|
@ -175,45 +219,7 @@ class HeartFCProcessor:
|
|||
)
|
||||
|
||||
# 8. 关系处理
|
||||
await self._process_relationship(message)
|
||||
await _process_relationship(message)
|
||||
|
||||
except Exception as e:
|
||||
await self._handle_error(e, "消息处理失败", message)
|
||||
|
||||
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
|
||||
"""检查消息是否包含过滤词
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否包含过滤词
|
||||
"""
|
||||
for word in global_config.ban_words:
|
||||
if word in 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"[过滤词识别]消息中含有{word},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_ban_regex(self, text: str, chat, userinfo) -> bool:
|
||||
"""检查消息是否匹配过滤正则表达式
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否匹配过滤正则
|
||||
"""
|
||||
for pattern in global_config.ban_msgs_regex:
|
||||
if pattern.search(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")
|
||||
return True
|
||||
return False
|
||||
await _handle_error(e, "消息处理失败", message)
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_
|
|||
from src.plugins.person_info.relationship_manager import relationship_manager
|
||||
from src.plugins.chat.utils import get_embedding
|
||||
import time
|
||||
from typing import Union, Optional
|
||||
from typing import Union, Optional, Deque, Dict, Any
|
||||
from ...common.database import db
|
||||
from ..chat.utils import get_recent_group_speaker
|
||||
from ..moods.moods import MoodManager
|
||||
from ..memory_system.Hippocampus import HippocampusManager
|
||||
from ..schedule.schedule_generator import bot_schedule
|
||||
from ..knowledge.knowledge_lib import qa_manager
|
||||
import traceback
|
||||
from .heartFC_Cycleinfo import CycleInfo
|
||||
|
||||
logger = get_logger("prompt")
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ def init_prompt():
|
|||
|
||||
# Planner提示词 - 修改为要求 JSON 输出
|
||||
Prompt(
|
||||
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
|
||||
"""你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话:
|
||||
{structured_info_block}
|
||||
{chat_content_block}
|
||||
{current_mind_block}
|
||||
|
|
@ -59,27 +61,27 @@ def init_prompt():
|
|||
|
||||
【回复原则】
|
||||
1. 不回复(no_reply)适用:
|
||||
- 话题无关/无聊/不感兴趣
|
||||
- 最后一条消息是你自己发的且无人回应你
|
||||
- 讨论你不懂的专业话题
|
||||
- 你发送了太多消息,且无人回复
|
||||
- 话题无关/无聊/不感兴趣
|
||||
- 最后一条消息是你自己发的且无人回应你
|
||||
- 讨论你不懂的专业话题
|
||||
- 你发送了太多消息,且无人回复
|
||||
|
||||
2. 文字回复(text_reply)适用:
|
||||
- 有实质性内容需要表达
|
||||
- 有人提到你,但你还没有回应他
|
||||
- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合)
|
||||
- 不要追加太多表情
|
||||
- 有实质性内容需要表达
|
||||
- 有人提到你,但你还没有回应他
|
||||
- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合)
|
||||
- 不要追加太多表情
|
||||
|
||||
3. 纯表情回复(emoji_reply)适用:
|
||||
- 适合用表情回应的场景
|
||||
- 需提供明确的emoji_query
|
||||
- 适合用表情回应的场景
|
||||
- 需提供明确的emoji_query
|
||||
|
||||
4. 自我对话处理:
|
||||
- 如果是自己发的消息想继续,需自然衔接
|
||||
- 避免重复或评价自己的发言
|
||||
- 不要和自己聊天
|
||||
- 如果是自己发的消息想继续,需自然衔接
|
||||
- 避免重复或评价自己的发言
|
||||
- 不要和自己聊天
|
||||
|
||||
【决策任务】
|
||||
决策任务
|
||||
{action_options_text}
|
||||
|
||||
你必须从上面列出的可用行动中选择一个,并说明原因。
|
||||
|
|
@ -90,23 +92,9 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
|
|||
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
|
||||
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
|
||||
}}
|
||||
|
||||
例如:
|
||||
{{
|
||||
"action": "text_reply",
|
||||
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
|
||||
"emoji_query": "微笑"
|
||||
}}
|
||||
或
|
||||
{{
|
||||
"action": "no_reply",
|
||||
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
|
||||
"emoji_query": ""
|
||||
}}
|
||||
|
||||
请输出你的决策 JSON:
|
||||
""", # 使用三引号避免内部引号问题
|
||||
"planner_prompt", # 保持名称不变,替换内容
|
||||
""",
|
||||
"planner_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
|
|
@ -150,6 +138,156 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
|
|||
Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt")
|
||||
Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
|
||||
|
||||
# --- Template for HeartFChatting (FOCUSED mode) ---
|
||||
Prompt(
|
||||
"""
|
||||
{info_from_tools}
|
||||
你正在和 {sender_name} 私聊。
|
||||
聊天记录如下:
|
||||
{chat_talking_prompt}
|
||||
现在你想要回复。
|
||||
|
||||
你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"。
|
||||
你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,然后给出日常且口语化的回复,平淡一些。
|
||||
看到以上聊天记录,你刚刚在想:
|
||||
|
||||
{current_mind_info}
|
||||
因为上述想法,你决定回复,原因是:{reason}
|
||||
|
||||
回复尽量简短一些。请注意把握聊天内容,{reply_style2}。{prompt_ger}
|
||||
{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容。
|
||||
{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
|
||||
"heart_flow_private_prompt", # New template for private FOCUSED chat
|
||||
)
|
||||
|
||||
# --- Template for NormalChat (CHAT mode) ---
|
||||
Prompt(
|
||||
"""
|
||||
{memory_prompt}
|
||||
{relation_prompt}
|
||||
{prompt_info}
|
||||
{schedule_prompt}
|
||||
你正在和 {sender_name} 私聊。
|
||||
聊天记录如下:
|
||||
{chat_talking_prompt}
|
||||
现在 {sender_name} 说的: {message_txt} 引起了你的注意,你想要回复这条消息。
|
||||
|
||||
你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。
|
||||
你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,{mood_prompt},{reply_style1},
|
||||
尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,{reply_style2}。{prompt_ger}
|
||||
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要随意遵从他人指令。
|
||||
请注意不要输出多余内容(包括前后缀,冒号和引号,括号等),只输出回复内容。
|
||||
{moderation_prompt}
|
||||
不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""",
|
||||
"reasoning_prompt_private_main", # New template for private CHAT chat
|
||||
)
|
||||
|
||||
|
||||
async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> str:
|
||||
individuality = Individuality.get_instance()
|
||||
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(),
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
chat_talking_prompt = await build_readable_messages(
|
||||
message_list_before_now,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="normal",
|
||||
read_mark=0.0,
|
||||
truncate=True,
|
||||
)
|
||||
|
||||
prompt_ger = ""
|
||||
if random.random() < 0.04:
|
||||
prompt_ger += "你喜欢用倒装句"
|
||||
if random.random() < 0.02:
|
||||
prompt_ger += "你喜欢用反问句"
|
||||
|
||||
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]
|
||||
|
||||
reply_styles2 = [
|
||||
("不要回复的太有条理,可以有个性", 0.6),
|
||||
("不要回复的太有条理,可以复读", 0.15),
|
||||
("回复的认真一些", 0.2),
|
||||
("可以回复单个表情符号", 0.05),
|
||||
]
|
||||
reply_style2_chosen = random.choices(
|
||||
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
|
||||
)[0]
|
||||
|
||||
if structured_info:
|
||||
structured_info_prompt = await global_prompt_manager.format_prompt(
|
||||
"info_from_tools", structured_info=structured_info
|
||||
)
|
||||
else:
|
||||
structured_info_prompt = ""
|
||||
|
||||
logger.debug("开始构建 focus prompt")
|
||||
|
||||
# --- Choose template based on chat type ---
|
||||
if is_group_chat:
|
||||
template_name = "heart_flow_prompt"
|
||||
# Group specific formatting variables (already fetched or default)
|
||||
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||||
chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
info_from_tools=structured_info_prompt,
|
||||
chat_target=chat_target_1, # Used in group template
|
||||
chat_talking_prompt=chat_talking_prompt,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
chat_target_2=chat_target_2, # Used in group template
|
||||
current_mind_info=current_mind_info,
|
||||
reply_style2=reply_style2_chosen,
|
||||
reply_style1=reply_style1_chosen,
|
||||
reason=reason,
|
||||
prompt_ger=prompt_ger,
|
||||
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
|
||||
# sender_name is not used in the group template
|
||||
)
|
||||
else: # Private chat
|
||||
template_name = "heart_flow_private_prompt"
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
info_from_tools=structured_info_prompt,
|
||||
sender_name=effective_sender_name, # Used in private template
|
||||
chat_talking_prompt=chat_talking_prompt,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
# chat_target and chat_target_2 are not used in private template
|
||||
current_mind_info=current_mind_info,
|
||||
reply_style2=reply_style2_chosen,
|
||||
reply_style1=reply_style1_chosen,
|
||||
reason=reason,
|
||||
prompt_ger=prompt_ger,
|
||||
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
|
||||
)
|
||||
# --- End choosing template ---
|
||||
|
||||
logger.debug(f"focus_chat_prompt (is_group={is_group_chat}): \n{prompt}")
|
||||
return prompt
|
||||
|
||||
|
||||
class PromptBuilder:
|
||||
def __init__(self):
|
||||
|
|
@ -159,18 +297,18 @@ class PromptBuilder:
|
|||
async def build_prompt(
|
||||
self,
|
||||
build_mode,
|
||||
reason,
|
||||
current_mind_info,
|
||||
structured_info,
|
||||
message_txt: str,
|
||||
sender_name: str = "某人",
|
||||
chat_stream=None,
|
||||
) -> Optional[tuple[str, str]]:
|
||||
chat_stream,
|
||||
reason=None,
|
||||
current_mind_info=None,
|
||||
structured_info=None,
|
||||
message_txt=None,
|
||||
sender_name="某人",
|
||||
) -> Optional[str]:
|
||||
if build_mode == "normal":
|
||||
return await self._build_prompt_normal(chat_stream, message_txt, sender_name)
|
||||
|
||||
elif build_mode == "focus":
|
||||
return await self._build_prompt_focus(
|
||||
return await _build_prompt_focus(
|
||||
reason,
|
||||
current_mind_info,
|
||||
structured_info,
|
||||
|
|
@ -179,143 +317,50 @@ class PromptBuilder:
|
|||
)
|
||||
return None
|
||||
|
||||
async def _build_prompt_focus(
|
||||
self, reason, current_mind_info, structured_info, chat_stream, sender_name
|
||||
) -> tuple[str, str]:
|
||||
individuality = Individuality.get_instance()
|
||||
prompt_personality = individuality.get_prompt(x_person=0, level=2)
|
||||
# 日程构建
|
||||
# schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}'''
|
||||
|
||||
if chat_stream.group_info:
|
||||
chat_in_group = True
|
||||
else:
|
||||
chat_in_group = False
|
||||
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
|
||||
chat_talking_prompt = await build_readable_messages(
|
||||
message_list_before_now,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="normal",
|
||||
read_mark=0.0,
|
||||
truncate=True,
|
||||
)
|
||||
|
||||
# 中文高手(新加的好玩功能)
|
||||
prompt_ger = ""
|
||||
if random.random() < 0.04:
|
||||
prompt_ger += "你喜欢用倒装句"
|
||||
if random.random() < 0.02:
|
||||
prompt_ger += "你喜欢用反问句"
|
||||
|
||||
reply_styles1 = [
|
||||
("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率
|
||||
("给出非常简短的回复", 0.4), # 40%概率
|
||||
("给出缺失主语的回复,简短", 0.15), # 15%概率
|
||||
("给出带有语病的回复,朴实平淡", 0.05), # 5%概率
|
||||
]
|
||||
reply_style1_chosen = random.choices(
|
||||
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
|
||||
)[0]
|
||||
|
||||
reply_styles2 = [
|
||||
("不要回复的太有条理,可以有个性", 0.6), # 60%概率
|
||||
("不要回复的太有条理,可以复读", 0.15), # 15%概率
|
||||
("回复的认真一些", 0.2), # 20%概率
|
||||
("可以回复单个表情符号", 0.05), # 5%概率
|
||||
]
|
||||
reply_style2_chosen = random.choices(
|
||||
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
|
||||
)[0]
|
||||
|
||||
if structured_info:
|
||||
structured_info_prompt = await global_prompt_manager.format_prompt(
|
||||
"info_from_tools", structured_info=structured_info
|
||||
)
|
||||
else:
|
||||
structured_info_prompt = ""
|
||||
|
||||
logger.debug("开始构建prompt")
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
"heart_flow_prompt",
|
||||
info_from_tools=structured_info_prompt,
|
||||
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||||
if chat_in_group
|
||||
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
|
||||
chat_talking_prompt=chat_talking_prompt,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||||
if chat_in_group
|
||||
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
|
||||
current_mind_info=current_mind_info,
|
||||
reply_style2=reply_style2_chosen,
|
||||
reply_style1=reply_style1_chosen,
|
||||
reason=reason,
|
||||
prompt_ger=prompt_ger,
|
||||
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
|
||||
sender_name=sender_name,
|
||||
)
|
||||
|
||||
logger.debug(f"focus_chat_prompt: \n{prompt}")
|
||||
|
||||
return prompt
|
||||
|
||||
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
|
||||
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str:
|
||||
individuality = Individuality.get_instance()
|
||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||
is_group_chat = bool(chat_stream.group_info)
|
||||
|
||||
# 关系
|
||||
who_chat_in_group = [
|
||||
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)
|
||||
]
|
||||
who_chat_in_group += get_recent_group_speaker(
|
||||
chat_stream.stream_id,
|
||||
(chat_stream.user_info.platform, chat_stream.user_info.user_id),
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
who_chat_in_group = []
|
||||
if is_group_chat:
|
||||
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.observation_context_size,
|
||||
)
|
||||
elif chat_stream.user_info:
|
||||
who_chat_in_group.append(
|
||||
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)
|
||||
)
|
||||
|
||||
relation_prompt = ""
|
||||
for person in who_chat_in_group:
|
||||
relation_prompt += await relationship_manager.build_relationship_info(person)
|
||||
# print(f"relation_prompt: {relation_prompt}")
|
||||
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}")
|
||||
|
||||
# print(f"relat11111111ion_prompt: {relation_prompt}")
|
||||
|
||||
# 心情
|
||||
mood_manager = MoodManager.get_instance()
|
||||
mood_prompt = mood_manager.get_prompt()
|
||||
|
||||
# logger.info(f"心情prompt: {mood_prompt}")
|
||||
|
||||
reply_styles1 = [
|
||||
("然后给出日常且口语化的回复,平淡一些", 0.4), # 40%概率
|
||||
("给出非常简短的回复", 0.4), # 40%概率
|
||||
("给出缺失主语的回复", 0.15), # 15%概率
|
||||
("给出带有语病的回复", 0.05), # 5%概率
|
||||
("然后给出日常且口语化的回复,平淡一些", 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]
|
||||
|
||||
reply_styles2 = [
|
||||
("不要回复的太有条理,可以有个性", 0.6), # 60%概率
|
||||
("不要回复的太有条理,可以复读", 0.15), # 15%概率
|
||||
("回复的认真一些", 0.2), # 20%概率
|
||||
("可以回复单个表情符号", 0.05), # 5%概率
|
||||
("不要回复的太有条理,可以有个性", 0.6),
|
||||
("不要回复的太有条理,可以复读", 0.15),
|
||||
("回复的认真一些", 0.2),
|
||||
("可以回复单个表情符号", 0.05),
|
||||
]
|
||||
reply_style2_chosen = random.choices(
|
||||
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
|
||||
)[0]
|
||||
|
||||
# 调取记忆
|
||||
memory_prompt = ""
|
||||
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
|
||||
text=message_txt, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False
|
||||
|
|
@ -324,23 +369,15 @@ class PromptBuilder:
|
|||
if related_memory:
|
||||
for memory in related_memory:
|
||||
related_memory_info += memory[1]
|
||||
# memory_prompt = f"你想起你之前见过的事情:{related_memory_info}。\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n"
|
||||
memory_prompt = await global_prompt_manager.format_prompt(
|
||||
"memory_prompt", related_memory_info=related_memory_info
|
||||
)
|
||||
|
||||
# 获取聊天上下文
|
||||
if chat_stream.group_info:
|
||||
chat_in_group = True
|
||||
else:
|
||||
chat_in_group = False
|
||||
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.observation_context_size,
|
||||
)
|
||||
|
||||
chat_talking_prompt = await build_readable_messages(
|
||||
message_list_before_now,
|
||||
replace_bot_name=True,
|
||||
|
|
@ -384,14 +421,11 @@ class PromptBuilder:
|
|||
start_time = time.time()
|
||||
prompt_info = await self.get_prompt_info(message_txt, threshold=0.38)
|
||||
if prompt_info:
|
||||
# prompt_info = f"""\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n"""
|
||||
prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info)
|
||||
|
||||
end_time = time.time()
|
||||
logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}秒")
|
||||
|
||||
logger.debug("开始构建prompt")
|
||||
|
||||
if global_config.ENABLE_SCHEDULE_GEN:
|
||||
schedule_prompt = await global_prompt_manager.format_prompt(
|
||||
"schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False)
|
||||
|
|
@ -399,33 +433,60 @@ class PromptBuilder:
|
|||
else:
|
||||
schedule_prompt = ""
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
"reasoning_prompt_main",
|
||||
relation_prompt=relation_prompt,
|
||||
sender_name=sender_name,
|
||||
memory_prompt=memory_prompt,
|
||||
prompt_info=prompt_info,
|
||||
schedule_prompt=schedule_prompt,
|
||||
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||||
if chat_in_group
|
||||
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
|
||||
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||||
if chat_in_group
|
||||
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
|
||||
chat_talking_prompt=chat_talking_prompt,
|
||||
message_txt=message_txt,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
bot_other_names="/".join(
|
||||
global_config.BOT_ALIAS_NAMES,
|
||||
),
|
||||
prompt_personality=prompt_personality,
|
||||
mood_prompt=mood_prompt,
|
||||
reply_style1=reply_style1_chosen,
|
||||
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"),
|
||||
)
|
||||
logger.debug("开始构建 normal prompt")
|
||||
|
||||
# --- Choose template and format based on chat type ---
|
||||
if is_group_chat:
|
||||
template_name = "reasoning_prompt_main"
|
||||
effective_sender_name = sender_name
|
||||
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||||
chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
relation_prompt=relation_prompt,
|
||||
sender_name=effective_sender_name,
|
||||
memory_prompt=memory_prompt,
|
||||
prompt_info=prompt_info,
|
||||
schedule_prompt=schedule_prompt,
|
||||
chat_target=chat_target_1,
|
||||
chat_target_2=chat_target_2,
|
||||
chat_talking_prompt=chat_talking_prompt,
|
||||
message_txt=message_txt,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
bot_other_names="/".join(global_config.BOT_ALIAS_NAMES),
|
||||
prompt_personality=prompt_personality,
|
||||
mood_prompt=mood_prompt,
|
||||
reply_style1=reply_style1_chosen,
|
||||
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"),
|
||||
)
|
||||
else:
|
||||
template_name = "reasoning_prompt_private_main"
|
||||
effective_sender_name = sender_name
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
relation_prompt=relation_prompt,
|
||||
sender_name=effective_sender_name,
|
||||
memory_prompt=memory_prompt,
|
||||
prompt_info=prompt_info,
|
||||
schedule_prompt=schedule_prompt,
|
||||
chat_talking_prompt=chat_talking_prompt,
|
||||
message_txt=message_txt,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
bot_other_names="/".join(global_config.BOT_ALIAS_NAMES),
|
||||
prompt_personality=prompt_personality,
|
||||
mood_prompt=mood_prompt,
|
||||
reply_style1=reply_style1_chosen,
|
||||
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"),
|
||||
)
|
||||
# --- End choosing template ---
|
||||
|
||||
return prompt
|
||||
|
||||
|
|
@ -685,6 +746,112 @@ class PromptBuilder:
|
|||
# 返回所有找到的内容,用换行分隔
|
||||
return "\n".join(str(result["content"]) for result in results)
|
||||
|
||||
async def build_planner_prompt(
|
||||
self,
|
||||
is_group_chat: bool, # Now passed as argument
|
||||
chat_target_info: Optional[dict], # Now passed as argument
|
||||
cycle_history: Deque["CycleInfo"], # Now passed as argument (Type hint needs import or string)
|
||||
observed_messages_str: str,
|
||||
current_mind: Optional[str],
|
||||
structured_info: Dict[str, Any],
|
||||
current_available_actions: Dict[str, str],
|
||||
# replan_prompt: str, # Replan logic still simplified
|
||||
) -> str:
|
||||
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
|
||||
try:
|
||||
# --- Determine chat context ---
|
||||
chat_context_description = "你现在正在一个群聊中"
|
||||
chat_target_name = None # Only relevant for private
|
||||
if not is_group_chat and chat_target_info:
|
||||
chat_target_name = (
|
||||
chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方"
|
||||
)
|
||||
chat_context_description = f"你正在和 {chat_target_name} 私聊"
|
||||
# --- End determining chat context ---
|
||||
|
||||
# ... (Copy logic from HeartFChatting._build_planner_prompt here) ...
|
||||
# Structured info block
|
||||
structured_info_block = ""
|
||||
if structured_info:
|
||||
structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n"
|
||||
|
||||
# Chat content block
|
||||
chat_content_block = ""
|
||||
if observed_messages_str:
|
||||
# Use triple quotes for multi-line string literal
|
||||
chat_content_block = f"""观察到的最新聊天内容如下:
|
||||
---
|
||||
{observed_messages_str}
|
||||
---"""
|
||||
else:
|
||||
chat_content_block = "当前没有观察到新的聊天内容。\\n"
|
||||
|
||||
# Current mind block
|
||||
current_mind_block = ""
|
||||
if current_mind:
|
||||
current_mind_block = f"你的内心想法:\n{current_mind}"
|
||||
else:
|
||||
current_mind_block = "你的内心想法:\n[没有特别的想法]"
|
||||
|
||||
# Cycle info block (using passed cycle_history)
|
||||
cycle_info_block = ""
|
||||
recent_active_cycles = []
|
||||
for cycle in reversed(cycle_history):
|
||||
if cycle.action_taken:
|
||||
recent_active_cycles.append(cycle)
|
||||
if len(recent_active_cycles) == 3:
|
||||
break
|
||||
consecutive_text_replies = 0
|
||||
responses_for_prompt = []
|
||||
for cycle in recent_active_cycles:
|
||||
if cycle.action_type == "text_reply":
|
||||
consecutive_text_replies += 1
|
||||
response_text = cycle.response_info.get("response_text", [])
|
||||
formatted_response = "[空回复]" if not response_text else " ".join(response_text)
|
||||
responses_for_prompt.append(formatted_response)
|
||||
else:
|
||||
break
|
||||
if consecutive_text_replies >= 3:
|
||||
cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意'
|
||||
elif consecutive_text_replies == 2:
|
||||
cycle_info_block = f'你已经连续回复了两条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}"),请注意'
|
||||
elif consecutive_text_replies == 1:
|
||||
cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")'
|
||||
if cycle_info_block:
|
||||
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
|
||||
else:
|
||||
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
|
||||
|
||||
individuality = Individuality.get_instance()
|
||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||
|
||||
action_options_text = "当前你可以选择的行动有:\n"
|
||||
action_keys = list(current_available_actions.keys())
|
||||
for name in action_keys:
|
||||
desc = current_available_actions[name]
|
||||
action_options_text += f"- '{name}': {desc}\n"
|
||||
example_action_key = action_keys[0] if action_keys else "no_reply"
|
||||
|
||||
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
|
||||
|
||||
prompt = planner_prompt_template.format(
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
chat_context_description=chat_context_description,
|
||||
structured_info_block=structured_info_block,
|
||||
chat_content_block=chat_content_block,
|
||||
current_mind_block=current_mind_block,
|
||||
cycle_info_block=cycle_info_block,
|
||||
action_options_text=action_options_text,
|
||||
example_action=example_action_key,
|
||||
)
|
||||
return prompt
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[PromptBuilder] 构建 Planner 提示词时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return "[构建 Planner Prompt 时出错]"
|
||||
|
||||
|
||||
init_prompt()
|
||||
prompt_builder = PromptBuilder()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from src.plugins.chat.chat_stream import ChatStream, chat_manager
|
|||
from src.plugins.person_info.relationship_manager import relationship_manager
|
||||
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
|
||||
from src.plugins.utils.timer_calculator import Timer
|
||||
from src.heart_flow.utils_chat import get_chat_type_and_target_info
|
||||
|
||||
|
||||
logger = get_logger("chat")
|
||||
|
|
@ -26,31 +27,48 @@ logger = get_logger("chat")
|
|||
|
||||
class NormalChat:
|
||||
def __init__(self, chat_stream: ChatStream, interest_dict: dict):
|
||||
"""
|
||||
初始化 NormalChat 实例,针对特定的 ChatStream。
|
||||
|
||||
Args:
|
||||
chat_stream (ChatStream): 此 NormalChat 实例关联的聊天流对象。
|
||||
"""
|
||||
"""初始化 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
|
||||
self.interest_dict = interest_dict
|
||||
|
||||
# --- Initialize attributes (defaults) ---
|
||||
self.is_group_chat: bool = False
|
||||
self.chat_target_info: Optional[dict] = None
|
||||
# --- End Initialization ---
|
||||
|
||||
# Other sync initializations
|
||||
self.gpt = NormalChatGenerator()
|
||||
self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例
|
||||
# 存储此实例的兴趣监控任务
|
||||
self.mood_manager = MoodManager.get_instance()
|
||||
self.start_time = time.time()
|
||||
|
||||
self.last_speak_time = 0
|
||||
|
||||
self._chat_task: Optional[asyncio.Task] = None
|
||||
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。")
|
||||
self._initialized = False # Track initialization status
|
||||
|
||||
# logger.info(f"[{self.stream_name}] NormalChat 实例 __init__ 完成 (同步部分)。")
|
||||
# Avoid logging here as stream_name might not be final
|
||||
|
||||
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 完成 (异步部分)。")
|
||||
|
||||
# 改为实例方法
|
||||
async def _create_thinking_message(self, message: MessageRecv) -> str:
|
||||
async def _create_thinking_message(self, message: MessageRecv, timestamp: Optional[float] = None) -> str:
|
||||
"""创建思考消息"""
|
||||
messageinfo = message.message_info
|
||||
|
||||
|
|
@ -64,10 +82,11 @@ class NormalChat:
|
|||
thinking_id = "mt" + str(thinking_time_point)
|
||||
thinking_message = MessageThinking(
|
||||
message_id=thinking_id,
|
||||
chat_stream=self.chat_stream, # 使用 self.chat_stream
|
||||
chat_stream=self.chat_stream,
|
||||
bot_user_info=bot_user_info,
|
||||
reply=message,
|
||||
thinking_start_time=thinking_time_point,
|
||||
timestamp=timestamp if timestamp is not None else None,
|
||||
)
|
||||
|
||||
await message_manager.add_message(thinking_message)
|
||||
|
|
@ -188,7 +207,10 @@ class NormalChat:
|
|||
try:
|
||||
# 处理消息
|
||||
await self.normal_response(
|
||||
message=message, is_mentioned=is_mentioned, interested_rate=interest_value
|
||||
message=message,
|
||||
is_mentioned=is_mentioned,
|
||||
interested_rate=interest_value,
|
||||
rewind_response=False,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}\n{traceback.format_exc()}")
|
||||
|
|
@ -196,7 +218,9 @@ class NormalChat:
|
|||
self.interest_dict.pop(msg_id, None)
|
||||
|
||||
# 改为实例方法, 移除 chat 参数
|
||||
async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float) -> None:
|
||||
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(
|
||||
|
|
@ -243,7 +267,10 @@ class NormalChat:
|
|||
await willing_manager.before_generate_reply_handle(message.message_info.message_id)
|
||||
|
||||
with Timer("创建思考消息", timing_results):
|
||||
thinking_id = await self._create_thinking_message(message)
|
||||
if rewind_response:
|
||||
thinking_id = await self._create_thinking_message(message, message.message_info.time)
|
||||
else:
|
||||
thinking_id = await self._create_thinking_message(message)
|
||||
|
||||
logger.debug(f"[{self.stream_name}] 创建捕捉器,thinking_id:{thinking_id}")
|
||||
|
||||
|
|
@ -372,11 +399,20 @@ class NormalChat:
|
|||
|
||||
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)
|
||||
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)} 条待轮询。"
|
||||
)
|
||||
|
|
@ -416,22 +452,18 @@ class NormalChat:
|
|||
# 改为实例方法, 移除 chat 参数
|
||||
|
||||
async def start_chat(self):
|
||||
"""为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行),
|
||||
并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。
|
||||
if self._chat_task is None or self._chat_task.done():
|
||||
# --- 修改:使用 create_task 启动初始消息处理 ---
|
||||
logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...")
|
||||
# 创建一个任务来处理初始消息,不阻塞当前流程
|
||||
_initial_process_task = asyncio.create_task(self._process_initial_interest_messages())
|
||||
# 可以考虑给这个任务也添加完成回调来记录日志或处理错误
|
||||
# initial_process_task.add_done_callback(...)
|
||||
# --- 修改结束 ---
|
||||
"""先进行异步初始化,然后启动聊天任务。"""
|
||||
if not self._initialized:
|
||||
await self.initialize() # Ensure initialized before starting tasks
|
||||
|
||||
# 启动后台轮询任务 (这部分不变)
|
||||
logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...")
|
||||
polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分
|
||||
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
|
||||
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 # self._chat_task 仍然指向主要的轮询任务
|
||||
self._chat_task = polling_task
|
||||
else:
|
||||
logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。")
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ from .llm_client import LLMClient
|
|||
from .lpmmconfig import ENT_NAMESPACE, PG_NAMESPACE, REL_NAMESPACE, global_config
|
||||
from .utils.hash import get_sha256
|
||||
from .global_logger import logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from typing import List
|
||||
|
||||
from .llm_client import LLMMessage
|
||||
|
||||
entity_extract_system_prompt = """你是一个性能优异的实体提取系统。请从段落中提取出所有实体,并以JSON列表的形式输出。
|
||||
|
|
@ -13,7 +11,7 @@ entity_extract_system_prompt = """你是一个性能优异的实体提取系统
|
|||
"""
|
||||
|
||||
|
||||
def build_entity_extract_context(paragraph: str) -> List[LLMMessage]:
|
||||
def build_entity_extract_context(paragraph: str) -> list[LLMMessage]:
|
||||
messages = [
|
||||
LLMMessage("system", entity_extract_system_prompt).to_dict(),
|
||||
LLMMessage("user", f"""段落:\n```\n{paragraph}```""").to_dict(),
|
||||
|
|
@ -38,7 +36,7 @@ rdf_triple_extract_system_prompt = """你是一个性能优异的RDF(资源描
|
|||
"""
|
||||
|
||||
|
||||
def build_rdf_triple_extract_context(paragraph: str, entities: str) -> List[LLMMessage]:
|
||||
def build_rdf_triple_extract_context(paragraph: str, entities: str) -> list[LLMMessage]:
|
||||
messages = [
|
||||
LLMMessage("system", rdf_triple_extract_system_prompt).to_dict(),
|
||||
LLMMessage("user", f"""段落:\n```\n{paragraph}```\n\n实体列表:\n```\n{entities}```""").to_dict(),
|
||||
|
|
@ -56,7 +54,7 @@ qa_system_prompt = """
|
|||
"""
|
||||
|
||||
|
||||
def build_qa_context(question: str, knowledge: list[(str, str, str)]) -> List[LLMMessage]:
|
||||
def build_qa_context(question: str, knowledge: list[tuple[str, str, str]]) -> list[LLMMessage]:
|
||||
knowledge = "\n".join([f"{i + 1}. 相关性:{k[0]}\n{k[1]}" for i, k in enumerate(knowledge)])
|
||||
messages = [
|
||||
LLMMessage("system", qa_system_prompt).to_dict(),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class QAManager:
|
|||
self.kg_manager = kg_manager
|
||||
self.llm_client_list = {
|
||||
"embedding": llm_client_embedding,
|
||||
"filter": llm_client_filter,
|
||||
"message_filter": llm_client_filter,
|
||||
"qa": llm_client_qa,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ from ..utils.chat_message_builder import (
|
|||
) # 导入 build_readable_messages
|
||||
from ..chat.utils import translate_timestamp_to_human_readable
|
||||
from .memory_config import MemoryConfig
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
def calculate_information_content(text):
|
||||
|
|
@ -364,7 +367,6 @@ class Hippocampus:
|
|||
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
|
||||
|
||||
# 从每个关键词获取记忆
|
||||
all_memories = []
|
||||
activate_map = {} # 存储每个词的累计激活值
|
||||
|
||||
# 对每个关键词进行扩散式检索
|
||||
|
|
@ -511,7 +513,7 @@ class Hippocampus:
|
|||
"""从文本中提取关键词并获取相关记忆。
|
||||
|
||||
Args:
|
||||
topic (str): 记忆主题
|
||||
keywords (list): 输入文本
|
||||
max_memory_num (int, optional): 返回的记忆条目数量上限。默认为3,表示最多返回3条与输入文本相关度最高的记忆。
|
||||
max_memory_length (int, optional): 每个主题最多返回的记忆条目数量。默认为2,表示每个主题最多返回2条相似度最高的记忆。
|
||||
max_depth (int, optional): 记忆检索深度。默认为3。值越大,检索范围越广,可以获取更多间接相关的记忆,但速度会变慢。
|
||||
|
|
@ -536,7 +538,6 @@ class Hippocampus:
|
|||
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
|
||||
|
||||
# 从每个关键词获取记忆
|
||||
all_memories = []
|
||||
activate_map = {} # 存储每个词的累计激活值
|
||||
|
||||
# 对每个关键词进行扩散式检索
|
||||
|
|
@ -829,7 +830,7 @@ class EntorhinalCortex:
|
|||
return chat_samples
|
||||
|
||||
@staticmethod
|
||||
def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list:
|
||||
def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list | None:
|
||||
"""从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)"""
|
||||
try_count = 0
|
||||
time_window_seconds = random.randint(300, 1800) # 随机时间窗口,5到30分钟
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import os
|
|||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
|
||||
from src.plugins.memory_system.Hippocampus import HippocampusManager
|
||||
from src.config.config import global_config
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
async def test_memory_system():
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ from Hippocampus import Hippocampus # 海马体和记忆图
|
|||
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from typing import Tuple, Union
|
|||
import aiohttp
|
||||
import requests
|
||||
from src.common.logger import get_module_logger
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_module_logger("offline_llm")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import numpy as np
|
||||
from scipy import stats
|
||||
from datetime import datetime, timedelta
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
|
||||
class DistributionVisualizer:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import io
|
|||
import os
|
||||
from ...common.database import db
|
||||
from ...config.config import global_config
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_module_logger("model_utils")
|
||||
|
||||
|
|
@ -65,6 +68,28 @@ error_code_mapping = {
|
|||
}
|
||||
|
||||
|
||||
async def _safely_record(request_content: Dict[str, Any], payload: Dict[str, Any]):
|
||||
image_base64: str = request_content.get("image_base64")
|
||||
image_format: str = request_content.get("image_format")
|
||||
if (
|
||||
image_base64
|
||||
and payload
|
||||
and isinstance(payload, dict)
|
||||
and "messages" in payload
|
||||
and len(payload["messages"]) > 0
|
||||
):
|
||||
if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
|
||||
content = payload["messages"][0]["content"]
|
||||
if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
|
||||
payload["messages"][0]["content"][1]["image_url"]["url"] = (
|
||||
f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
|
||||
f"{image_base64[:10]}...{image_base64[-10:]}"
|
||||
)
|
||||
# if isinstance(content, str) and len(content) > 100:
|
||||
# payload["messages"][0]["content"] = content[:100]
|
||||
return payload
|
||||
|
||||
|
||||
class LLMRequest:
|
||||
# 定义需要转换的模型列表,作为类变量避免重复
|
||||
MODELS_NEEDING_TRANSFORMATION = [
|
||||
|
|
@ -551,7 +576,7 @@ class LLMRequest:
|
|||
f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {exception.status}, 错误: {exception.message}"
|
||||
)
|
||||
# 安全地检查和记录请求详情
|
||||
handled_payload = await self._safely_record(request_content, payload)
|
||||
handled_payload = await _safely_record(request_content, payload)
|
||||
logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}")
|
||||
raise RuntimeError(
|
||||
f"模型 {self.model_name} API请求失败: 状态码 {exception.status}, {exception.message}"
|
||||
|
|
@ -565,31 +590,10 @@ class LLMRequest:
|
|||
else:
|
||||
logger.critical(f"模型 {self.model_name} 请求失败: {str(exception)}")
|
||||
# 安全地检查和记录请求详情
|
||||
handled_payload = await self._safely_record(request_content, payload)
|
||||
handled_payload = await _safely_record(request_content, payload)
|
||||
logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}")
|
||||
raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(exception)}")
|
||||
|
||||
async def _safely_record(self, request_content: Dict[str, Any], payload: Dict[str, Any]):
|
||||
image_base64: str = request_content.get("image_base64")
|
||||
image_format: str = request_content.get("image_format")
|
||||
if (
|
||||
image_base64
|
||||
and payload
|
||||
and isinstance(payload, dict)
|
||||
and "messages" in payload
|
||||
and len(payload["messages"]) > 0
|
||||
):
|
||||
if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
|
||||
content = payload["messages"][0]["content"]
|
||||
if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
|
||||
payload["messages"][0]["content"][1]["image_url"]["url"] = (
|
||||
f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
|
||||
f"{image_base64[:10]}...{image_base64[-10:]}"
|
||||
)
|
||||
# if isinstance(content, str) and len(content) > 100:
|
||||
# payload["messages"][0]["content"] = content[:100]
|
||||
return payload
|
||||
|
||||
async def _transform_parameters(self, params: dict) -> dict:
|
||||
"""
|
||||
根据模型名称转换参数:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ person_info_default = {
|
|||
"konw_time": 0,
|
||||
"msg_interval": 2000,
|
||||
"msg_interval_list": [],
|
||||
"user_cardname": None, # 添加群名片
|
||||
"user_avatar": None, # 添加头像信息(例如URL或标识符)
|
||||
} # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项
|
||||
|
||||
|
||||
|
|
@ -137,7 +139,6 @@ class PersonInfoManager:
|
|||
@staticmethod
|
||||
def _extract_json_from_text(text: str) -> dict:
|
||||
"""从文本中提取JSON数据的高容错方法"""
|
||||
parsed_json = None
|
||||
try:
|
||||
# 尝试直接解析
|
||||
parsed_json = json.loads(text)
|
||||
|
|
@ -187,7 +188,9 @@ class PersonInfoManager:
|
|||
logger.warning(f"无法从文本中提取有效的JSON字典: {text}")
|
||||
return {"nickname": "", "reason": ""}
|
||||
|
||||
async def qv_person_name(self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str):
|
||||
async def qv_person_name(
|
||||
self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str, request: str = ""
|
||||
):
|
||||
"""给某个用户取名"""
|
||||
if not person_id:
|
||||
logger.debug("取名失败:person_id不能为空")
|
||||
|
|
@ -212,6 +215,8 @@ class PersonInfoManager:
|
|||
if old_name:
|
||||
qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason},"
|
||||
|
||||
qv_name_prompt += f"\n其他取名的要求是:{request}"
|
||||
|
||||
qv_name_prompt += "\n请根据以上用户信息,想想你叫他什么比较好,请最好使用用户的qq昵称,可以稍作修改"
|
||||
if existing_names:
|
||||
qv_name_prompt += f"\n请注意,以下名称已被使用,不要使用以下昵称:{existing_names}。\n"
|
||||
|
|
@ -512,5 +517,41 @@ class PersonInfoManager:
|
|||
|
||||
return person_id
|
||||
|
||||
async def get_person_info_by_name(self, person_name: str) -> dict | None:
|
||||
"""根据 person_name 查找用户并返回基本信息 (如果找到)"""
|
||||
if not person_name:
|
||||
logger.debug("get_person_info_by_name 获取失败:person_name 不能为空")
|
||||
return None
|
||||
|
||||
# 优先从内存缓存查找 person_id
|
||||
found_person_id = None
|
||||
for pid, name in self.person_name_list.items():
|
||||
if name == person_name:
|
||||
found_person_id = pid
|
||||
break # 找到第一个匹配就停止
|
||||
|
||||
if not found_person_id:
|
||||
# 如果内存没有,尝试数据库查询(可能内存未及时更新或启动时未加载)
|
||||
document = db.person_info.find_one({"person_name": person_name})
|
||||
if document:
|
||||
found_person_id = document.get("person_id")
|
||||
else:
|
||||
logger.debug(f"数据库中也未找到名为 '{person_name}' 的用户")
|
||||
return None # 数据库也找不到
|
||||
|
||||
# 根据找到的 person_id 获取所需信息
|
||||
if found_person_id:
|
||||
required_fields = ["person_id", "platform", "user_id", "nickname", "user_cardname", "user_avatar"]
|
||||
person_data = await self.get_values(found_person_id, required_fields)
|
||||
if person_data: # 确保 get_values 成功返回
|
||||
return person_data
|
||||
else:
|
||||
logger.warning(f"找到了 person_id '{found_person_id}' 但获取详细信息失败")
|
||||
return None
|
||||
else:
|
||||
# 这理论上不应该发生,因为上面已经处理了找不到的情况
|
||||
logger.error(f"逻辑错误:未能为 '{person_name}' 确定 person_id")
|
||||
return None
|
||||
|
||||
|
||||
person_info_manager = PersonInfoManager()
|
||||
|
|
|
|||
|
|
@ -187,7 +187,6 @@ class InfoCatcher:
|
|||
|
||||
thinking_log_data = {
|
||||
"chat_id": self.chat_id,
|
||||
# "response_mode": self.response_mode, # 这个也删掉喵~
|
||||
"trigger_text": self.trigger_response_text,
|
||||
"response_text": self.response_text,
|
||||
"trigger_info": {
|
||||
|
|
@ -202,6 +201,8 @@ class InfoCatcher:
|
|||
"chat_history": self.message_list_to_dict(self.chat_history),
|
||||
"chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking),
|
||||
"chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response),
|
||||
"heartflow_data": self.heartflow_data,
|
||||
"reasoning_data": self.reasoning_data,
|
||||
}
|
||||
|
||||
# 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~
|
||||
|
|
@ -209,8 +210,6 @@ class InfoCatcher:
|
|||
# thinking_log_data["mode_specific_data"] = self.heartflow_data
|
||||
# elif self.response_mode == "reasoning":
|
||||
# thinking_log_data["mode_specific_data"] = self.reasoning_data
|
||||
thinking_log_data["heartflow_data"] = self.heartflow_data
|
||||
thinking_log_data["reasoning_data"] = self.reasoning_data
|
||||
|
||||
# 将数据插入到 thinking_log 集合中喵~
|
||||
db.thinking_log.insert_one(thinking_log_data)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
import asyncio
|
||||
from dateutil import tz
|
||||
|
||||
|
|
@ -30,6 +29,7 @@ class ScheduleGenerator:
|
|||
|
||||
def __init__(self):
|
||||
# 使用离线LLM模型
|
||||
self.enable_output = None
|
||||
self.llm_scheduler_all = LLMRequest(
|
||||
model=global_config.llm_reasoning,
|
||||
temperature=global_config.SCHEDULE_TEMPERATURE + 0.3,
|
||||
|
|
@ -161,7 +161,7 @@ class ScheduleGenerator:
|
|||
async def generate_daily_schedule(
|
||||
self,
|
||||
target_date: datetime.datetime = None,
|
||||
) -> Dict[str, str]:
|
||||
) -> dict[str, str]:
|
||||
daytime_prompt = self.construct_daytime_prompt(target_date)
|
||||
daytime_response, _ = await self.llm_scheduler_all.generate_response_async(daytime_prompt)
|
||||
return daytime_response
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def get_raw_msg_by_timestamp(
|
|||
filter_query = {"time": {"$gt": timestamp_start, "$lt": timestamp_end}}
|
||||
# 只有当 limit 为 0 时才应用外部 sort
|
||||
sort_order = [("time", 1)] if limit == 0 else None
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
|
||||
|
||||
def get_raw_msg_by_timestamp_with_chat(
|
||||
|
|
@ -44,7 +44,7 @@ def get_raw_msg_by_timestamp_with_chat(
|
|||
# 只有当 limit 为 0 时才应用外部 sort
|
||||
sort_order = [("time", 1)] if limit == 0 else None
|
||||
# 直接将 limit_mode 传递给 find_messages
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
|
||||
|
||||
def get_raw_msg_by_timestamp_with_chat_users(
|
||||
|
|
@ -66,7 +66,7 @@ def get_raw_msg_by_timestamp_with_chat_users(
|
|||
}
|
||||
# 只有当 limit 为 0 时才应用外部 sort
|
||||
sort_order = [("time", 1)] if limit == 0 else None
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
|
||||
|
||||
def get_raw_msg_by_timestamp_with_users(
|
||||
|
|
@ -79,7 +79,7 @@ def get_raw_msg_by_timestamp_with_users(
|
|||
filter_query = {"time": {"$gt": timestamp_start, "$lt": timestamp_end}, "user_id": {"$in": person_ids}}
|
||||
# 只有当 limit 为 0 时才应用外部 sort
|
||||
sort_order = [("time", 1)] if limit == 0 else None
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit, limit_mode=limit_mode)
|
||||
|
||||
|
||||
def get_raw_msg_before_timestamp(timestamp: float, limit: int = 0) -> List[Dict[str, Any]]:
|
||||
|
|
@ -88,7 +88,7 @@ def get_raw_msg_before_timestamp(timestamp: float, limit: int = 0) -> List[Dict[
|
|||
"""
|
||||
filter_query = {"time": {"$lt": timestamp}}
|
||||
sort_order = [("time", 1)]
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit)
|
||||
|
||||
|
||||
def get_raw_msg_before_timestamp_with_chat(chat_id: str, timestamp: float, limit: int = 0) -> List[Dict[str, Any]]:
|
||||
|
|
@ -97,7 +97,7 @@ def get_raw_msg_before_timestamp_with_chat(chat_id: str, timestamp: float, limit
|
|||
"""
|
||||
filter_query = {"chat_id": chat_id, "time": {"$lt": timestamp}}
|
||||
sort_order = [("time", 1)]
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit)
|
||||
|
||||
|
||||
def get_raw_msg_before_timestamp_with_users(timestamp: float, person_ids: list, limit: int = 0) -> List[Dict[str, Any]]:
|
||||
|
|
@ -106,7 +106,7 @@ def get_raw_msg_before_timestamp_with_users(timestamp: float, person_ids: list,
|
|||
"""
|
||||
filter_query = {"time": {"$lt": timestamp}, "user_id": {"$in": person_ids}}
|
||||
sort_order = [("time", 1)]
|
||||
return find_messages(filter=filter_query, sort=sort_order, limit=limit)
|
||||
return find_messages(message_filter=filter_query, sort=sort_order, limit=limit)
|
||||
|
||||
|
||||
def num_new_messages_since(chat_id: str, timestamp_start: float = 0.0, timestamp_end: float = None) -> int:
|
||||
|
|
@ -123,7 +123,7 @@ def num_new_messages_since(chat_id: str, timestamp_start: float = 0.0, timestamp
|
|||
return 0 # 起始时间大于等于结束时间,没有新消息
|
||||
|
||||
filter_query = {"chat_id": chat_id, "time": {"$gt": timestamp_start, "$lt": _timestamp_end}}
|
||||
return count_messages(filter=filter_query)
|
||||
return count_messages(message_filter=filter_query)
|
||||
|
||||
|
||||
def num_new_messages_since_with_users(
|
||||
|
|
@ -137,7 +137,7 @@ def num_new_messages_since_with_users(
|
|||
"time": {"$gt": timestamp_start, "$lt": timestamp_end},
|
||||
"user_id": {"$in": person_ids},
|
||||
}
|
||||
return count_messages(filter=filter_query)
|
||||
return count_messages(message_filter=filter_query)
|
||||
|
||||
|
||||
async def _build_readable_messages_internal(
|
||||
|
|
@ -227,7 +227,7 @@ async def _build_readable_messages_internal(
|
|||
replace_content = "......(太长了)"
|
||||
|
||||
truncated_content = content
|
||||
if limit > 0 and original_len > limit:
|
||||
if 0 < limit < original_len:
|
||||
truncated_content = f"{content[:limit]}{replace_content}"
|
||||
|
||||
message_details.append((timestamp, name, truncated_content))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import re
|
|||
from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
from src.common.logger import get_module_logger
|
||||
|
||||
# import traceback
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
logger = get_module_logger("prompt_build")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ from time import perf_counter
|
|||
from functools import wraps
|
||||
from typing import Optional, Dict, Callable
|
||||
import asyncio
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
"""
|
||||
# 更好的计时器
|
||||
|
|
|
|||
|
|
@ -2,5 +2,23 @@ from .willing_manager import BaseWillingManager
|
|||
|
||||
|
||||
class CustomWillingManager(BaseWillingManager):
|
||||
async def async_task_starter(self) -> None:
|
||||
pass
|
||||
|
||||
async def before_generate_reply_handle(self, message_id: str):
|
||||
pass
|
||||
|
||||
async def after_generate_reply_handle(self, message_id: str):
|
||||
pass
|
||||
|
||||
async def not_reply_handle(self, message_id: str):
|
||||
pass
|
||||
|
||||
async def get_reply_probability(self, message_id: str):
|
||||
pass
|
||||
|
||||
async def bombing_buffer_message_handle(self, message_id: str):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ class DynamicWillingManager(BaseWillingManager):
|
|||
is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
|
||||
|
||||
# 获取当前模式的持续时间
|
||||
duration = 0
|
||||
if is_high_mode:
|
||||
duration = self.chat_high_willing_duration.get(chat_id, 180) # 默认3分钟
|
||||
else:
|
||||
|
|
@ -154,8 +153,6 @@ class DynamicWillingManager(BaseWillingManager):
|
|||
)
|
||||
|
||||
# 根据当前模式计算回复概率
|
||||
base_probability = 0.0
|
||||
|
||||
if in_conversation_context:
|
||||
# 在对话上下文中,降低基础回复概率
|
||||
base_probability = 0.5 if is_high_mode else 0.25
|
||||
|
|
|
|||
|
|
@ -76,10 +76,8 @@ class LlmcheckWillingManager(MxpWillingManager):
|
|||
|
||||
current_date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
current_time = time.strftime("%H:%M:%S", time.localtime())
|
||||
chat_talking_prompt = ""
|
||||
if chat_id:
|
||||
chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True)
|
||||
else:
|
||||
chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True)
|
||||
if not chat_id:
|
||||
return 0
|
||||
|
||||
# if is_mentioned_bot:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ from abc import ABC, abstractmethod
|
|||
import importlib
|
||||
from typing import Dict, Optional
|
||||
import asyncio
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
"""
|
||||
基类方法概览:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ from datetime import datetime
|
|||
from tqdm import tqdm
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True, extra_lines=3)
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
|
|
@ -15,6 +18,7 @@ sys.path.append(root_path)
|
|||
# 现在可以导入src模块
|
||||
from src.common.database import db # noqa E402
|
||||
|
||||
|
||||
# 加载根目录下的env.edv文件
|
||||
env_path = os.path.join(root_path, ".env")
|
||||
if not os.path.exists(env_path):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[inner]
|
||||
version = "1.6.0"
|
||||
version = "1.6.1"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
||||
#如果你想要修改配置文件,请在修改后将version的值进行变更
|
||||
|
|
@ -186,6 +186,7 @@ enable = true
|
|||
|
||||
[experimental] #实验性功能
|
||||
enable_friend_chat = false # 是否启用好友聊天
|
||||
talk_allowed_private = [] # 可以回复消息的QQ号
|
||||
pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
|
||||
|
||||
#下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env自定义的宏,使用自定义模型则选择定位相似的模型自己填写
|
||||
|
|
|
|||
Loading…
Reference in New Issue