Merge branch 'dev' of https://github.com/A0000Xz/MaiBot into dev
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ tool_call_benchmark.py
|
||||
run_maibot_core.bat
|
||||
run_napcat_adapter.bat
|
||||
run_ad.bat
|
||||
s4u.s4u
|
||||
llm_tool_benchmark_results.json
|
||||
MaiBot-Napcat-Adapter-main
|
||||
MaiBot-Napcat-Adapter
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.13.2-slim-bookworm
|
||||
FROM python:3.13.5-slim-bookworm
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# 工作目录
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
|
||||
## 🔥 更新和安装
|
||||
|
||||
|
||||
**最新版本: v0.8.1** ([更新日志](changelogs/changelog.md))
|
||||
|
||||
可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
|
||||
可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/tag/v0.1.0)下载最新启动器
|
||||
**GitHub 分支说明:**
|
||||
|
||||
1
bot.py
1
bot.py
@@ -314,6 +314,7 @@ if __name__ == "__main__":
|
||||
# Schedule tasks returns a future that runs forever.
|
||||
# We can run console_input_loop concurrently.
|
||||
main_tasks = loop.create_task(main_system.schedule_tasks())
|
||||
|
||||
# 仅在 TTY 中启用 console_input_loop
|
||||
if sys.stdin.isatty():
|
||||
logger.info("检测到终端环境,启用控制台输入循环")
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## [0.8.1] - 2025-6-27
|
||||
## [0.8.2] - 2025-7-5
|
||||
|
||||
优化和修复:
|
||||
|
||||
- 修复在auto模式下,私聊会转为normal的bug
|
||||
- 修复一般过滤次序问题
|
||||
- 优化normal_chat代码,采用和focus一致的关系构建
|
||||
- 优化计时信息和Log
|
||||
- 添加回复超时检查
|
||||
- normal的插件允许llm激活
|
||||
- 合并action激活器
|
||||
- emoji统一可选随机激活或llm激活
|
||||
- 移除observation和processor,简化focus的代码逻辑
|
||||
|
||||
## [0.8.1] - 2025-7-5
|
||||
|
||||
功能更新:
|
||||
|
||||
- normal现在和focus一样支持tool
|
||||
- focus现在和normal一样每次调用lpmm
|
||||
- 移除人格表达
|
||||
|
||||
优化和修复:
|
||||
|
||||
- 修复表情包配置无效问题
|
||||
- 合并normal和focus的prompt构建
|
||||
|
||||
|
||||
|
||||
- 非TTY环境禁用console_input_loop
|
||||
- 修复过滤消息仍被存储至数据库的问题
|
||||
- 私聊强制开启focus模式
|
||||
- 支持解析reply_to和at
|
||||
- 修复focus冷却时间导致的固定沉默
|
||||
- 移除豆包画图插件,此插件现在插件广场提供
|
||||
- 修复表达器无法读取原始文本
|
||||
- 修复normal planner没有超时退出问题
|
||||
|
||||
## [0.8.0] - 2025-6-27
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
services:
|
||||
adapters:
|
||||
container_name: maim-bot-adapters
|
||||
#### prod ####
|
||||
image: unclas/maimbot-adapter:latest
|
||||
# image: infinitycat/maimbot-adapter:latest
|
||||
#### dev ####
|
||||
# image: unclas/maimbot-adapter:dev
|
||||
# image: infinitycat/maimbot-adapter:dev
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# ports:
|
||||
# - "8095:8095"
|
||||
volumes:
|
||||
- ./docker-config/adapters/config.toml:/adapters/config.toml
|
||||
- ./docker-config/adapters/config.toml:/adapters/config.toml # 持久化adapters配置文件
|
||||
- ./data/adapters:/adapters/data # adapters 数据持久化
|
||||
restart: always
|
||||
networks:
|
||||
- maim_bot
|
||||
|
||||
core:
|
||||
container_name: maim-bot-core
|
||||
#### prod ####
|
||||
image: sengokucola/maibot:latest
|
||||
# image: infinitycat/maibot:latest
|
||||
# dev
|
||||
#### dev ####
|
||||
# image: sengokucola/maibot:dev
|
||||
# image: infinitycat/maibot:dev
|
||||
environment:
|
||||
@@ -25,15 +32,15 @@ services:
|
||||
# - PRIVACY_AGREE=42dddb3cbe2b784b45a2781407b298a1 # 同意EULA
|
||||
# ports:
|
||||
# - "8000:8000"
|
||||
# - "27017:27017"
|
||||
volumes:
|
||||
- ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件
|
||||
- ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件
|
||||
- ./data/MaiMBot/maibot_statistics.html:/MaiMBot/maibot_statistics.html #统计数据输出
|
||||
- ./data/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题
|
||||
- ./data/MaiMBot:/MaiMBot/data # 共享目录
|
||||
restart: always
|
||||
networks:
|
||||
- maim_bot
|
||||
|
||||
napcat:
|
||||
environment:
|
||||
- NAPCAT_UID=1000
|
||||
@@ -43,13 +50,14 @@ services:
|
||||
- "6099:6099"
|
||||
volumes:
|
||||
- ./docker-config/napcat:/app/napcat/config # 持久化napcat配置文件
|
||||
- ./data/qq:/app/.config/QQ # 持久化QQ本体并同步qq表情和图片到adapters
|
||||
- ./data/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题
|
||||
- ./data/qq:/app/.config/QQ # 持久化QQ本体
|
||||
- ./data/MaiMBot:/MaiMBot/data # 共享目录
|
||||
container_name: maim-bot-napcat
|
||||
restart: always
|
||||
image: mlikiowa/napcat-docker:latest
|
||||
networks:
|
||||
- maim_bot
|
||||
|
||||
sqlite-web:
|
||||
image: coleifer/sqlite-web
|
||||
container_name: sqlite-web
|
||||
@@ -62,6 +70,7 @@ services:
|
||||
- SQLITE_DATABASE=MaiMBot/MaiBot.db # 你的数据库文件
|
||||
networks:
|
||||
- maim_bot
|
||||
|
||||
networks:
|
||||
maim_bot:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from src.chat.heart_flow.heartflow import heartflow
|
||||
from src.chat.heart_flow.sub_heartflow import ChatState
|
||||
from src.common.logger import get_logger
|
||||
import time
|
||||
|
||||
logger = get_logger("api")
|
||||
|
||||
@@ -20,40 +19,6 @@ async def forced_change_subheartflow_status(subheartflow_id: str, status: ChatSt
|
||||
return False
|
||||
|
||||
|
||||
async def get_subheartflow_cycle_info(subheartflow_id: str, history_len: int) -> dict:
|
||||
"""获取子心流的循环信息"""
|
||||
subheartflow_cycle_info = await heartflow.api_get_subheartflow_cycle_info(subheartflow_id, history_len)
|
||||
logger.debug(f"子心流 {subheartflow_id} 循环信息: {subheartflow_cycle_info}")
|
||||
if subheartflow_cycle_info:
|
||||
return subheartflow_cycle_info
|
||||
else:
|
||||
logger.warning(f"子心流 {subheartflow_id} 循环信息未找到")
|
||||
return None
|
||||
|
||||
|
||||
async def get_normal_chat_replies(subheartflow_id: str, limit: int = 10) -> list:
|
||||
"""获取子心流的NormalChat回复记录
|
||||
|
||||
Args:
|
||||
subheartflow_id: 子心流ID
|
||||
limit: 最大返回数量,默认10条
|
||||
|
||||
Returns:
|
||||
list: 回复记录列表,如果未找到则返回空列表
|
||||
"""
|
||||
replies = await heartflow.api_get_normal_chat_replies(subheartflow_id, limit)
|
||||
logger.debug(f"子心流 {subheartflow_id} NormalChat回复记录: 获取到 {len(replies) if replies else 0} 条")
|
||||
if replies:
|
||||
# 格式化时间戳为可读时间
|
||||
for reply in replies:
|
||||
if "time" in reply:
|
||||
reply["formatted_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(reply["time"]))
|
||||
return replies
|
||||
else:
|
||||
logger.warning(f"子心流 {subheartflow_id} NormalChat回复记录未找到")
|
||||
return []
|
||||
|
||||
|
||||
async def get_all_states():
|
||||
"""获取所有状态"""
|
||||
all_states = await heartflow.api_get_all_states()
|
||||
|
||||
@@ -109,3 +109,4 @@ async def get_system_basic_info():
|
||||
def start_api_server():
|
||||
"""启动API服务器"""
|
||||
get_global_server().register_router(router, prefix="/api/v1")
|
||||
# pass
|
||||
|
||||
62
src/audio/mock_audio.py
Normal file
62
src/audio/mock_audio.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("MockAudio")
|
||||
|
||||
|
||||
class MockAudioPlayer:
|
||||
"""
|
||||
一个模拟的音频播放器,它会根据音频数据的"长度"来模拟播放时间。
|
||||
"""
|
||||
|
||||
def __init__(self, audio_data: bytes):
|
||||
self._audio_data = audio_data
|
||||
# 模拟音频时长:假设每 1024 字节代表 0.5 秒的音频
|
||||
self._duration = (len(audio_data) / 1024.0) * 0.5
|
||||
|
||||
async def play(self):
|
||||
"""模拟播放音频。该过程可以被中断。"""
|
||||
if self._duration <= 0:
|
||||
return
|
||||
logger.info(f"开始播放模拟音频,预计时长: {self._duration:.2f} 秒...")
|
||||
try:
|
||||
await asyncio.sleep(self._duration)
|
||||
logger.info("模拟音频播放完毕。")
|
||||
except asyncio.CancelledError:
|
||||
logger.info("音频播放被中断。")
|
||||
raise # 重新抛出异常,以便上层逻辑可以捕获它
|
||||
|
||||
|
||||
class MockAudioGenerator:
|
||||
"""
|
||||
一个模拟的文本到语音(TTS)生成器。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 模拟生成速度:每秒生成的字符数
|
||||
self.chars_per_second = 25.0
|
||||
|
||||
async def generate(self, text: str) -> bytes:
|
||||
"""
|
||||
模拟从文本生成音频数据。该过程可以被中断。
|
||||
|
||||
Args:
|
||||
text: 需要转换为音频的文本。
|
||||
|
||||
Returns:
|
||||
模拟的音频数据(bytes)。
|
||||
"""
|
||||
if not text:
|
||||
return b""
|
||||
|
||||
generation_time = len(text) / self.chars_per_second
|
||||
logger.info(f"模拟生成音频... 文本长度: {len(text)}, 预计耗时: {generation_time:.2f} 秒...")
|
||||
try:
|
||||
await asyncio.sleep(generation_time)
|
||||
# 生成虚拟的音频数据,其长度与文本长度成正比
|
||||
mock_audio_data = b"\x01\x02\x03" * (len(text) * 40)
|
||||
logger.info(f"模拟音频生成完毕,数据大小: {len(mock_audio_data) / 1024:.2f} KB。")
|
||||
return mock_audio_data
|
||||
except asyncio.CancelledError:
|
||||
logger.info("音频生成被中断。")
|
||||
raise # 重新抛出异常
|
||||
@@ -80,14 +80,16 @@ class ExpressionSelector:
|
||||
)
|
||||
|
||||
def get_random_expressions(
|
||||
self, chat_id: str, style_num: int, grammar_num: int, personality_num: int
|
||||
self, chat_id: str, total_num: int, style_percentage: float, grammar_percentage: float
|
||||
) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
|
||||
(
|
||||
learnt_style_expressions,
|
||||
learnt_grammar_expressions,
|
||||
personality_expressions,
|
||||
) = self.expression_learner.get_expression_by_chat_id(chat_id)
|
||||
|
||||
style_num = int(total_num * style_percentage)
|
||||
grammar_num = int(total_num * grammar_percentage)
|
||||
|
||||
# 按权重抽样(使用count作为权重)
|
||||
if learnt_style_expressions:
|
||||
style_weights = [expr.get("count", 1) for expr in learnt_style_expressions]
|
||||
@@ -101,13 +103,7 @@ class ExpressionSelector:
|
||||
else:
|
||||
selected_grammar = []
|
||||
|
||||
if personality_expressions:
|
||||
personality_weights = [expr.get("count", 1) for expr in personality_expressions]
|
||||
selected_personality = weighted_sample(personality_expressions, personality_weights, personality_num)
|
||||
else:
|
||||
selected_personality = []
|
||||
|
||||
return selected_style, selected_grammar, selected_personality
|
||||
return selected_style, selected_grammar
|
||||
|
||||
def update_expressions_count_batch(self, expressions_to_update: List[Dict[str, str]], increment: float = 0.1):
|
||||
"""对一批表达方式更新count值,按文件分组后一次性写入"""
|
||||
@@ -174,7 +170,7 @@ class ExpressionSelector:
|
||||
"""使用LLM选择适合的表达方式"""
|
||||
|
||||
# 1. 获取35个随机表达方式(现在按权重抽取)
|
||||
style_exprs, grammar_exprs, personality_exprs = self.get_random_expressions(chat_id, 25, 25, 10)
|
||||
style_exprs, grammar_exprs = self.get_random_expressions(chat_id, 50, 0.5, 0.5)
|
||||
|
||||
# 2. 构建所有表达方式的索引和情境列表
|
||||
all_expressions = []
|
||||
@@ -196,14 +192,6 @@ class ExpressionSelector:
|
||||
all_expressions.append(expr_with_type)
|
||||
all_situations.append(f"{len(all_expressions)}.{expr['situation']}")
|
||||
|
||||
# 添加personality表达方式
|
||||
for expr in personality_exprs:
|
||||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||||
expr_with_type = expr.copy()
|
||||
expr_with_type["type"] = "style_personality"
|
||||
all_expressions.append(expr_with_type)
|
||||
all_situations.append(f"{len(all_expressions)}.{expr['situation']}")
|
||||
|
||||
if not all_expressions:
|
||||
logger.warning("没有找到可用的表达方式")
|
||||
return []
|
||||
@@ -260,7 +248,7 @@ class ExpressionSelector:
|
||||
|
||||
# 对选中的所有表达方式,一次性更新count数
|
||||
if valid_expressions:
|
||||
self.update_expressions_count_batch(valid_expressions, 0.003)
|
||||
self.update_expressions_count_batch(valid_expressions, 0.006)
|
||||
|
||||
# logger.info(f"LLM从{len(all_expressions)}个情境中选择了{len(valid_expressions)}个")
|
||||
return valid_expressions
|
||||
|
||||
@@ -29,7 +29,7 @@ def init_prompt() -> None:
|
||||
4. 思考有没有特殊的梗,一并总结成语言风格
|
||||
5. 例子仅供参考,请严格根据群聊内容总结!!!
|
||||
注意:总结成如下格式的规律,总结的内容要详细,但具有概括性:
|
||||
当"xxxxxx"时,可以"xxxxxx", xxxxxx不超过20个字,为特定句式或表达
|
||||
例如:当"AAAAA"时,可以"BBBBB", AAAAA代表某个具体的场景,不超过20个字。BBBBB代表对应的语言风格,特定句式或表达方式,不超过20个字。
|
||||
|
||||
例如:
|
||||
当"对某件事表示十分惊叹,有些意外"时,使用"我嘞个xxxx"
|
||||
@@ -69,21 +69,18 @@ class ExpressionLearner:
|
||||
# TODO: API-Adapter修改标记
|
||||
self.express_learn_model: LLMRequest = LLMRequest(
|
||||
model=global_config.model.replyer_1,
|
||||
temperature=0.2,
|
||||
temperature=0.3,
|
||||
request_type="expressor.learner",
|
||||
)
|
||||
self.llm_model = None
|
||||
|
||||
def get_expression_by_chat_id(
|
||||
self, chat_id: str
|
||||
) -> Tuple[List[Dict[str, str]], List[Dict[str, str]], List[Dict[str, str]]]:
|
||||
def get_expression_by_chat_id(self, chat_id: str) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
|
||||
"""
|
||||
获取指定chat_id的style和grammar表达方式, 同时获取全局的personality表达方式
|
||||
获取指定chat_id的style和grammar表达方式
|
||||
返回的每个表达方式字典中都包含了source_id, 用于后续的更新操作
|
||||
"""
|
||||
learnt_style_expressions = []
|
||||
learnt_grammar_expressions = []
|
||||
personality_expressions = []
|
||||
|
||||
# 获取style表达方式
|
||||
style_dir = os.path.join("data", "expression", "learnt_style", str(chat_id))
|
||||
@@ -111,19 +108,7 @@ class ExpressionLearner:
|
||||
except Exception as e:
|
||||
logger.error(f"读取grammar表达方式失败: {e}")
|
||||
|
||||
# 获取personality表达方式
|
||||
personality_file = os.path.join("data", "expression", "personality", "expressions.json")
|
||||
if os.path.exists(personality_file):
|
||||
try:
|
||||
with open(personality_file, "r", encoding="utf-8") as f:
|
||||
expressions = json.load(f)
|
||||
for expr in expressions:
|
||||
expr["source_id"] = "personality" # 添加来源ID
|
||||
personality_expressions.append(expr)
|
||||
except Exception as e:
|
||||
logger.error(f"读取personality表达方式失败: {e}")
|
||||
|
||||
return learnt_style_expressions, learnt_grammar_expressions, personality_expressions
|
||||
return learnt_style_expressions, learnt_grammar_expressions
|
||||
|
||||
def is_similar(self, s1: str, s2: str) -> bool:
|
||||
"""
|
||||
@@ -428,6 +413,7 @@ class ExpressionLearner:
|
||||
|
||||
init_prompt()
|
||||
|
||||
|
||||
expression_learner = None
|
||||
|
||||
|
||||
|
||||
@@ -2,24 +2,20 @@
|
||||
# 外部世界可以是某个聊天 不同平台的聊天 也可以是任意媒体
|
||||
from datetime import datetime
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail
|
||||
from src.chat.focus_chat.hfc_utils import CycleDetail
|
||||
from typing import List
|
||||
# Import the new utility function
|
||||
|
||||
logger = get_logger("observation")
|
||||
logger = get_logger("loop_info")
|
||||
|
||||
|
||||
# 所有观察的基类
|
||||
class HFCloopObservation:
|
||||
class FocusLoopInfo:
|
||||
def __init__(self, observe_id):
|
||||
self.observe_info = ""
|
||||
self.observe_id = observe_id
|
||||
self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间
|
||||
self.history_loop: List[CycleDetail] = []
|
||||
|
||||
def get_observe_info(self):
|
||||
return self.observe_info
|
||||
|
||||
def add_loop_info(self, loop_info: CycleDetail):
|
||||
self.history_loop.append(loop_info)
|
||||
|
||||
@@ -50,11 +46,6 @@ class HFCloopObservation:
|
||||
action_taken_time_str = (
|
||||
datetime.fromtimestamp(action_taken_time).strftime("%H:%M:%S") if action_taken_time > 0 else "未知时间"
|
||||
)
|
||||
# print(action_type)
|
||||
# print(action_reasoning)
|
||||
# print(is_taken)
|
||||
# print(action_taken_time_str)
|
||||
# print("--------------------------------")
|
||||
if action_reasoning != cycle_last_reason:
|
||||
cycle_last_reason = action_reasoning
|
||||
action_reasoning_str = f"你选择这个action的原因是:{action_reasoning}"
|
||||
@@ -71,9 +62,6 @@ class HFCloopObservation:
|
||||
else:
|
||||
action_detailed_str += f"{action_taken_time_str}时,你选择回复(action:{action_type},内容是:'{response_text}'),但是动作失败了。{action_reasoning_str}\n"
|
||||
elif action_type == "no_reply":
|
||||
# action_detailed_str += (
|
||||
# f"{action_taken_time_str}时,你选择不回复(action:{action_type}),{action_reasoning_str}\n"
|
||||
# )
|
||||
pass
|
||||
else:
|
||||
if is_taken:
|
||||
@@ -88,18 +76,6 @@ class HFCloopObservation:
|
||||
else:
|
||||
cycle_info_block = "\n"
|
||||
|
||||
# 根据连续文本回复的数量构建提示信息
|
||||
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]}"),请注意'
|
||||
|
||||
# 包装提示块,增加可读性,即使没有连续回复也给个标记
|
||||
# if cycle_info_block:
|
||||
# cycle_info_block = f"\n你最近的回复\n{cycle_info_block}\n"
|
||||
# else:
|
||||
# cycle_info_block = "\n"
|
||||
|
||||
# 获取history_loop中最新添加的
|
||||
if self.history_loop:
|
||||
last_loop = self.history_loop[0]
|
||||
@@ -113,16 +89,3 @@ class HFCloopObservation:
|
||||
cycle_info_block += f"距离你上一次阅读消息并思考和规划,已经过去了{time_diff}秒\n"
|
||||
else:
|
||||
cycle_info_block += "你还没看过消息\n"
|
||||
|
||||
self.observe_info = cycle_info_block
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将观察对象转换为可序列化的字典"""
|
||||
# 只序列化基本信息,避免循环引用
|
||||
return {
|
||||
"observe_info": self.observe_info,
|
||||
"observe_id": self.observe_id,
|
||||
"last_observe_time": self.last_observe_time,
|
||||
# 不序列化history_loop,避免循环引用
|
||||
"history_loop_count": len(self.history_loop),
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import time
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
from src.common.logger import get_logger
|
||||
import json
|
||||
|
||||
logger = get_logger("hfc") # Logger Name Changed
|
||||
|
||||
log_dir = "log/log_cycle_debug/"
|
||||
|
||||
|
||||
class CycleDetail:
|
||||
"""循环信息记录类"""
|
||||
|
||||
def __init__(self, cycle_id: int):
|
||||
self.cycle_id = cycle_id
|
||||
self.prefix = ""
|
||||
self.thinking_id = ""
|
||||
self.start_time = time.time()
|
||||
self.end_time: Optional[float] = None
|
||||
self.timers: Dict[str, float] = {}
|
||||
|
||||
# 新字段
|
||||
self.loop_observation_info: Dict[str, Any] = {}
|
||||
self.loop_processor_info: Dict[str, Any] = {} # 前处理器信息
|
||||
self.loop_plan_info: Dict[str, Any] = {}
|
||||
self.loop_action_info: Dict[str, Any] = {}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""将循环信息转换为字典格式"""
|
||||
|
||||
def convert_to_serializable(obj, depth=0, seen=None):
|
||||
if seen is None:
|
||||
seen = set()
|
||||
|
||||
# 防止递归过深
|
||||
if depth > 5: # 降低递归深度限制
|
||||
return str(obj)
|
||||
|
||||
# 防止循环引用
|
||||
obj_id = id(obj)
|
||||
if obj_id in seen:
|
||||
return str(obj)
|
||||
seen.add(obj_id)
|
||||
|
||||
try:
|
||||
if hasattr(obj, "to_dict"):
|
||||
# 对于有to_dict方法的对象,直接调用其to_dict方法
|
||||
return obj.to_dict()
|
||||
elif isinstance(obj, dict):
|
||||
# 对于字典,只保留基本类型和可序列化的值
|
||||
return {
|
||||
k: convert_to_serializable(v, depth + 1, seen)
|
||||
for k, v in obj.items()
|
||||
if isinstance(k, (str, int, float, bool))
|
||||
}
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
# 对于列表和元组,只保留可序列化的元素
|
||||
return [
|
||||
convert_to_serializable(item, depth + 1, seen)
|
||||
for item in obj
|
||||
if not isinstance(item, (dict, list, tuple))
|
||||
or isinstance(item, (str, int, float, bool, type(None)))
|
||||
]
|
||||
elif isinstance(obj, (str, int, float, bool, type(None))):
|
||||
return obj
|
||||
else:
|
||||
return str(obj)
|
||||
finally:
|
||||
seen.remove(obj_id)
|
||||
|
||||
return {
|
||||
"cycle_id": self.cycle_id,
|
||||
"start_time": self.start_time,
|
||||
"end_time": self.end_time,
|
||||
"timers": self.timers,
|
||||
"thinking_id": self.thinking_id,
|
||||
"loop_observation_info": convert_to_serializable(self.loop_observation_info),
|
||||
"loop_processor_info": convert_to_serializable(self.loop_processor_info),
|
||||
"loop_plan_info": convert_to_serializable(self.loop_plan_info),
|
||||
"loop_action_info": convert_to_serializable(self.loop_action_info),
|
||||
}
|
||||
|
||||
def complete_cycle(self):
|
||||
"""完成循环,记录结束时间"""
|
||||
self.end_time = time.time()
|
||||
|
||||
# 处理 prefix,只保留中英文字符和基本标点
|
||||
if not self.prefix:
|
||||
self.prefix = "group"
|
||||
else:
|
||||
# 只保留中文、英文字母、数字和基本标点
|
||||
allowed_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
|
||||
self.prefix = (
|
||||
"".join(char for char in self.prefix if "\u4e00" <= char <= "\u9fff" or char in allowed_chars)
|
||||
or "group"
|
||||
)
|
||||
|
||||
# current_time_minute = time.strftime("%Y%m%d_%H%M", time.localtime())
|
||||
|
||||
# try:
|
||||
# self.log_cycle_to_file(
|
||||
# log_dir + self.prefix + f"/{current_time_minute}_cycle_" + str(self.cycle_id) + ".json"
|
||||
# )
|
||||
# except Exception as e:
|
||||
# logger.warning(f"写入文件日志,可能是群名称包含非法字符: {e}")
|
||||
|
||||
def log_cycle_to_file(self, file_path: str):
|
||||
"""将循环信息写入文件"""
|
||||
# 如果目录不存在,则创建目
|
||||
dir_name = os.path.dirname(file_path)
|
||||
# 去除特殊字符,保留字母、数字、下划线、中划线和中文
|
||||
dir_name = "".join(
|
||||
char for char in dir_name if char.isalnum() or char in ["_", "-", "/"] or "\u4e00" <= char <= "\u9fff"
|
||||
)
|
||||
# print("dir_name:", dir_name)
|
||||
if dir_name and not os.path.exists(dir_name):
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
# 写入文件
|
||||
|
||||
file_path = os.path.join(dir_name, os.path.basename(file_path))
|
||||
# print("file_path:", file_path)
|
||||
with open(file_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(self.to_dict(), ensure_ascii=False) + "\n")
|
||||
|
||||
def set_thinking_id(self, thinking_id: str):
|
||||
"""设置思考消息ID"""
|
||||
self.thinking_id = thinking_id
|
||||
|
||||
def set_loop_info(self, loop_info: Dict[str, Any]):
|
||||
"""设置循环信息"""
|
||||
self.loop_observation_info = loop_info["loop_observation_info"]
|
||||
self.loop_processor_info = loop_info["loop_processor_info"]
|
||||
self.loop_plan_info = loop_info["loop_plan_info"]
|
||||
self.loop_action_info = loop_info["loop_action_info"]
|
||||
@@ -9,66 +9,23 @@ from rich.traceback import install
|
||||
from src.chat.utils.prompt_builder import global_prompt_manager
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.utils.timer_calculator import Timer
|
||||
from src.chat.heart_flow.observation.observation import Observation
|
||||
from src.chat.focus_chat.heartFC_Cycleinfo import CycleDetail
|
||||
from src.chat.focus_chat.info.info_base import InfoBase
|
||||
from src.chat.focus_chat.info_processors.chattinginfo_processor import ChattingInfoProcessor
|
||||
from src.chat.focus_chat.info_processors.working_memory_processor import WorkingMemoryProcessor
|
||||
from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation
|
||||
from src.chat.heart_flow.observation.working_observation import WorkingMemoryObservation
|
||||
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
|
||||
from src.chat.heart_flow.observation.actions_observation import ActionObservation
|
||||
|
||||
from src.chat.focus_chat.memory_activator import MemoryActivator
|
||||
from src.chat.focus_chat.info_processors.base_processor import BaseProcessor
|
||||
from src.chat.focus_chat.planners.planner_factory import PlannerFactory
|
||||
from src.chat.focus_chat.planners.modify_actions import ActionModifier
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.chat.focus_chat.focus_loop_info import FocusLoopInfo
|
||||
from src.chat.planner_actions.planner import ActionPlanner
|
||||
from src.chat.planner_actions.action_modifier import ActionModifier
|
||||
from src.chat.planner_actions.action_manager import ActionManager
|
||||
from src.config.config import global_config
|
||||
from src.chat.focus_chat.hfc_performance_logger import HFCPerformanceLogger
|
||||
from src.chat.focus_chat.hfc_version_manager import get_hfc_version
|
||||
from src.person_info.relationship_builder_manager import relationship_builder_manager
|
||||
from src.chat.focus_chat.hfc_utils import CycleDetail
|
||||
|
||||
|
||||
install(extra_lines=3)
|
||||
|
||||
# 注释:原来的动作修改超时常量已移除,因为改为顺序执行
|
||||
|
||||
# 定义观察器映射:键是观察器名称,值是 (观察器类, 初始化参数)
|
||||
OBSERVATION_CLASSES = {
|
||||
"ChattingObservation": (ChattingObservation, "chat_id"),
|
||||
"WorkingMemoryObservation": (WorkingMemoryObservation, "observe_id"),
|
||||
"HFCloopObservation": (HFCloopObservation, "observe_id"),
|
||||
}
|
||||
|
||||
# 定义处理器映射:键是处理器名称,值是 (处理器类, 可选的配置键名)
|
||||
PROCESSOR_CLASSES = {
|
||||
"ChattingInfoProcessor": (ChattingInfoProcessor, None),
|
||||
"WorkingMemoryProcessor": (WorkingMemoryProcessor, "working_memory_processor"),
|
||||
}
|
||||
|
||||
logger = get_logger("hfc") # Logger Name Changed
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
管理一个连续的Focus Chat循环
|
||||
@@ -80,7 +37,6 @@ class HeartFChatting:
|
||||
self,
|
||||
chat_id: str,
|
||||
on_stop_focus_chat: Optional[Callable[[], Awaitable[None]]] = None,
|
||||
performance_version: str = None,
|
||||
):
|
||||
"""
|
||||
HeartFChatting 初始化函数
|
||||
@@ -95,8 +51,6 @@ class HeartFChatting:
|
||||
self.chat_stream = get_chat_manager().get_stream(self.stream_id)
|
||||
self.log_prefix = f"[{get_chat_manager().get_stream_name(self.stream_id) or self.stream_id}]"
|
||||
|
||||
self.memory_activator = MemoryActivator()
|
||||
|
||||
self.relationship_builder = relationship_builder_manager.get_or_create_builder(self.stream_id)
|
||||
|
||||
# 新增:消息计数器和疲惫阈值
|
||||
@@ -106,31 +60,11 @@ class HeartFChatting:
|
||||
self._message_threshold = max(10, int(30 * global_config.chat.exit_focus_threshold))
|
||||
self._fatigue_triggered = False # 是否已触发疲惫退出
|
||||
|
||||
# 初始化观察器
|
||||
self.observations: List[Observation] = []
|
||||
self._register_observations()
|
||||
|
||||
# 根据配置文件和默认规则确定启用的处理器
|
||||
config_processor_settings = global_config.focus_chat_processor
|
||||
self.enabled_processor_names = []
|
||||
|
||||
for proc_name, (_proc_class, config_key) in PROCESSOR_CLASSES.items():
|
||||
# 检查处理器是否应该启用
|
||||
if not config_key or getattr(config_processor_settings, config_key, True):
|
||||
self.enabled_processor_names.append(proc_name)
|
||||
|
||||
# logger.info(f"{self.log_prefix} 将启用的处理器: {self.enabled_processor_names}")
|
||||
|
||||
self.processors: List[BaseProcessor] = []
|
||||
self._register_default_processors()
|
||||
self.loop_info: FocusLoopInfo = FocusLoopInfo(observe_id=self.stream_id)
|
||||
|
||||
self.action_manager = ActionManager()
|
||||
self.action_planner = PlannerFactory.create_planner(
|
||||
log_prefix=self.log_prefix, action_manager=self.action_manager
|
||||
)
|
||||
self.action_modifier = ActionModifier(action_manager=self.action_manager)
|
||||
self.action_observation = ActionObservation(observe_id=self.stream_id)
|
||||
self.action_observation.set_action_manager(self.action_manager)
|
||||
self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager)
|
||||
self.action_modifier = ActionModifier(action_manager=self.action_manager, chat_id=self.stream_id)
|
||||
|
||||
self._processing_lock = asyncio.Lock()
|
||||
|
||||
@@ -147,77 +81,20 @@ class HeartFChatting:
|
||||
# 存储回调函数
|
||||
self.on_stop_focus_chat = on_stop_focus_chat
|
||||
|
||||
self.reply_timeout_count = 0
|
||||
self.plan_timeout_count = 0
|
||||
|
||||
# 初始化性能记录器
|
||||
# 如果没有指定版本号,则使用全局版本管理器的版本号
|
||||
actual_version = performance_version or get_hfc_version()
|
||||
self.performance_logger = HFCPerformanceLogger(chat_id, actual_version)
|
||||
|
||||
self.performance_logger = HFCPerformanceLogger(chat_id)
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix} HeartFChatting 初始化完成,消息疲惫阈值: {self._message_threshold}条(基于exit_focus_threshold={global_config.chat.exit_focus_threshold}计算,仅在auto模式下生效)"
|
||||
)
|
||||
|
||||
def _register_observations(self):
|
||||
"""注册所有观察器"""
|
||||
self.observations = [] # 清空已有的
|
||||
|
||||
for name, (observation_class, param_name) in OBSERVATION_CLASSES.items():
|
||||
try:
|
||||
# 检查是否需要跳过WorkingMemoryObservation
|
||||
if name == "WorkingMemoryObservation":
|
||||
# 如果工作记忆处理器被禁用,则跳过WorkingMemoryObservation
|
||||
if not global_config.focus_chat.working_memory_processor:
|
||||
logger.debug(f"{self.log_prefix} 工作记忆处理器已禁用,跳过注册观察器 {name}")
|
||||
continue
|
||||
|
||||
# 根据参数名使用正确的参数
|
||||
kwargs = {param_name: self.stream_id}
|
||||
observation = observation_class(**kwargs)
|
||||
self.observations.append(observation)
|
||||
logger.debug(f"{self.log_prefix} 注册观察器 {name}")
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 观察器 {name} 构造失败: {e}")
|
||||
|
||||
if self.observations:
|
||||
logger.info(f"{self.log_prefix} 已注册观察器: {[o.__class__.__name__ for o in self.observations]}")
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 没有注册任何观察器")
|
||||
|
||||
def _register_default_processors(self):
|
||||
"""根据 self.enabled_processor_names 注册信息处理器"""
|
||||
self.processors = [] # 清空已有的
|
||||
|
||||
for name in self.enabled_processor_names: # 'name' is "ChattingInfoProcessor", etc.
|
||||
processor_info = PROCESSOR_CLASSES.get(name) # processor_info is (ProcessorClass, config_key)
|
||||
if processor_info:
|
||||
processor_actual_class = processor_info[0] # 获取实际的类定义
|
||||
# 根据处理器类名判断构造参数
|
||||
if name == "ChattingInfoProcessor":
|
||||
self.processors.append(processor_actual_class())
|
||||
elif name == "WorkingMemoryProcessor":
|
||||
self.processors.append(processor_actual_class(subheartflow_id=self.stream_id))
|
||||
else:
|
||||
# 对于PROCESSOR_CLASSES中定义但此处未明确处理构造的处理器
|
||||
try:
|
||||
self.processors.append(processor_actual_class()) # 尝试无参构造
|
||||
logger.debug(f"{self.log_prefix} 注册处理器 {name} (尝试无参构造).")
|
||||
except TypeError:
|
||||
logger.error(
|
||||
f"{self.log_prefix} 处理器 {name} 构造失败。它可能需要参数(如 subheartflow_id)但未在注册逻辑中明确处理。"
|
||||
)
|
||||
else:
|
||||
# 这理论上不应该发生,因为 enabled_processor_names 是从 PROCESSOR_CLASSES 的键生成的
|
||||
logger.warning(
|
||||
f"{self.log_prefix} 在 PROCESSOR_CLASSES 中未找到名为 '{name}' 的处理器定义,将跳过注册。"
|
||||
)
|
||||
|
||||
if self.processors:
|
||||
logger.info(f"{self.log_prefix} 已注册处理器: {[p.__class__.__name__ for p in self.processors]}")
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 没有注册任何处理器。这可能是由于配置错误或所有处理器都被禁用了。")
|
||||
|
||||
async def start(self):
|
||||
"""检查是否需要启动主循环,如果未激活则启动。"""
|
||||
logger.debug(f"{self.log_prefix} 开始启动 HeartFChatting")
|
||||
|
||||
# 如果循环已经激活,直接返回
|
||||
if self._loop_active:
|
||||
@@ -238,8 +115,6 @@ class HeartFChatting:
|
||||
try:
|
||||
# 等待旧任务确实被取消
|
||||
await asyncio.wait_for(self._loop_task, timeout=5.0)
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
pass # 忽略取消或超时错误
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.log_prefix} 等待旧任务取消时出错: {e}")
|
||||
self._loop_task = None # 清理旧任务引用
|
||||
@@ -292,7 +167,6 @@ class HeartFChatting:
|
||||
|
||||
# 初始化周期状态
|
||||
cycle_timers = {}
|
||||
loop_cycle_start_time = time.monotonic()
|
||||
|
||||
# 执行规划和处理阶段
|
||||
try:
|
||||
@@ -315,6 +189,7 @@ class HeartFChatting:
|
||||
|
||||
if loop_info["loop_action_info"]["command"] == "stop_focus_chat":
|
||||
logger.info(f"{self.log_prefix} 麦麦决定停止专注聊天")
|
||||
|
||||
# 如果设置了回调函数,则调用它
|
||||
if self.on_stop_focus_chat:
|
||||
try:
|
||||
@@ -332,14 +207,11 @@ class HeartFChatting:
|
||||
logger.error(f"{self.log_prefix} 处理上下文时出错: {e}")
|
||||
# 为当前循环设置错误状态,防止后续重复报错
|
||||
error_loop_info = {
|
||||
"loop_observation_info": {},
|
||||
"loop_processor_info": {},
|
||||
"loop_plan_info": {
|
||||
"action_result": {
|
||||
"action_type": "error",
|
||||
"action_data": {},
|
||||
},
|
||||
"observed_messages": "",
|
||||
},
|
||||
"loop_action_info": {
|
||||
"action_taken": False,
|
||||
@@ -357,22 +229,10 @@ class HeartFChatting:
|
||||
|
||||
self._current_cycle_detail.set_loop_info(loop_info)
|
||||
|
||||
# 从observations列表中获取HFCloopObservation
|
||||
hfcloop_observation = next(
|
||||
(obs for obs in self.observations if isinstance(obs, HFCloopObservation)), None
|
||||
)
|
||||
if hfcloop_observation:
|
||||
hfcloop_observation.add_loop_info(self._current_cycle_detail)
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 未找到HFCloopObservation实例")
|
||||
self.loop_info.add_loop_info(self._current_cycle_detail)
|
||||
|
||||
self._current_cycle_detail.timers = cycle_timers
|
||||
|
||||
# 防止循环过快消耗资源
|
||||
await _handle_cycle_delay(
|
||||
loop_info["loop_action_info"]["action_taken"], loop_cycle_start_time, self.log_prefix
|
||||
)
|
||||
|
||||
# 完成当前循环并保存历史
|
||||
self._current_cycle_detail.complete_cycle()
|
||||
self._cycle_history.append(self._current_cycle_detail)
|
||||
@@ -383,24 +243,11 @@ class HeartFChatting:
|
||||
formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
|
||||
timer_strings.append(f"{name}: {formatted_time}")
|
||||
|
||||
# 新增:输出每个处理器的耗时
|
||||
processor_time_costs = self._current_cycle_detail.loop_processor_info.get(
|
||||
"processor_time_costs", {}
|
||||
)
|
||||
processor_time_strings = []
|
||||
for pname, ptime in processor_time_costs.items():
|
||||
formatted_ptime = f"{ptime * 1000:.2f}毫秒" if ptime < 1 else f"{ptime:.2f}秒"
|
||||
processor_time_strings.append(f"{pname}: {formatted_ptime}")
|
||||
processor_time_log = (
|
||||
("\n前处理器耗时: " + "; ".join(processor_time_strings)) if processor_time_strings else ""
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix} 第{self._current_cycle_detail.cycle_id}次思考,"
|
||||
f"耗时: {self._current_cycle_detail.end_time - self._current_cycle_detail.start_time:.1f}秒, "
|
||||
f"动作: {self._current_cycle_detail.loop_plan_info.get('action_result', {}).get('action_type', '未知动作')}"
|
||||
f"选择动作: {self._current_cycle_detail.loop_plan_info.get('action_result', {}).get('action_type', '未知动作')}"
|
||||
+ (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "")
|
||||
+ processor_time_log
|
||||
)
|
||||
|
||||
# 记录性能数据
|
||||
@@ -411,7 +258,6 @@ class HeartFChatting:
|
||||
"action_type": action_result.get("action_type", "unknown"),
|
||||
"total_time": self._current_cycle_detail.end_time - self._current_cycle_detail.start_time,
|
||||
"step_times": cycle_timers.copy(),
|
||||
"processor_time_costs": processor_time_costs, # 处理器时间
|
||||
"reasoning": action_result.get("reasoning", ""),
|
||||
"success": self._current_cycle_detail.loop_action_info.get("action_taken", False),
|
||||
}
|
||||
@@ -431,15 +277,12 @@ class HeartFChatting:
|
||||
# 如果_current_cycle_detail存在但未完成,为其设置错误状态
|
||||
if self._current_cycle_detail and not hasattr(self._current_cycle_detail, "end_time"):
|
||||
error_loop_info = {
|
||||
"loop_observation_info": {},
|
||||
"loop_processor_info": {},
|
||||
"loop_plan_info": {
|
||||
"action_result": {
|
||||
"action_type": "error",
|
||||
"action_data": {},
|
||||
"reasoning": f"循环处理失败: {e}",
|
||||
},
|
||||
"observed_messages": "",
|
||||
},
|
||||
"loop_action_info": {
|
||||
"action_taken": False,
|
||||
@@ -485,85 +328,10 @@ class HeartFChatting:
|
||||
if acquired and self._processing_lock.locked():
|
||||
self._processing_lock.release()
|
||||
|
||||
async def _process_processors(self, observations: List[Observation]) -> tuple[List[InfoBase], Dict[str, float]]:
|
||||
# 记录并行任务开始时间
|
||||
parallel_start_time = time.time()
|
||||
logger.debug(f"{self.log_prefix} 开始信息处理器并行任务")
|
||||
|
||||
processor_tasks = []
|
||||
task_to_name_map = {}
|
||||
processor_time_costs = {} # 新增: 记录每个处理器耗时
|
||||
|
||||
for processor in self.processors:
|
||||
processor_name = processor.__class__.log_prefix
|
||||
|
||||
async def run_with_timeout(proc=processor):
|
||||
return await asyncio.wait_for(proc.process_info(observations=observations), 30)
|
||||
|
||||
task = asyncio.create_task(run_with_timeout())
|
||||
|
||||
processor_tasks.append(task)
|
||||
task_to_name_map[task] = processor_name
|
||||
logger.debug(f"{self.log_prefix} 启动处理器任务: {processor_name}")
|
||||
|
||||
pending_tasks = set(processor_tasks)
|
||||
all_plan_info: List[InfoBase] = []
|
||||
|
||||
while pending_tasks:
|
||||
done, pending_tasks = await asyncio.wait(pending_tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
for task in done:
|
||||
processor_name = task_to_name_map[task]
|
||||
task_completed_time = time.time()
|
||||
duration_since_parallel_start = task_completed_time - parallel_start_time
|
||||
|
||||
try:
|
||||
result_list = await task
|
||||
logger.info(f"{self.log_prefix} 处理器 {processor_name} 已完成!")
|
||||
if result_list is not None:
|
||||
all_plan_info.extend(result_list)
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 处理器 {processor_name} 返回了 None")
|
||||
# 记录耗时
|
||||
processor_time_costs[processor_name] = duration_since_parallel_start
|
||||
except asyncio.TimeoutError:
|
||||
logger.info(f"{self.log_prefix} 处理器 {processor_name} 超时(>30s),已跳过")
|
||||
processor_time_costs[processor_name] = 30
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"{self.log_prefix} 处理器 {processor_name} 执行失败,耗时 (自并行开始): {duration_since_parallel_start:.2f}秒. 错误: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
traceback.print_exc()
|
||||
processor_time_costs[processor_name] = duration_since_parallel_start
|
||||
|
||||
if pending_tasks:
|
||||
current_progress_time = time.time()
|
||||
elapsed_for_log = current_progress_time - parallel_start_time
|
||||
pending_names_for_log = [task_to_name_map[t] for t in pending_tasks]
|
||||
logger.info(
|
||||
f"{self.log_prefix} 信息处理已进行 {elapsed_for_log:.2f}秒,待完成任务: {', '.join(pending_names_for_log)}"
|
||||
)
|
||||
|
||||
# 所有任务完成后的最终日志
|
||||
parallel_end_time = time.time()
|
||||
total_duration = parallel_end_time - parallel_start_time
|
||||
logger.info(f"{self.log_prefix} 所有处理器任务全部完成,总耗时: {total_duration:.2f}秒")
|
||||
# logger.debug(f"{self.log_prefix} 所有信息处理器处理后的信息: {all_plan_info}")
|
||||
|
||||
return all_plan_info, processor_time_costs
|
||||
|
||||
async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> dict:
|
||||
try:
|
||||
loop_start_time = time.time()
|
||||
with Timer("观察", cycle_timers):
|
||||
# 执行所有观察器的观察
|
||||
for observation in self.observations:
|
||||
await observation.observe()
|
||||
|
||||
loop_observation_info = {
|
||||
"observations": self.observations,
|
||||
}
|
||||
await self.loop_info.observe()
|
||||
|
||||
await self.relationship_builder.build_relation()
|
||||
|
||||
@@ -573,39 +341,18 @@ class HeartFChatting:
|
||||
try:
|
||||
# 调用完整的动作修改流程
|
||||
await self.action_modifier.modify_actions(
|
||||
observations=self.observations,
|
||||
loop_info=self.loop_info,
|
||||
mode="focus",
|
||||
)
|
||||
|
||||
await self.action_observation.observe()
|
||||
self.observations.append(self.action_observation)
|
||||
logger.debug(f"{self.log_prefix} 动作修改完成")
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 动作修改失败: {e}")
|
||||
# 继续执行,不中断流程
|
||||
|
||||
# 第二步:信息处理器
|
||||
with Timer("信息处理器", cycle_timers):
|
||||
try:
|
||||
all_plan_info, processor_time_costs = await self._process_processors(self.observations)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 信息处理器失败: {e}")
|
||||
# 设置默认值以继续执行
|
||||
all_plan_info = []
|
||||
processor_time_costs = {}
|
||||
|
||||
loop_processor_info = {
|
||||
"all_plan_info": all_plan_info,
|
||||
"processor_time_costs": processor_time_costs,
|
||||
}
|
||||
|
||||
logger.debug(f"{self.log_prefix} 并行阶段完成,准备进入规划器,plan_info数量: {len(all_plan_info)}")
|
||||
|
||||
with Timer("规划器", cycle_timers):
|
||||
plan_result = await self.action_planner.plan(all_plan_info, self.observations, loop_start_time)
|
||||
plan_result = await self.action_planner.plan()
|
||||
|
||||
loop_plan_info = {
|
||||
"action_result": plan_result.get("action_result", {}),
|
||||
"observed_messages": plan_result.get("observed_messages", ""),
|
||||
}
|
||||
|
||||
action_type, action_data, reasoning = (
|
||||
@@ -614,6 +361,8 @@ class HeartFChatting:
|
||||
plan_result.get("action_result", {}).get("reasoning", "未提供理由"),
|
||||
)
|
||||
|
||||
action_data["loop_start_time"] = loop_start_time
|
||||
|
||||
if action_type == "reply":
|
||||
action_str = "回复"
|
||||
elif action_type == "no_reply":
|
||||
@@ -621,7 +370,7 @@ class HeartFChatting:
|
||||
else:
|
||||
action_str = action_type
|
||||
|
||||
logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}'")
|
||||
logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}',理由是:{reasoning}")
|
||||
|
||||
# 动作执行计时
|
||||
with Timer("动作执行", cycle_timers):
|
||||
@@ -637,8 +386,6 @@ class HeartFChatting:
|
||||
}
|
||||
|
||||
loop_info = {
|
||||
"loop_observation_info": loop_observation_info,
|
||||
"loop_processor_info": loop_processor_info,
|
||||
"loop_plan_info": loop_plan_info,
|
||||
"loop_action_info": loop_action_info,
|
||||
}
|
||||
@@ -649,11 +396,8 @@ class HeartFChatting:
|
||||
logger.error(f"{self.log_prefix} FOCUS聊天处理失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return {
|
||||
"loop_observation_info": {},
|
||||
"loop_processor_info": {},
|
||||
"loop_plan_info": {
|
||||
"action_result": {"action_type": "error", "action_data": {}, "reasoning": f"处理失败: {e}"},
|
||||
"observed_messages": "",
|
||||
},
|
||||
"loop_action_info": {"action_taken": False, "reply_text": "", "command": "", "taken_time": time.time()},
|
||||
}
|
||||
@@ -698,7 +442,7 @@ class HeartFChatting:
|
||||
return False, "", ""
|
||||
|
||||
if not action_handler:
|
||||
logger.warning(f"{self.log_prefix} 未能创建动作处理器: {action}, 原因: {reasoning}")
|
||||
logger.warning(f"{self.log_prefix} 未能创建动作处理器: {action}")
|
||||
return False, "", ""
|
||||
|
||||
# 处理动作并获取结果
|
||||
@@ -734,12 +478,15 @@ class HeartFChatting:
|
||||
)
|
||||
# 设置系统命令,在下次循环检查时触发退出
|
||||
command = "stop_focus_chat"
|
||||
elif self._message_count >= current_threshold and global_config.chat.chat_mode != "auto":
|
||||
logger.info(
|
||||
f"{self.log_prefix} [非auto模式] 已发送 {self._message_count} 条消息,达到疲惫阈值 {current_threshold},但非auto模式不会自动退出"
|
||||
else:
|
||||
if reply_text == "timeout":
|
||||
self.reply_timeout_count += 1
|
||||
if self.reply_timeout_count > 5:
|
||||
logger.warning(
|
||||
f"[{self.log_prefix} ] 连续回复超时次数过多,{global_config.chat.thinking_timeout}秒 内大模型没有返回有效内容,请检查你的api是否速度过慢或配置错误。建议不要使用推理模型,推理模型生成速度过慢。或者尝试拉高thinking_timeout参数,这可能导致回复时间过长。"
|
||||
)
|
||||
|
||||
logger.debug(f"{self.log_prefix} 麦麦执行了'{action}', 返回结果'{success}', '{reply_text}', '{command}'")
|
||||
logger.warning(f"{self.log_prefix} 回复生成超时{global_config.chat.thinking_timeout}s,已跳过")
|
||||
return False, "", ""
|
||||
|
||||
return success, reply_text, command
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ class HFCPerformanceLogger:
|
||||
"""HFC性能记录管理器"""
|
||||
|
||||
# 版本号常量,可在启动时修改
|
||||
INTERNAL_VERSION = "v1.0.0"
|
||||
INTERNAL_VERSION = "v7.0.0"
|
||||
|
||||
def __init__(self, chat_id: str, version: str = None):
|
||||
def __init__(self, chat_id: str):
|
||||
self.chat_id = chat_id
|
||||
self.version = version or self.INTERNAL_VERSION
|
||||
self.version = self.INTERNAL_VERSION
|
||||
self.log_dir = Path("log/hfc_loop")
|
||||
self.session_start_time = datetime.now()
|
||||
|
||||
@@ -41,8 +41,6 @@ class HFCPerformanceLogger:
|
||||
"action_type": cycle_data.get("action_type", "unknown"),
|
||||
"total_time": cycle_data.get("total_time", 0),
|
||||
"step_times": cycle_data.get("step_times", {}),
|
||||
"processor_time_costs": cycle_data.get("processor_time_costs", {}), # 前处理器时间
|
||||
"post_processor_time_costs": cycle_data.get("post_processor_time_costs", {}), # 后处理器时间
|
||||
"reasoning": cycle_data.get("reasoning", ""),
|
||||
"success": cycle_data.get("success", False),
|
||||
}
|
||||
@@ -60,13 +58,6 @@ class HFCPerformanceLogger:
|
||||
f"time={record['total_time']:.2f}s",
|
||||
]
|
||||
|
||||
# 添加后处理器时间信息到日志
|
||||
if record["post_processor_time_costs"]:
|
||||
post_processor_stats = ", ".join(
|
||||
[f"{name}: {time_cost:.3f}s" for name, time_cost in record["post_processor_time_costs"].items()]
|
||||
)
|
||||
log_parts.append(f"post_processors=({post_processor_stats})")
|
||||
|
||||
logger.debug(f"记录HFC循环数据: {', '.join(log_parts)}")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,9 +5,104 @@ from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.message_receive.message import UserInfo
|
||||
from src.common.logger import get_logger
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
log_dir = "log/log_cycle_debug/"
|
||||
|
||||
|
||||
class CycleDetail:
|
||||
"""循环信息记录类"""
|
||||
|
||||
def __init__(self, cycle_id: int):
|
||||
self.cycle_id = cycle_id
|
||||
self.prefix = ""
|
||||
self.thinking_id = ""
|
||||
self.start_time = time.time()
|
||||
self.end_time: Optional[float] = None
|
||||
self.timers: Dict[str, float] = {}
|
||||
|
||||
self.loop_plan_info: Dict[str, Any] = {}
|
||||
self.loop_action_info: Dict[str, Any] = {}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""将循环信息转换为字典格式"""
|
||||
|
||||
def convert_to_serializable(obj, depth=0, seen=None):
|
||||
if seen is None:
|
||||
seen = set()
|
||||
|
||||
# 防止递归过深
|
||||
if depth > 5: # 降低递归深度限制
|
||||
return str(obj)
|
||||
|
||||
# 防止循环引用
|
||||
obj_id = id(obj)
|
||||
if obj_id in seen:
|
||||
return str(obj)
|
||||
seen.add(obj_id)
|
||||
|
||||
try:
|
||||
if hasattr(obj, "to_dict"):
|
||||
# 对于有to_dict方法的对象,直接调用其to_dict方法
|
||||
return obj.to_dict()
|
||||
elif isinstance(obj, dict):
|
||||
# 对于字典,只保留基本类型和可序列化的值
|
||||
return {
|
||||
k: convert_to_serializable(v, depth + 1, seen)
|
||||
for k, v in obj.items()
|
||||
if isinstance(k, (str, int, float, bool))
|
||||
}
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
# 对于列表和元组,只保留可序列化的元素
|
||||
return [
|
||||
convert_to_serializable(item, depth + 1, seen)
|
||||
for item in obj
|
||||
if not isinstance(item, (dict, list, tuple))
|
||||
or isinstance(item, (str, int, float, bool, type(None)))
|
||||
]
|
||||
elif isinstance(obj, (str, int, float, bool, type(None))):
|
||||
return obj
|
||||
else:
|
||||
return str(obj)
|
||||
finally:
|
||||
seen.remove(obj_id)
|
||||
|
||||
return {
|
||||
"cycle_id": self.cycle_id,
|
||||
"start_time": self.start_time,
|
||||
"end_time": self.end_time,
|
||||
"timers": self.timers,
|
||||
"thinking_id": self.thinking_id,
|
||||
"loop_plan_info": convert_to_serializable(self.loop_plan_info),
|
||||
"loop_action_info": convert_to_serializable(self.loop_action_info),
|
||||
}
|
||||
|
||||
def complete_cycle(self):
|
||||
"""完成循环,记录结束时间"""
|
||||
self.end_time = time.time()
|
||||
|
||||
# 处理 prefix,只保留中英文字符和基本标点
|
||||
if not self.prefix:
|
||||
self.prefix = "group"
|
||||
else:
|
||||
# 只保留中文、英文字母、数字和基本标点
|
||||
allowed_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
|
||||
self.prefix = (
|
||||
"".join(char for char in self.prefix if "\u4e00" <= char <= "\u9fff" or char in allowed_chars)
|
||||
or "group"
|
||||
)
|
||||
|
||||
def set_thinking_id(self, thinking_id: str):
|
||||
"""设置思考消息ID"""
|
||||
self.thinking_id = thinking_id
|
||||
|
||||
def set_loop_info(self, loop_info: Dict[str, Any]):
|
||||
"""设置循环信息"""
|
||||
self.loop_plan_info = loop_info["loop_plan_info"]
|
||||
self.loop_action_info = loop_info["loop_action_info"]
|
||||
|
||||
|
||||
async def create_empty_anchor_message(
|
||||
platform: str, group_info: dict, chat_stream: ChatStream
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"""
|
||||
HFC性能记录版本号管理器
|
||||
|
||||
用于管理HFC性能记录的内部版本号,支持:
|
||||
1. 默认版本号设置
|
||||
2. 启动时版本号配置
|
||||
3. 版本号验证和格式化
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("hfc_version")
|
||||
|
||||
|
||||
class HFCVersionManager:
|
||||
"""HFC版本号管理器"""
|
||||
|
||||
# 默认版本号
|
||||
DEFAULT_VERSION = "v4.0.0"
|
||||
|
||||
# 当前运行时版本号
|
||||
_current_version: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def set_version(cls, version: str) -> bool:
|
||||
"""
|
||||
设置当前运行时版本号
|
||||
|
||||
参数:
|
||||
version: 版本号字符串,格式如 v1.0.0 或 1.0.0
|
||||
|
||||
返回:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
try:
|
||||
validated_version = cls._validate_version(version)
|
||||
if validated_version:
|
||||
cls._current_version = validated_version
|
||||
logger.info(f"HFC性能记录版本已设置为: {validated_version}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"无效的版本号格式: {version}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"设置版本号失败: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_version(cls) -> str:
|
||||
"""
|
||||
获取当前版本号
|
||||
|
||||
返回:
|
||||
str: 当前版本号
|
||||
"""
|
||||
if cls._current_version:
|
||||
return cls._current_version
|
||||
|
||||
# 尝试从环境变量获取
|
||||
env_version = os.getenv("HFC_PERFORMANCE_VERSION")
|
||||
if env_version:
|
||||
if cls.set_version(env_version):
|
||||
return cls._current_version
|
||||
|
||||
# 返回默认版本号
|
||||
return cls.DEFAULT_VERSION
|
||||
|
||||
@classmethod
|
||||
def auto_generate_version(cls, base_version: str = None) -> str:
|
||||
"""
|
||||
自动生成版本号(基于时间戳)
|
||||
|
||||
参数:
|
||||
base_version: 基础版本号,如果不提供则使用默认版本
|
||||
|
||||
返回:
|
||||
str: 生成的版本号
|
||||
"""
|
||||
if not base_version:
|
||||
base_version = cls.DEFAULT_VERSION
|
||||
|
||||
# 提取基础版本号的主要部分
|
||||
base_match = re.match(r"v?(\d+\.\d+)", base_version)
|
||||
if base_match:
|
||||
base_part = base_match.group(1)
|
||||
else:
|
||||
base_part = "1.0"
|
||||
|
||||
# 添加时间戳
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
||||
generated_version = f"v{base_part}.{timestamp}"
|
||||
|
||||
cls.set_version(generated_version)
|
||||
logger.info(f"自动生成版本号: {generated_version}")
|
||||
|
||||
return generated_version
|
||||
|
||||
@classmethod
|
||||
def _validate_version(cls, version: str) -> Optional[str]:
|
||||
"""
|
||||
验证版本号格式
|
||||
|
||||
参数:
|
||||
version: 待验证的版本号
|
||||
|
||||
返回:
|
||||
Optional[str]: 验证后的版本号,失败返回None
|
||||
"""
|
||||
if not version or not isinstance(version, str):
|
||||
return None
|
||||
|
||||
version = version.strip()
|
||||
|
||||
# 支持的格式:
|
||||
# v1.0.0, 1.0.0, v1.0, 1.0, v1.0.0.20241222_1530 等
|
||||
patterns = [
|
||||
r"^v?(\d+\.\d+\.\d+)$", # v1.0.0 或 1.0.0
|
||||
r"^v?(\d+\.\d+)$", # v1.0 或 1.0
|
||||
r"^v?(\d+\.\d+\.\d+\.\w+)$", # v1.0.0.build 或 1.0.0.build
|
||||
r"^v?(\d+\.\d+\.\w+)$", # v1.0.build 或 1.0.build
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.match(pattern, version)
|
||||
if match:
|
||||
# 确保版本号以v开头
|
||||
if not version.startswith("v"):
|
||||
version = "v" + version
|
||||
return version
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def reset_version(cls):
|
||||
"""重置版本号为默认值"""
|
||||
cls._current_version = None
|
||||
logger.info("HFC版本号已重置为默认值")
|
||||
|
||||
@classmethod
|
||||
def get_version_info(cls) -> dict:
|
||||
"""
|
||||
获取版本信息
|
||||
|
||||
返回:
|
||||
dict: 版本相关信息
|
||||
"""
|
||||
current = cls.get_version()
|
||||
return {
|
||||
"current_version": current,
|
||||
"default_version": cls.DEFAULT_VERSION,
|
||||
"is_custom": current != cls.DEFAULT_VERSION,
|
||||
"env_version": os.getenv("HFC_PERFORMANCE_VERSION"),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# 全局函数,方便使用
|
||||
def set_hfc_version(version: str) -> bool:
|
||||
"""设置HFC性能记录版本号"""
|
||||
return HFCVersionManager.set_version(version)
|
||||
|
||||
|
||||
def get_hfc_version() -> str:
|
||||
"""获取当前HFC性能记录版本号"""
|
||||
return HFCVersionManager.get_version()
|
||||
|
||||
|
||||
def auto_generate_hfc_version(base_version: str = None) -> str:
|
||||
"""自动生成HFC版本号"""
|
||||
return HFCVersionManager.auto_generate_version(base_version)
|
||||
|
||||
|
||||
def reset_hfc_version():
|
||||
"""重置HFC版本号"""
|
||||
HFCVersionManager.reset_version()
|
||||
|
||||
|
||||
# 在模块加载时显示当前版本信息
|
||||
if __name__ != "__main__":
|
||||
current_version = HFCVersionManager.get_version()
|
||||
logger.debug(f"HFC性能记录模块已加载,当前版本: {current_version}")
|
||||
@@ -1,83 +0,0 @@
|
||||
from typing import Dict, Optional, Any, List
|
||||
from dataclasses import dataclass
|
||||
from .info_base import InfoBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionInfo(InfoBase):
|
||||
"""动作信息类
|
||||
|
||||
用于管理和记录动作的变更信息,包括需要添加或移除的动作。
|
||||
继承自 InfoBase 类,使用字典存储具体数据。
|
||||
|
||||
Attributes:
|
||||
type (str): 信息类型标识符,固定为 "action"
|
||||
|
||||
Data Fields:
|
||||
add_actions (List[str]): 需要添加的动作列表
|
||||
remove_actions (List[str]): 需要移除的动作列表
|
||||
reason (str): 变更原因说明
|
||||
"""
|
||||
|
||||
type: str = "action"
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""获取信息类型"""
|
||||
return self.type
|
||||
|
||||
def get_data(self) -> Dict[str, Any]:
|
||||
"""获取信息数据"""
|
||||
return self.data
|
||||
|
||||
def set_action_changes(self, action_changes: Dict[str, List[str]]) -> None:
|
||||
"""设置动作变更信息
|
||||
|
||||
Args:
|
||||
action_changes (Dict[str, List[str]]): 包含要增加和删除的动作列表
|
||||
{
|
||||
"add": ["action1", "action2"],
|
||||
"remove": ["action3"]
|
||||
}
|
||||
"""
|
||||
self.data["add_actions"] = action_changes.get("add", [])
|
||||
self.data["remove_actions"] = action_changes.get("remove", [])
|
||||
|
||||
def set_reason(self, reason: str) -> None:
|
||||
"""设置变更原因
|
||||
|
||||
Args:
|
||||
reason (str): 动作变更的原因说明
|
||||
"""
|
||||
self.data["reason"] = reason
|
||||
|
||||
def get_add_actions(self) -> List[str]:
|
||||
"""获取需要添加的动作列表
|
||||
|
||||
Returns:
|
||||
List[str]: 需要添加的动作列表
|
||||
"""
|
||||
return self.data.get("add_actions", [])
|
||||
|
||||
def get_remove_actions(self) -> List[str]:
|
||||
"""获取需要移除的动作列表
|
||||
|
||||
Returns:
|
||||
List[str]: 需要移除的动作列表
|
||||
"""
|
||||
return self.data.get("remove_actions", [])
|
||||
|
||||
def get_reason(self) -> Optional[str]:
|
||||
"""获取变更原因
|
||||
|
||||
Returns:
|
||||
Optional[str]: 动作变更的原因说明,如果未设置则返回 None
|
||||
"""
|
||||
return self.data.get("reason")
|
||||
|
||||
def has_changes(self) -> bool:
|
||||
"""检查是否有动作变更
|
||||
|
||||
Returns:
|
||||
bool: 如果有任何动作需要添加或移除则返回True
|
||||
"""
|
||||
return bool(self.get_add_actions() or self.get_remove_actions())
|
||||
@@ -1,97 +0,0 @@
|
||||
from typing import Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
from .info_base import InfoBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatInfo(InfoBase):
|
||||
"""聊天信息类
|
||||
|
||||
用于记录和管理聊天相关的信息,包括聊天ID、名称和类型等。
|
||||
继承自 InfoBase 类,使用字典存储具体数据。
|
||||
|
||||
Attributes:
|
||||
type (str): 信息类型标识符,固定为 "chat"
|
||||
|
||||
Data Fields:
|
||||
chat_id (str): 聊天的唯一标识符
|
||||
chat_name (str): 聊天的名称
|
||||
chat_type (str): 聊天的类型
|
||||
"""
|
||||
|
||||
type: str = "chat"
|
||||
|
||||
def set_chat_id(self, chat_id: str) -> None:
|
||||
"""设置聊天ID
|
||||
|
||||
Args:
|
||||
chat_id (str): 聊天的唯一标识符
|
||||
"""
|
||||
self.data["chat_id"] = chat_id
|
||||
|
||||
def set_chat_name(self, chat_name: str) -> None:
|
||||
"""设置聊天名称
|
||||
|
||||
Args:
|
||||
chat_name (str): 聊天的名称
|
||||
"""
|
||||
self.data["chat_name"] = chat_name
|
||||
|
||||
def set_chat_type(self, chat_type: str) -> None:
|
||||
"""设置聊天类型
|
||||
|
||||
Args:
|
||||
chat_type (str): 聊天的类型
|
||||
"""
|
||||
self.data["chat_type"] = chat_type
|
||||
|
||||
def get_chat_id(self) -> Optional[str]:
|
||||
"""获取聊天ID
|
||||
|
||||
Returns:
|
||||
Optional[str]: 聊天的唯一标识符,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("chat_id")
|
||||
|
||||
def get_chat_name(self) -> Optional[str]:
|
||||
"""获取聊天名称
|
||||
|
||||
Returns:
|
||||
Optional[str]: 聊天的名称,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("chat_name")
|
||||
|
||||
def get_chat_type(self) -> Optional[str]:
|
||||
"""获取聊天类型
|
||||
|
||||
Returns:
|
||||
Optional[str]: 聊天的类型,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("chat_type")
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""获取信息类型
|
||||
|
||||
Returns:
|
||||
str: 当前信息对象的类型标识符
|
||||
"""
|
||||
return self.type
|
||||
|
||||
def get_data(self) -> Dict[str, str]:
|
||||
"""获取所有信息数据
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含所有信息数据的字典
|
||||
"""
|
||||
return self.data
|
||||
|
||||
def get_info(self, key: str) -> Optional[str]:
|
||||
"""获取特定属性的信息
|
||||
|
||||
Args:
|
||||
key: 要获取的属性键名
|
||||
|
||||
Returns:
|
||||
Optional[str]: 属性值,如果键不存在则返回 None
|
||||
"""
|
||||
return self.data.get(key)
|
||||
@@ -1,157 +0,0 @@
|
||||
from typing import Dict, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
from .info_base import InfoBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class CycleInfo(InfoBase):
|
||||
"""循环信息类
|
||||
|
||||
用于记录和管理心跳循环的相关信息,包括循环ID、时间信息、动作信息等。
|
||||
继承自 InfoBase 类,使用字典存储具体数据。
|
||||
|
||||
Attributes:
|
||||
type (str): 信息类型标识符,固定为 "cycle"
|
||||
|
||||
Data Fields:
|
||||
cycle_id (str): 当前循环的唯一标识符
|
||||
start_time (str): 循环开始的时间
|
||||
end_time (str): 循环结束的时间
|
||||
action (str): 在循环中采取的动作
|
||||
action_data (Dict[str, Any]): 动作相关的详细数据
|
||||
reason (str): 触发循环的原因
|
||||
observe_info (str): 当前的回复信息
|
||||
"""
|
||||
|
||||
type: str = "cycle"
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""获取信息类型"""
|
||||
return self.type
|
||||
|
||||
def get_data(self) -> Dict[str, str]:
|
||||
"""获取信息数据"""
|
||||
return self.data
|
||||
|
||||
def get_info(self, key: str) -> Optional[str]:
|
||||
"""获取特定属性的信息
|
||||
|
||||
Args:
|
||||
key: 要获取的属性键名
|
||||
|
||||
Returns:
|
||||
属性值,如果键不存在则返回 None
|
||||
"""
|
||||
return self.data.get(key)
|
||||
|
||||
def set_cycle_id(self, cycle_id: str) -> None:
|
||||
"""设置循环ID
|
||||
|
||||
Args:
|
||||
cycle_id (str): 循环的唯一标识符
|
||||
"""
|
||||
self.data["cycle_id"] = cycle_id
|
||||
|
||||
def set_start_time(self, start_time: str) -> None:
|
||||
"""设置开始时间
|
||||
|
||||
Args:
|
||||
start_time (str): 循环开始的时间,建议使用标准时间格式
|
||||
"""
|
||||
self.data["start_time"] = start_time
|
||||
|
||||
def set_end_time(self, end_time: str) -> None:
|
||||
"""设置结束时间
|
||||
|
||||
Args:
|
||||
end_time (str): 循环结束的时间,建议使用标准时间格式
|
||||
"""
|
||||
self.data["end_time"] = end_time
|
||||
|
||||
def set_action(self, action: str) -> None:
|
||||
"""设置采取的动作
|
||||
|
||||
Args:
|
||||
action (str): 在循环中执行的动作名称
|
||||
"""
|
||||
self.data["action"] = action
|
||||
|
||||
def set_action_data(self, action_data: Dict[str, Any]) -> None:
|
||||
"""设置动作数据
|
||||
|
||||
Args:
|
||||
action_data (Dict[str, Any]): 动作相关的详细数据,将被转换为字符串存储
|
||||
"""
|
||||
self.data["action_data"] = str(action_data)
|
||||
|
||||
def set_reason(self, reason: str) -> None:
|
||||
"""设置原因
|
||||
|
||||
Args:
|
||||
reason (str): 触发循环的原因说明
|
||||
"""
|
||||
self.data["reason"] = reason
|
||||
|
||||
def set_observe_info(self, observe_info: str) -> None:
|
||||
"""设置回复信息
|
||||
|
||||
Args:
|
||||
observe_info (str): 当前的回复信息
|
||||
"""
|
||||
self.data["observe_info"] = observe_info
|
||||
|
||||
def get_cycle_id(self) -> Optional[str]:
|
||||
"""获取循环ID
|
||||
|
||||
Returns:
|
||||
Optional[str]: 循环的唯一标识符,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("cycle_id")
|
||||
|
||||
def get_start_time(self) -> Optional[str]:
|
||||
"""获取开始时间
|
||||
|
||||
Returns:
|
||||
Optional[str]: 循环开始的时间,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("start_time")
|
||||
|
||||
def get_end_time(self) -> Optional[str]:
|
||||
"""获取结束时间
|
||||
|
||||
Returns:
|
||||
Optional[str]: 循环结束的时间,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("end_time")
|
||||
|
||||
def get_action(self) -> Optional[str]:
|
||||
"""获取采取的动作
|
||||
|
||||
Returns:
|
||||
Optional[str]: 在循环中执行的动作名称,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("action")
|
||||
|
||||
def get_action_data(self) -> Optional[str]:
|
||||
"""获取动作数据
|
||||
|
||||
Returns:
|
||||
Optional[str]: 动作相关的详细数据(字符串形式),如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("action_data")
|
||||
|
||||
def get_reason(self) -> Optional[str]:
|
||||
"""获取原因
|
||||
|
||||
Returns:
|
||||
Optional[str]: 触发循环的原因说明,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("reason")
|
||||
|
||||
def get_observe_info(self) -> Optional[str]:
|
||||
"""获取回复信息
|
||||
|
||||
Returns:
|
||||
Optional[str]: 当前的回复信息,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("observe_info")
|
||||
@@ -1,69 +0,0 @@
|
||||
from typing import Dict, Optional, Any, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class InfoBase:
|
||||
"""信息基类
|
||||
|
||||
这是一个基础信息类,用于存储和管理各种类型的信息数据。
|
||||
所有具体的信息类都应该继承自这个基类。
|
||||
|
||||
Attributes:
|
||||
type (str): 信息类型标识符,默认为 "base"
|
||||
data (Dict[str, Union[str, Dict, list]]): 存储具体信息数据的字典,
|
||||
支持存储字符串、字典、列表等嵌套数据结构
|
||||
"""
|
||||
|
||||
type: str = "base"
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
processed_info: str = ""
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""获取信息类型
|
||||
|
||||
Returns:
|
||||
str: 当前信息对象的类型标识符
|
||||
"""
|
||||
return self.type
|
||||
|
||||
def get_data(self) -> Dict[str, Any]:
|
||||
"""获取所有信息数据
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含所有信息数据的字典
|
||||
"""
|
||||
return self.data
|
||||
|
||||
def get_info(self, key: str) -> Optional[Any]:
|
||||
"""获取特定属性的信息
|
||||
|
||||
Args:
|
||||
key: 要获取的属性键名
|
||||
|
||||
Returns:
|
||||
Optional[Any]: 属性值,如果键不存在则返回 None
|
||||
"""
|
||||
return self.data.get(key)
|
||||
|
||||
def get_info_list(self, key: str) -> List[Any]:
|
||||
"""获取特定属性的信息列表
|
||||
|
||||
Args:
|
||||
key: 要获取的属性键名
|
||||
|
||||
Returns:
|
||||
List[Any]: 属性值列表,如果键不存在则返回空列表
|
||||
"""
|
||||
value = self.data.get(key)
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return []
|
||||
|
||||
def get_processed_info(self) -> str:
|
||||
"""获取处理后的信息
|
||||
|
||||
Returns:
|
||||
str: 处理后的信息字符串
|
||||
"""
|
||||
return self.processed_info
|
||||
@@ -1,165 +0,0 @@
|
||||
from typing import Dict, Optional
|
||||
from dataclasses import dataclass
|
||||
from .info_base import InfoBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObsInfo(InfoBase):
|
||||
"""OBS信息类
|
||||
|
||||
用于记录和管理OBS相关的信息,包括说话消息、截断后的说话消息和聊天类型。
|
||||
继承自 InfoBase 类,使用字典存储具体数据。
|
||||
|
||||
Attributes:
|
||||
type (str): 信息类型标识符,固定为 "obs"
|
||||
|
||||
Data Fields:
|
||||
talking_message (str): 说话消息内容
|
||||
talking_message_str_truncate (str): 截断后的说话消息内容
|
||||
talking_message_str_short (str): 简短版本的说话消息内容(使用最新一半消息)
|
||||
talking_message_str_truncate_short (str): 截断简短版本的说话消息内容(使用最新一半消息)
|
||||
chat_type (str): 聊天类型,可以是 "private"(私聊)、"group"(群聊)或 "other"(其他)
|
||||
"""
|
||||
|
||||
type: str = "obs"
|
||||
|
||||
def set_talking_message(self, message: str) -> None:
|
||||
"""设置说话消息
|
||||
|
||||
Args:
|
||||
message (str): 说话消息内容
|
||||
"""
|
||||
self.data["talking_message"] = message
|
||||
|
||||
def set_talking_message_str_truncate(self, message: str) -> None:
|
||||
"""设置截断后的说话消息
|
||||
|
||||
Args:
|
||||
message (str): 截断后的说话消息内容
|
||||
"""
|
||||
self.data["talking_message_str_truncate"] = message
|
||||
|
||||
def set_talking_message_str_short(self, message: str) -> None:
|
||||
"""设置简短版本的说话消息
|
||||
|
||||
Args:
|
||||
message (str): 简短版本的说话消息内容
|
||||
"""
|
||||
self.data["talking_message_str_short"] = message
|
||||
|
||||
def set_talking_message_str_truncate_short(self, message: str) -> None:
|
||||
"""设置截断简短版本的说话消息
|
||||
|
||||
Args:
|
||||
message (str): 截断简短版本的说话消息内容
|
||||
"""
|
||||
self.data["talking_message_str_truncate_short"] = message
|
||||
|
||||
def set_previous_chat_info(self, message: str) -> None:
|
||||
"""设置之前聊天信息
|
||||
|
||||
Args:
|
||||
message (str): 之前聊天信息内容
|
||||
"""
|
||||
self.data["previous_chat_info"] = message
|
||||
|
||||
def set_chat_type(self, chat_type: str) -> None:
|
||||
"""设置聊天类型
|
||||
|
||||
Args:
|
||||
chat_type (str): 聊天类型,可以是 "private"(私聊)、"group"(群聊)或 "other"(其他)
|
||||
"""
|
||||
if chat_type not in ["private", "group", "other"]:
|
||||
chat_type = "other"
|
||||
self.data["chat_type"] = chat_type
|
||||
|
||||
def set_chat_target(self, chat_target: str) -> None:
|
||||
"""设置聊天目标
|
||||
|
||||
Args:
|
||||
chat_target (str): 聊天目标,可以是 "private"(私聊)、"group"(群聊)或 "other"(其他)
|
||||
"""
|
||||
self.data["chat_target"] = chat_target
|
||||
|
||||
def set_chat_id(self, chat_id: str) -> None:
|
||||
"""设置聊天ID
|
||||
|
||||
Args:
|
||||
chat_id (str): 聊天ID
|
||||
"""
|
||||
self.data["chat_id"] = chat_id
|
||||
|
||||
def get_chat_id(self) -> Optional[str]:
|
||||
"""获取聊天ID
|
||||
|
||||
Returns:
|
||||
Optional[str]: 聊天ID,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("chat_id")
|
||||
|
||||
def get_talking_message(self) -> Optional[str]:
|
||||
"""获取说话消息
|
||||
|
||||
Returns:
|
||||
Optional[str]: 说话消息内容,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("talking_message")
|
||||
|
||||
def get_talking_message_str_truncate(self) -> Optional[str]:
|
||||
"""获取截断后的说话消息
|
||||
|
||||
Returns:
|
||||
Optional[str]: 截断后的说话消息内容,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("talking_message_str_truncate")
|
||||
|
||||
def get_talking_message_str_short(self) -> Optional[str]:
|
||||
"""获取简短版本的说话消息
|
||||
|
||||
Returns:
|
||||
Optional[str]: 简短版本的说话消息内容,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("talking_message_str_short")
|
||||
|
||||
def get_talking_message_str_truncate_short(self) -> Optional[str]:
|
||||
"""获取截断简短版本的说话消息
|
||||
|
||||
Returns:
|
||||
Optional[str]: 截断简短版本的说话消息内容,如果未设置则返回 None
|
||||
"""
|
||||
return self.get_info("talking_message_str_truncate_short")
|
||||
|
||||
def get_chat_type(self) -> str:
|
||||
"""获取聊天类型
|
||||
|
||||
Returns:
|
||||
str: 聊天类型,默认为 "other"
|
||||
"""
|
||||
return self.get_info("chat_type") or "other"
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""获取信息类型
|
||||
|
||||
Returns:
|
||||
str: 当前信息对象的类型标识符
|
||||
"""
|
||||
return self.type
|
||||
|
||||
def get_data(self) -> Dict[str, str]:
|
||||
"""获取所有信息数据
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含所有信息数据的字典
|
||||
"""
|
||||
return self.data
|
||||
|
||||
def get_info(self, key: str) -> Optional[str]:
|
||||
"""获取特定属性的信息
|
||||
|
||||
Args:
|
||||
key: 要获取的属性键名
|
||||
|
||||
Returns:
|
||||
Optional[str]: 属性值,如果键不存在则返回 None
|
||||
"""
|
||||
return self.data.get(key)
|
||||
@@ -1,86 +0,0 @@
|
||||
from typing import Dict, Optional, List
|
||||
from dataclasses import dataclass
|
||||
from .info_base import InfoBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkingMemoryInfo(InfoBase):
|
||||
type: str = "workingmemory"
|
||||
|
||||
processed_info: str = ""
|
||||
|
||||
def set_talking_message(self, message: str) -> None:
|
||||
"""设置说话消息
|
||||
|
||||
Args:
|
||||
message (str): 说话消息内容
|
||||
"""
|
||||
self.data["talking_message"] = message
|
||||
|
||||
def set_working_memory(self, working_memory: List[str]) -> None:
|
||||
"""设置工作记忆列表
|
||||
|
||||
Args:
|
||||
working_memory (List[str]): 工作记忆内容列表
|
||||
"""
|
||||
self.data["working_memory"] = working_memory
|
||||
|
||||
def add_working_memory(self, working_memory: str) -> None:
|
||||
"""添加一条工作记忆
|
||||
|
||||
Args:
|
||||
working_memory (str): 工作记忆内容,格式为"记忆要点:xxx"
|
||||
"""
|
||||
working_memory_list = self.data.get("working_memory", [])
|
||||
working_memory_list.append(working_memory)
|
||||
self.data["working_memory"] = working_memory_list
|
||||
|
||||
def get_working_memory(self) -> List[str]:
|
||||
"""获取所有工作记忆
|
||||
|
||||
Returns:
|
||||
List[str]: 工作记忆内容列表,每条记忆格式为"记忆要点:xxx"
|
||||
"""
|
||||
return self.data.get("working_memory", [])
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""获取信息类型
|
||||
|
||||
Returns:
|
||||
str: 当前信息对象的类型标识符
|
||||
"""
|
||||
return self.type
|
||||
|
||||
def get_data(self) -> Dict[str, List[str]]:
|
||||
"""获取所有信息数据
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: 包含所有信息数据的字典
|
||||
"""
|
||||
return self.data
|
||||
|
||||
def get_info(self, key: str) -> Optional[List[str]]:
|
||||
"""获取特定属性的信息
|
||||
|
||||
Args:
|
||||
key: 要获取的属性键名
|
||||
|
||||
Returns:
|
||||
Optional[List[str]]: 属性值,如果键不存在则返回 None
|
||||
"""
|
||||
return self.data.get(key)
|
||||
|
||||
def get_processed_info(self) -> str:
|
||||
"""获取处理后的信息
|
||||
|
||||
Returns:
|
||||
str: 处理后的信息数据,所有记忆要点按行拼接
|
||||
"""
|
||||
all_memory = self.get_working_memory()
|
||||
memory_str = ""
|
||||
for memory in all_memory:
|
||||
memory_str += f"{memory}\n"
|
||||
|
||||
self.processed_info = memory_str
|
||||
|
||||
return self.processed_info
|
||||
@@ -1,51 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Any
|
||||
from src.chat.focus_chat.info.info_base import InfoBase
|
||||
from src.chat.heart_flow.observation.observation import Observation
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("base_processor")
|
||||
|
||||
|
||||
class BaseProcessor(ABC):
|
||||
"""信息处理器基类
|
||||
|
||||
所有具体的信息处理器都应该继承这个基类,并实现process_info方法。
|
||||
支持处理InfoBase和Observation类型的输入。
|
||||
"""
|
||||
|
||||
log_prefix = "Base信息处理器"
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self):
|
||||
"""初始化处理器"""
|
||||
|
||||
@abstractmethod
|
||||
async def process_info(
|
||||
self,
|
||||
observations: List[Observation] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[InfoBase]:
|
||||
"""处理信息对象的抽象方法
|
||||
|
||||
Args:
|
||||
infos: InfoBase对象列表
|
||||
observations: 可选的Observation对象列表
|
||||
**kwargs: 其他可选参数
|
||||
|
||||
Returns:
|
||||
List[InfoBase]: 处理后的InfoBase实例列表
|
||||
"""
|
||||
pass
|
||||
|
||||
def _create_processed_item(self, info_type: str, info_data: Any) -> dict:
|
||||
"""创建处理后的信息项
|
||||
|
||||
Args:
|
||||
info_type: 信息类型
|
||||
info_data: 信息数据
|
||||
|
||||
Returns:
|
||||
dict: 处理后的信息项
|
||||
"""
|
||||
return {"type": info_type, "id": f"info_{info_type}", "content": info_data, "ttl": 3}
|
||||
@@ -1,142 +0,0 @@
|
||||
from typing import List, Any
|
||||
from src.chat.focus_chat.info.obs_info import ObsInfo
|
||||
from src.chat.heart_flow.observation.observation import Observation
|
||||
from src.chat.focus_chat.info.info_base import InfoBase
|
||||
from .base_processor import BaseProcessor
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
|
||||
from datetime import datetime
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
|
||||
logger = get_logger("processor")
|
||||
|
||||
|
||||
class ChattingInfoProcessor(BaseProcessor):
|
||||
"""观察处理器
|
||||
|
||||
用于处理Observation对象,将其转换为ObsInfo对象。
|
||||
"""
|
||||
|
||||
log_prefix = "聊天信息处理"
|
||||
|
||||
def __init__(self):
|
||||
"""初始化观察处理器"""
|
||||
super().__init__()
|
||||
# TODO: API-Adapter修改标记
|
||||
self.model_summary = LLMRequest(
|
||||
model=global_config.model.utils_small,
|
||||
temperature=0.7,
|
||||
request_type="focus.observation.chat",
|
||||
)
|
||||
|
||||
async def process_info(
|
||||
self,
|
||||
observations: List[Observation] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[InfoBase]:
|
||||
"""处理Observation对象
|
||||
|
||||
Args:
|
||||
infos: InfoBase对象列表
|
||||
observations: 可选的Observation对象列表
|
||||
**kwargs: 其他可选参数
|
||||
|
||||
Returns:
|
||||
List[InfoBase]: 处理后的ObsInfo实例列表
|
||||
"""
|
||||
# print(f"observations: {observations}")
|
||||
processed_infos = []
|
||||
|
||||
# 处理Observation对象
|
||||
if observations:
|
||||
for obs in observations:
|
||||
# print(f"obs: {obs}")
|
||||
if isinstance(obs, ChattingObservation):
|
||||
obs_info = ObsInfo()
|
||||
|
||||
# 设置聊天ID
|
||||
if hasattr(obs, "chat_id"):
|
||||
obs_info.set_chat_id(obs.chat_id)
|
||||
|
||||
# 设置说话消息
|
||||
if hasattr(obs, "talking_message_str"):
|
||||
# print(f"设置说话消息:obs.talking_message_str: {obs.talking_message_str}")
|
||||
obs_info.set_talking_message(obs.talking_message_str)
|
||||
|
||||
# 设置截断后的说话消息
|
||||
if hasattr(obs, "talking_message_str_truncate"):
|
||||
# print(f"设置截断后的说话消息:obs.talking_message_str_truncate: {obs.talking_message_str_truncate}")
|
||||
obs_info.set_talking_message_str_truncate(obs.talking_message_str_truncate)
|
||||
|
||||
# 设置简短版本的说话消息
|
||||
if hasattr(obs, "talking_message_str_short"):
|
||||
obs_info.set_talking_message_str_short(obs.talking_message_str_short)
|
||||
|
||||
# 设置截断简短版本的说话消息
|
||||
if hasattr(obs, "talking_message_str_truncate_short"):
|
||||
obs_info.set_talking_message_str_truncate_short(obs.talking_message_str_truncate_short)
|
||||
|
||||
if hasattr(obs, "mid_memory_info"):
|
||||
# print(f"设置之前聊天信息:obs.mid_memory_info: {obs.mid_memory_info}")
|
||||
obs_info.set_previous_chat_info(obs.mid_memory_info)
|
||||
|
||||
# 设置聊天类型
|
||||
is_group_chat = obs.is_group_chat
|
||||
if is_group_chat:
|
||||
chat_type = "group"
|
||||
else:
|
||||
chat_type = "private"
|
||||
if hasattr(obs, "chat_target_info") and obs.chat_target_info:
|
||||
obs_info.set_chat_target(obs.chat_target_info.get("person_name", "某人"))
|
||||
obs_info.set_chat_type(chat_type)
|
||||
|
||||
# logger.debug(f"聊天信息处理器处理后的信息: {obs_info}")
|
||||
|
||||
processed_infos.append(obs_info)
|
||||
|
||||
return processed_infos
|
||||
|
||||
async def chat_compress(self, obs: ChattingObservation):
|
||||
log_msg = ""
|
||||
if obs.compressor_prompt:
|
||||
summary = ""
|
||||
try:
|
||||
summary_result, _ = await self.model_summary.generate_response_async(obs.compressor_prompt)
|
||||
summary = "没有主题的闲聊"
|
||||
if summary_result:
|
||||
summary = summary_result
|
||||
except Exception as e:
|
||||
log_msg = f"总结主题失败 for chat {obs.chat_id}: {e}"
|
||||
logger.error(log_msg)
|
||||
else:
|
||||
log_msg = f"chat_compress 完成 for chat {obs.chat_id}, summary: {summary}"
|
||||
logger.info(log_msg)
|
||||
|
||||
mid_memory = {
|
||||
"id": str(int(datetime.now().timestamp())),
|
||||
"theme": summary,
|
||||
"messages": obs.oldest_messages, # 存储原始消息对象
|
||||
"readable_messages": obs.oldest_messages_str,
|
||||
# "timestamps": oldest_timestamps,
|
||||
"chat_id": obs.chat_id,
|
||||
"created_at": datetime.now().timestamp(),
|
||||
}
|
||||
|
||||
obs.mid_memories.append(mid_memory)
|
||||
if len(obs.mid_memories) > obs.max_mid_memory_len:
|
||||
obs.mid_memories.pop(0) # 移除最旧的
|
||||
|
||||
mid_memory_str = "之前聊天的内容概述是:\n"
|
||||
for mid_memory_item in obs.mid_memories: # 重命名循环变量以示区分
|
||||
time_diff = int((datetime.now().timestamp() - mid_memory_item["created_at"]) / 60)
|
||||
mid_memory_str += (
|
||||
f"距离现在{time_diff}分钟前(聊天记录id:{mid_memory_item['id']}):{mid_memory_item['theme']}\n"
|
||||
)
|
||||
obs.mid_memory_info = mid_memory_str
|
||||
|
||||
obs.compressor_prompt = ""
|
||||
obs.oldest_messages = []
|
||||
obs.oldest_messages_str = ""
|
||||
|
||||
return log_msg
|
||||
@@ -1,28 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.chat.focus_chat.info.info_base import InfoBase
|
||||
|
||||
|
||||
class BasePlanner(ABC):
|
||||
"""规划器基类"""
|
||||
|
||||
def __init__(self, log_prefix: str, action_manager: ActionManager):
|
||||
self.log_prefix = log_prefix
|
||||
self.action_manager = action_manager
|
||||
|
||||
@abstractmethod
|
||||
async def plan(
|
||||
self, all_plan_info: List[InfoBase], running_memorys: List[Dict[str, Any]], loop_start_time: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
规划下一步行动
|
||||
|
||||
Args:
|
||||
all_plan_info: 所有计划信息
|
||||
running_memorys: 回忆信息
|
||||
loop_start_time: 循环开始时间
|
||||
Returns:
|
||||
Dict[str, Any]: 规划结果
|
||||
"""
|
||||
pass
|
||||
@@ -1,45 +0,0 @@
|
||||
from typing import Dict, Type
|
||||
from src.chat.focus_chat.planners.base_planner import BasePlanner
|
||||
from src.chat.focus_chat.planners.planner_simple import ActionPlanner as SimpleActionPlanner
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("planner_factory")
|
||||
|
||||
|
||||
class PlannerFactory:
|
||||
"""规划器工厂类,用于创建不同类型的规划器实例"""
|
||||
|
||||
# 注册所有可用的规划器类型
|
||||
_planner_types: Dict[str, Type[BasePlanner]] = {
|
||||
"simple": SimpleActionPlanner,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def register_planner(cls, name: str, planner_class: Type[BasePlanner]) -> None:
|
||||
"""
|
||||
注册新的规划器类型
|
||||
|
||||
Args:
|
||||
name: 规划器类型名称
|
||||
planner_class: 规划器类
|
||||
"""
|
||||
cls._planner_types[name] = planner_class
|
||||
logger.info(f"注册新的规划器类型: {name}")
|
||||
|
||||
@classmethod
|
||||
def create_planner(cls, log_prefix: str, action_manager: ActionManager) -> BasePlanner:
|
||||
"""
|
||||
创建规划器实例
|
||||
|
||||
Args:
|
||||
log_prefix: 日志前缀
|
||||
action_manager: 动作管理器实例
|
||||
|
||||
Returns:
|
||||
BasePlanner: 规划器实例
|
||||
"""
|
||||
|
||||
planner_class = cls._planner_types["simple"]
|
||||
logger.info(f"{log_prefix} 使用simple规划器")
|
||||
return planner_class(log_prefix=log_prefix, action_manager=action_manager)
|
||||
@@ -1,173 +0,0 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import Optional, Coroutine, Callable, Any, List
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager
|
||||
from src.config.config import global_config
|
||||
|
||||
logger = get_logger("background_tasks")
|
||||
|
||||
|
||||
# 新增私聊激活检查间隔
|
||||
PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒
|
||||
|
||||
CLEANUP_INTERVAL_SECONDS = 1200
|
||||
|
||||
|
||||
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 的后台周期性任务。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
subheartflow_manager: SubHeartflowManager,
|
||||
):
|
||||
self.subheartflow_manager = subheartflow_manager
|
||||
|
||||
# Task references
|
||||
self._cleanup_task: Optional[asyncio.Task] = None
|
||||
self._hf_judge_state_update_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):
|
||||
"""启动所有后台任务
|
||||
|
||||
功能说明:
|
||||
- 启动核心后台任务: 状态更新、清理、日志记录、兴趣评估和随机停用
|
||||
- 每个任务启动前检查是否已在运行
|
||||
- 将任务引用保存到任务列表
|
||||
"""
|
||||
|
||||
task_configs = []
|
||||
|
||||
# 根据 chat_mode 条件添加其他任务
|
||||
if not (global_config.chat.chat_mode == "normal"):
|
||||
task_configs.extend(
|
||||
[
|
||||
(
|
||||
self._run_cleanup_cycle,
|
||||
"info",
|
||||
f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s",
|
||||
"_cleanup_task",
|
||||
),
|
||||
# 新增私聊激活任务配置
|
||||
(
|
||||
# Use lambda to pass the interval to the runner function
|
||||
lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS),
|
||||
"debug",
|
||||
f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s",
|
||||
"_private_chat_activation_task",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 统一启动所有任务
|
||||
for task_func, log_level, log_msg, task_attr_name in task_configs:
|
||||
# 检查任务变量是否存在且未完成
|
||||
current_task_var = getattr(self, task_attr_name)
|
||||
if current_task_var is None or current_task_var.done():
|
||||
new_task = asyncio.create_task(task_func())
|
||||
setattr(self, task_attr_name, new_task) # 更新任务变量
|
||||
if new_task not in self._tasks: # 避免重复添加
|
||||
self._tasks.append(new_task)
|
||||
|
||||
# 根据配置记录不同级别的日志
|
||||
getattr(logger, log_level)(log_msg)
|
||||
else:
|
||||
logger.warning(f"{task_attr_name}任务已在运行")
|
||||
|
||||
async def stop_tasks(self):
|
||||
"""停止所有后台任务。
|
||||
|
||||
该方法会:
|
||||
1. 遍历所有后台任务并取消未完成的任务
|
||||
2. 等待所有取消操作完成
|
||||
3. 清空任务列表
|
||||
"""
|
||||
logger.info("正在停止所有后台任务...")
|
||||
cancelled_count = 0
|
||||
|
||||
# 第一步:取消所有运行中的任务
|
||||
for task in self._tasks:
|
||||
if task and not task.done():
|
||||
task.cancel() # 发送取消请求
|
||||
cancelled_count += 1
|
||||
|
||||
# 第二步:处理取消结果
|
||||
if cancelled_count > 0:
|
||||
logger.debug(f"正在等待{cancelled_count}个任务完成取消...")
|
||||
# 使用gather等待所有取消操作完成,忽略异常
|
||||
await asyncio.gather(*[t for t in self._tasks if t and t.cancelled()], return_exceptions=True)
|
||||
logger.info(f"成功取消{cancelled_count}个后台任务")
|
||||
else:
|
||||
logger.info("没有需要取消的后台任务")
|
||||
|
||||
# 第三步:清空任务列表
|
||||
self._tasks = [] # 重置任务列表
|
||||
|
||||
# 状态转换处理
|
||||
|
||||
async def _perform_cleanup_work(self):
|
||||
"""执行子心流清理任务
|
||||
1. 获取需要清理的不活跃子心流列表
|
||||
2. 逐个停止这些子心流
|
||||
3. 记录清理结果
|
||||
"""
|
||||
# 获取需要清理的子心流列表(包含ID和原因)
|
||||
flows_to_stop = self.subheartflow_manager.get_inactive_subheartflows()
|
||||
|
||||
if not flows_to_stop:
|
||||
return # 没有需要清理的子心流直接返回
|
||||
|
||||
logger.info(f"准备删除 {len(flows_to_stop)} 个不活跃(1h)子心流")
|
||||
stopped_count = 0
|
||||
|
||||
# 逐个停止子心流
|
||||
for flow_id in flows_to_stop:
|
||||
success = await self.subheartflow_manager.delete_subflow(flow_id)
|
||||
if success:
|
||||
stopped_count += 1
|
||||
logger.debug(f"[清理任务] 已停止子心流 {flow_id}")
|
||||
|
||||
# 记录最终清理结果
|
||||
logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流")
|
||||
|
||||
async def _run_cleanup_cycle(self):
|
||||
await _run_periodic_loop(
|
||||
task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
|
||||
)
|
||||
|
||||
# 新增私聊激活任务运行器
|
||||
async def _run_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,
|
||||
)
|
||||
@@ -1,84 +1,56 @@
|
||||
from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState
|
||||
from src.common.logger import get_logger
|
||||
from typing import Any, Optional, List
|
||||
from src.chat.heart_flow.subheartflow_manager import SubHeartflowManager
|
||||
from src.chat.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager
|
||||
from typing import Any, Optional
|
||||
from typing import Dict
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
|
||||
logger = get_logger("heartflow")
|
||||
|
||||
|
||||
class Heartflow:
|
||||
"""主心流协调器,负责初始化并协调各个子系统:
|
||||
- 状态管理 (MaiState)
|
||||
- 子心流管理 (SubHeartflow)
|
||||
- 后台任务 (BackgroundTaskManager)
|
||||
"""
|
||||
"""主心流协调器,负责初始化并协调聊天"""
|
||||
|
||||
def __init__(self):
|
||||
# 子心流管理 (在初始化时传入 current_state)
|
||||
self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager()
|
||||
|
||||
# 后台任务管理器 (整合所有定时任务)
|
||||
self.background_task_manager: BackgroundTaskManager = BackgroundTaskManager(
|
||||
subheartflow_manager=self.subheartflow_manager,
|
||||
)
|
||||
self.subheartflows: Dict[Any, "SubHeartflow"] = {}
|
||||
|
||||
async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
|
||||
"""获取或创建一个新的SubHeartflow实例 - 委托给 SubHeartflowManager"""
|
||||
# 不再需要传入 self.current_state
|
||||
return await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
|
||||
"""获取或创建一个新的SubHeartflow实例"""
|
||||
if subheartflow_id in self.subheartflows:
|
||||
subflow = self.subheartflows.get(subheartflow_id)
|
||||
if subflow:
|
||||
return subflow
|
||||
|
||||
try:
|
||||
new_subflow = SubHeartflow(
|
||||
subheartflow_id,
|
||||
)
|
||||
|
||||
await new_subflow.initialize()
|
||||
|
||||
# 注册子心流
|
||||
self.subheartflows[subheartflow_id] = new_subflow
|
||||
heartflow_name = get_chat_manager().get_stream_name(subheartflow_id) or subheartflow_id
|
||||
logger.info(f"[{heartflow_name}] 开始接收消息")
|
||||
|
||||
return new_subflow
|
||||
except Exception as e:
|
||||
logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def force_change_subheartflow_status(self, subheartflow_id: str, status: ChatState) -> None:
|
||||
"""强制改变子心流的状态"""
|
||||
# 这里的 message 是可选的,可能是一个消息对象,也可能是其他类型的数据
|
||||
return await self.subheartflow_manager.force_change_state(subheartflow_id, status)
|
||||
return await self.force_change_state(subheartflow_id, status)
|
||||
|
||||
async def api_get_all_states(self):
|
||||
"""获取所有状态"""
|
||||
return await self.interest_logger.api_get_all_states()
|
||||
|
||||
async def api_get_subheartflow_cycle_info(self, subheartflow_id: str, history_len: int) -> Optional[dict]:
|
||||
"""获取子心流的循环信息"""
|
||||
subheartflow = await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
|
||||
if not subheartflow:
|
||||
logger.warning(f"尝试获取不存在的子心流 {subheartflow_id} 的周期信息")
|
||||
return None
|
||||
heartfc_instance = subheartflow.heart_fc_instance
|
||||
if not heartfc_instance:
|
||||
logger.warning(f"子心流 {subheartflow_id} 没有心流实例,无法获取周期信息")
|
||||
return None
|
||||
|
||||
return heartfc_instance.get_cycle_history(last_n=history_len)
|
||||
|
||||
async def api_get_normal_chat_replies(self, subheartflow_id: str, limit: int = 10) -> Optional[List[dict]]:
|
||||
"""获取子心流的NormalChat回复记录
|
||||
|
||||
Args:
|
||||
subheartflow_id: 子心流ID
|
||||
limit: 最大返回数量,默认10条
|
||||
|
||||
Returns:
|
||||
Optional[List[dict]]: 回复记录列表,如果子心流不存在则返回None
|
||||
"""
|
||||
subheartflow = await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
|
||||
if not subheartflow:
|
||||
logger.warning(f"尝试获取不存在的子心流 {subheartflow_id} 的NormalChat回复记录")
|
||||
return None
|
||||
|
||||
return subheartflow.get_normal_chat_recent_replies(limit)
|
||||
|
||||
async def heartflow_start_working(self):
|
||||
"""启动后台任务"""
|
||||
await self.background_task_manager.start_tasks()
|
||||
logger.info("[Heartflow] 后台任务已启动")
|
||||
|
||||
# 根本不会用到这个函数吧,那样麦麦直接死了
|
||||
async def stop_working(self):
|
||||
"""停止所有任务和子心流"""
|
||||
logger.info("[Heartflow] 正在停止任务和子心流...")
|
||||
await self.background_task_manager.stop_tasks()
|
||||
await self.subheartflow_manager.deactivate_all_subflows()
|
||||
logger.info("[Heartflow] 所有任务和子心流已停止")
|
||||
async def force_change_state(self, subflow_id: Any, target_state: ChatState) -> bool:
|
||||
"""强制改变指定子心流的状态"""
|
||||
subflow = self.subheartflows.get(subflow_id)
|
||||
if not subflow:
|
||||
logger.warning(f"[强制状态转换]尝试转换不存在的子心流{subflow_id} 到 {target_state.value}")
|
||||
return False
|
||||
await subflow.change_chat_state(target_state)
|
||||
logger.info(f"[强制状态转换]子心流 {subflow_id} 已转换到 {target_state.value}")
|
||||
return True
|
||||
|
||||
|
||||
heartflow = Heartflow()
|
||||
|
||||
@@ -3,38 +3,21 @@ from src.config.config import global_config
|
||||
from src.chat.message_receive.message import MessageRecv
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.chat.heart_flow.heartflow import heartflow
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager, ChatStream
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.utils.utils import is_mentioned_bot_in_message
|
||||
from src.chat.utils.timer_calculator import Timer
|
||||
from src.common.logger import get_logger
|
||||
|
||||
import math
|
||||
import re
|
||||
import math
|
||||
import traceback
|
||||
from typing import Optional, Tuple
|
||||
from maim_message import UserInfo
|
||||
from typing import Tuple
|
||||
|
||||
from src.person_info.relationship_manager import get_relationship_manager
|
||||
|
||||
# from ..message_receive.message_buffer import message_buffer
|
||||
|
||||
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:
|
||||
"""处理用户关系逻辑
|
||||
|
||||
@@ -90,46 +73,6 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
|
||||
return interested_rate, is_mentioned
|
||||
|
||||
|
||||
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
||||
"""检查消息是否包含过滤词
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否包含过滤词
|
||||
"""
|
||||
for word in global_config.message_receive.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: ChatStream, userinfo: UserInfo) -> bool:
|
||||
"""检查消息是否匹配过滤正则表达式
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否匹配过滤正则
|
||||
"""
|
||||
for pattern in global_config.message_receive.ban_msgs_regex:
|
||||
if re.search(pattern, text):
|
||||
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
|
||||
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
||||
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class HeartFCMessageReceiver:
|
||||
"""心流处理器,负责处理接收到的消息并计算兴趣度"""
|
||||
|
||||
@@ -167,12 +110,6 @@ class HeartFCMessageReceiver:
|
||||
subheartflow = await heartflow.get_or_create_subheartflow(chat.stream_id)
|
||||
message.update_chat_stream(chat)
|
||||
|
||||
# 3. 过滤检查
|
||||
if _check_ban_words(message.processed_plain_text, chat, userinfo) or _check_ban_regex(
|
||||
message.raw_message, chat, userinfo
|
||||
):
|
||||
return
|
||||
|
||||
# 6. 兴趣度计算与更新
|
||||
interested_rate, is_mentioned = await _calculate_interest(message)
|
||||
subheartflow.add_message_to_normal_chat_cache(message, interested_rate, is_mentioned)
|
||||
@@ -183,7 +120,6 @@ class HeartFCMessageReceiver:
|
||||
current_talk_frequency = global_config.chat.get_current_talk_frequency(chat.stream_id)
|
||||
|
||||
# 如果消息中包含图片标识,则日志展示为图片
|
||||
import re
|
||||
|
||||
picid_match = re.search(r"\[picid:([^\]]+)\]", message.processed_plain_text)
|
||||
if picid_match:
|
||||
@@ -198,4 +134,5 @@ class HeartFCMessageReceiver:
|
||||
await _process_relationship(message)
|
||||
|
||||
except Exception as e:
|
||||
await _handle_error(e, "消息处理失败", message)
|
||||
logger.error(f"消息处理失败: {e}")
|
||||
print(traceback.format_exc())
|
||||
@@ -1,46 +0,0 @@
|
||||
# 定义了来自外部世界的信息
|
||||
# 外部世界可以是某个聊天 不同平台的聊天 也可以是任意媒体
|
||||
from datetime import datetime
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
|
||||
logger = get_logger("observation")
|
||||
|
||||
|
||||
# 特殊的观察,专门用于观察动作
|
||||
# 所有观察的基类
|
||||
class ActionObservation:
|
||||
def __init__(self, observe_id):
|
||||
self.observe_info = ""
|
||||
self.observe_id = observe_id
|
||||
self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间
|
||||
self.action_manager: ActionManager = None
|
||||
|
||||
self.all_actions = {}
|
||||
self.all_using_actions = {}
|
||||
|
||||
def get_observe_info(self):
|
||||
return self.observe_info
|
||||
|
||||
def set_action_manager(self, action_manager: ActionManager):
|
||||
self.action_manager = action_manager
|
||||
self.all_actions = self.action_manager.get_registered_actions()
|
||||
|
||||
async def observe(self):
|
||||
action_info_block = ""
|
||||
self.all_using_actions = self.action_manager.get_using_actions()
|
||||
for action_name, action_info in self.all_using_actions.items():
|
||||
action_info_block += f"\n{action_name}: {action_info.get('description', '')}"
|
||||
action_info_block += "\n注意,除了上面动作选项之外,你在群聊里不能做其他任何事情,这是你能力的边界\n"
|
||||
|
||||
self.observe_info = action_info_block
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将观察对象转换为可序列化的字典"""
|
||||
return {
|
||||
"observe_info": self.observe_info,
|
||||
"observe_id": self.observe_id,
|
||||
"last_observe_time": self.last_observe_time,
|
||||
"all_actions": self.all_actions,
|
||||
"all_using_actions": self.all_using_actions,
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
from datetime import datetime
|
||||
from src.config.config import global_config
|
||||
from src.chat.utils.chat_message_builder import (
|
||||
get_raw_msg_before_timestamp_with_chat,
|
||||
build_readable_messages,
|
||||
get_raw_msg_by_timestamp_with_chat,
|
||||
num_new_messages_since,
|
||||
get_person_id_list,
|
||||
)
|
||||
from src.chat.utils.prompt_builder import global_prompt_manager, Prompt
|
||||
from typing import Optional
|
||||
import difflib
|
||||
from src.chat.message_receive.message import MessageRecv
|
||||
from src.chat.heart_flow.observation.observation import Observation
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
|
||||
logger = get_logger("observation")
|
||||
|
||||
# 定义提示模板
|
||||
Prompt(
|
||||
"""这是qq群聊的聊天记录,请总结以下聊天记录的主题:
|
||||
{chat_logs}
|
||||
请概括这段聊天记录的主题和主要内容
|
||||
主题:简短的概括,包括时间,人物和事件,不要超过20个字
|
||||
内容:具体的信息内容,包括人物、事件和信息,不要超过200个字,不要分点。
|
||||
|
||||
请用json格式返回,格式如下:
|
||||
{{
|
||||
"theme": "主题,例如 2025-06-14 10:00:00 群聊 麦麦 和 网友 讨论了 游戏 的话题",
|
||||
"content": "内容,可以是对聊天记录的概括,也可以是聊天记录的详细内容"
|
||||
}}
|
||||
""",
|
||||
"chat_summary_group_prompt", # Template for group chat
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""这是你和{chat_target}的私聊记录,请总结以下聊天记录的主题:
|
||||
{chat_logs}
|
||||
请用一句话概括,包括事件,时间,和主要信息,不要分点。
|
||||
主题:简短的介绍,不要超过10个字
|
||||
内容:包括人物、事件和主要信息,不要分点。
|
||||
|
||||
请用json格式返回,格式如下:
|
||||
{{
|
||||
"theme": "主题",
|
||||
"content": "内容"
|
||||
}}""",
|
||||
"chat_summary_private_prompt", # Template for private chat
|
||||
)
|
||||
|
||||
|
||||
class ChattingObservation(Observation):
|
||||
def __init__(self, chat_id):
|
||||
super().__init__(chat_id)
|
||||
self.chat_id = chat_id
|
||||
self.platform = "qq"
|
||||
|
||||
self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_id)
|
||||
|
||||
self.talking_message = []
|
||||
self.talking_message_str = ""
|
||||
self.talking_message_str_truncate = ""
|
||||
self.talking_message_str_short = ""
|
||||
self.talking_message_str_truncate_short = ""
|
||||
self.name = global_config.bot.nickname
|
||||
self.nick_name = global_config.bot.alias_names
|
||||
self.max_now_obs_len = global_config.chat.max_context_size
|
||||
self.overlap_len = global_config.focus_chat.compressed_length
|
||||
self.person_list = []
|
||||
self.compressor_prompt = ""
|
||||
self.oldest_messages = []
|
||||
self.oldest_messages_str = ""
|
||||
|
||||
self.last_observe_time = datetime.now().timestamp()
|
||||
initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10)
|
||||
initial_messages_short = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 5)
|
||||
self.last_observe_time = initial_messages[-1]["time"] if initial_messages else self.last_observe_time
|
||||
self.talking_message = initial_messages
|
||||
self.talking_message_short = initial_messages_short
|
||||
self.talking_message_str = build_readable_messages(self.talking_message, show_actions=True)
|
||||
self.talking_message_str_truncate = build_readable_messages(
|
||||
self.talking_message, show_actions=True, truncate=True
|
||||
)
|
||||
self.talking_message_str_short = build_readable_messages(self.talking_message_short, show_actions=True)
|
||||
self.talking_message_str_truncate_short = build_readable_messages(
|
||||
self.talking_message_short, show_actions=True, truncate=True
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将观察对象转换为可序列化的字典"""
|
||||
return {
|
||||
"chat_id": self.chat_id,
|
||||
"platform": self.platform,
|
||||
"is_group_chat": self.is_group_chat,
|
||||
"chat_target_info": self.chat_target_info,
|
||||
"talking_message_str": self.talking_message_str,
|
||||
"talking_message_str_truncate": self.talking_message_str_truncate,
|
||||
"talking_message_str_short": self.talking_message_str_short,
|
||||
"talking_message_str_truncate_short": self.talking_message_str_truncate_short,
|
||||
"name": self.name,
|
||||
"nick_name": self.nick_name,
|
||||
"last_observe_time": self.last_observe_time,
|
||||
}
|
||||
|
||||
def get_observe_info(self, ids=None):
|
||||
return self.talking_message_str
|
||||
|
||||
def get_recv_message_by_text(self, sender: str, text: str) -> Optional[MessageRecv]:
|
||||
"""
|
||||
根据回复的纯文本
|
||||
1. 在talking_message中查找最新的,最匹配的消息
|
||||
2. 如果找到,则返回消息
|
||||
"""
|
||||
find_msg = None
|
||||
reverse_talking_message = list(reversed(self.talking_message))
|
||||
|
||||
for message in reverse_talking_message:
|
||||
user_id = message["user_id"]
|
||||
platform = message["platform"]
|
||||
person_id = get_person_info_manager().get_person_id(platform, user_id)
|
||||
person_name = get_person_info_manager().get_value(person_id, "person_name")
|
||||
if person_name == sender:
|
||||
similarity = difflib.SequenceMatcher(None, text, message["processed_plain_text"]).ratio()
|
||||
if similarity >= 0.9:
|
||||
find_msg = message
|
||||
break
|
||||
|
||||
if not find_msg:
|
||||
return None
|
||||
|
||||
user_info = {
|
||||
"platform": find_msg.get("user_platform", ""),
|
||||
"user_id": find_msg.get("user_id", ""),
|
||||
"user_nickname": find_msg.get("user_nickname", ""),
|
||||
"user_cardname": find_msg.get("user_cardname", ""),
|
||||
}
|
||||
|
||||
group_info = {}
|
||||
if find_msg.get("chat_info_group_id"):
|
||||
group_info = {
|
||||
"platform": find_msg.get("chat_info_group_platform", ""),
|
||||
"group_id": find_msg.get("chat_info_group_id", ""),
|
||||
"group_name": find_msg.get("chat_info_group_name", ""),
|
||||
}
|
||||
|
||||
content_format = ""
|
||||
accept_format = ""
|
||||
template_items = {}
|
||||
|
||||
format_info = {"content_format": content_format, "accept_format": accept_format}
|
||||
template_info = {
|
||||
"template_items": template_items,
|
||||
}
|
||||
|
||||
message_info = {
|
||||
"platform": self.platform,
|
||||
"message_id": find_msg.get("message_id"),
|
||||
"time": find_msg.get("time"),
|
||||
"group_info": group_info,
|
||||
"user_info": user_info,
|
||||
"additional_config": find_msg.get("additional_config"),
|
||||
"format_info": format_info,
|
||||
"template_info": template_info,
|
||||
}
|
||||
message_dict = {
|
||||
"message_info": message_info,
|
||||
"raw_message": find_msg.get("processed_plain_text"),
|
||||
"detailed_plain_text": find_msg.get("processed_plain_text"),
|
||||
"processed_plain_text": find_msg.get("processed_plain_text"),
|
||||
}
|
||||
find_rec_msg = MessageRecv(message_dict)
|
||||
|
||||
find_rec_msg.update_chat_stream(get_chat_manager().get_or_create_stream(self.chat_id))
|
||||
|
||||
return find_rec_msg
|
||||
|
||||
async def observe(self):
|
||||
# 自上一次观察的新消息
|
||||
new_messages_list = get_raw_msg_by_timestamp_with_chat(
|
||||
chat_id=self.chat_id,
|
||||
timestamp_start=self.last_observe_time,
|
||||
timestamp_end=datetime.now().timestamp(),
|
||||
limit=self.max_now_obs_len,
|
||||
limit_mode="latest",
|
||||
)
|
||||
|
||||
# print(f"new_messages_list: {new_messages_list}")
|
||||
|
||||
last_obs_time_mark = self.last_observe_time
|
||||
if new_messages_list:
|
||||
self.last_observe_time = new_messages_list[-1]["time"]
|
||||
self.talking_message.extend(new_messages_list)
|
||||
|
||||
if len(self.talking_message) > self.max_now_obs_len:
|
||||
# 计算需要移除的消息数量,保留最新的 max_now_obs_len 条
|
||||
messages_to_remove_count = len(self.talking_message) - self.max_now_obs_len
|
||||
oldest_messages = self.talking_message[:messages_to_remove_count]
|
||||
self.talking_message = self.talking_message[messages_to_remove_count:]
|
||||
|
||||
# 构建压缩提示
|
||||
oldest_messages_str = build_readable_messages(
|
||||
messages=oldest_messages, timestamp_mode="normal_no_YMD", read_mark=0, show_actions=True
|
||||
)
|
||||
|
||||
# 根据聊天类型选择提示模板
|
||||
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:
|
||||
prompt_template_name = "chat_summary_private_prompt"
|
||||
chat_target_name = "对方"
|
||||
if self.chat_target_info:
|
||||
chat_target_name = (
|
||||
self.chat_target_info.get("person_name")
|
||||
or self.chat_target_info.get("user_nickname")
|
||||
or chat_target_name
|
||||
)
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
prompt_template_name,
|
||||
chat_target=chat_target_name,
|
||||
chat_logs=oldest_messages_str,
|
||||
)
|
||||
|
||||
self.compressor_prompt = prompt
|
||||
|
||||
# 构建当前消息
|
||||
self.talking_message_str = build_readable_messages(
|
||||
messages=self.talking_message,
|
||||
timestamp_mode="lite",
|
||||
read_mark=last_obs_time_mark,
|
||||
show_actions=True,
|
||||
)
|
||||
self.talking_message_str_truncate = build_readable_messages(
|
||||
messages=self.talking_message,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
read_mark=last_obs_time_mark,
|
||||
truncate=True,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
# 构建简短版本 - 使用最新一半的消息
|
||||
half_count = len(self.talking_message) // 2
|
||||
recent_messages = self.talking_message[-half_count:] if half_count > 0 else self.talking_message
|
||||
|
||||
self.talking_message_str_short = build_readable_messages(
|
||||
messages=recent_messages,
|
||||
timestamp_mode="lite",
|
||||
read_mark=last_obs_time_mark,
|
||||
show_actions=True,
|
||||
)
|
||||
self.talking_message_str_truncate_short = build_readable_messages(
|
||||
messages=recent_messages,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
read_mark=last_obs_time_mark,
|
||||
truncate=True,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
self.person_list = await get_person_id_list(self.talking_message)
|
||||
|
||||
# logger.debug(
|
||||
# f"Chat {self.chat_id} - 现在聊天内容:{self.talking_message_str}"
|
||||
# )
|
||||
|
||||
async def has_new_messages_since(self, timestamp: float) -> bool:
|
||||
"""检查指定时间戳之后是否有新消息"""
|
||||
count = num_new_messages_since(chat_id=self.chat_id, timestamp_start=timestamp)
|
||||
return count > 0
|
||||
@@ -1,25 +0,0 @@
|
||||
# 定义了来自外部世界的信息
|
||||
# 外部世界可以是某个聊天 不同平台的聊天 也可以是任意媒体
|
||||
from datetime import datetime
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("observation")
|
||||
|
||||
|
||||
# 所有观察的基类
|
||||
class Observation:
|
||||
def __init__(self, observe_id):
|
||||
self.observe_info = ""
|
||||
self.observe_id = observe_id
|
||||
self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将观察对象转换为可序列化的字典"""
|
||||
return {
|
||||
"observe_info": self.observe_info,
|
||||
"observe_id": self.observe_id,
|
||||
"last_observe_time": self.last_observe_time,
|
||||
}
|
||||
|
||||
async def observe(self):
|
||||
pass
|
||||
@@ -1,34 +0,0 @@
|
||||
# 定义了来自外部世界的信息
|
||||
# 外部世界可以是某个聊天 不同平台的聊天 也可以是任意媒体
|
||||
from datetime import datetime
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.focus_chat.working_memory.working_memory import WorkingMemory
|
||||
from src.chat.focus_chat.working_memory.memory_item import MemoryItem
|
||||
from typing import List
|
||||
# Import the new utility function
|
||||
|
||||
logger = get_logger("observation")
|
||||
|
||||
|
||||
# 所有观察的基类
|
||||
class WorkingMemoryObservation:
|
||||
def __init__(self, observe_id):
|
||||
self.observe_info = ""
|
||||
self.observe_id = observe_id
|
||||
self.last_observe_time = datetime.now().timestamp()
|
||||
|
||||
self.working_memory = WorkingMemory(chat_id=observe_id)
|
||||
|
||||
self.retrieved_working_memory = []
|
||||
|
||||
def get_observe_info(self):
|
||||
return self.working_memory
|
||||
|
||||
def add_retrieved_working_memory(self, retrieved_working_memory: List[MemoryItem]):
|
||||
self.retrieved_working_memory.append(retrieved_working_memory)
|
||||
|
||||
def get_retrieved_working_memory(self):
|
||||
return self.retrieved_working_memory
|
||||
|
||||
async def observe(self):
|
||||
pass
|
||||
@@ -1,5 +1,3 @@
|
||||
from .observation.observation import Observation
|
||||
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
@@ -10,7 +8,7 @@ from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.focus_chat.heartFC_chat import HeartFChatting
|
||||
from src.chat.normal_chat.normal_chat import NormalChat
|
||||
from src.chat.heart_flow.chat_state_info import ChatState, ChatStateInfo
|
||||
from .utils_chat import get_chat_type_and_target_info
|
||||
from src.chat.utils.utils import get_chat_type_and_target_info
|
||||
from src.config.config import global_config
|
||||
from rich.traceback import install
|
||||
|
||||
@@ -46,10 +44,6 @@ class SubHeartflow:
|
||||
# 兴趣消息集合
|
||||
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
|
||||
|
||||
# 活动状态管理
|
||||
self.should_stop = False # 停止标志
|
||||
self.task: Optional[asyncio.Task] = None # 后台任务
|
||||
|
||||
# focus模式退出冷却时间管理
|
||||
self.last_focus_exit_time: float = 0 # 上次退出focus模式的时间
|
||||
|
||||
@@ -62,7 +56,10 @@ class SubHeartflow:
|
||||
"""异步初始化方法,创建兴趣流并确定聊天类型"""
|
||||
|
||||
# 根据配置决定初始状态
|
||||
if global_config.chat.chat_mode == "focus":
|
||||
if not self.is_group_chat:
|
||||
logger.debug(f"{self.log_prefix} 检测到是私聊,将直接尝试进入 FOCUSED 状态。")
|
||||
await self.change_chat_state(ChatState.FOCUSED)
|
||||
elif global_config.chat.chat_mode == "focus":
|
||||
logger.debug(f"{self.log_prefix} 配置为 focus 模式,将直接尝试进入 FOCUSED 状态。")
|
||||
await self.change_chat_state(ChatState.FOCUSED)
|
||||
else: # "auto" 或其他模式保持原有逻辑或默认为 NORMAL
|
||||
@@ -123,6 +120,7 @@ class SubHeartflow:
|
||||
chat_stream=chat_stream,
|
||||
interest_dict=self.interest_dict,
|
||||
on_switch_to_focus_callback=self._handle_switch_to_focus_request,
|
||||
get_cooldown_progress_callback=self.get_cooldown_progress,
|
||||
)
|
||||
|
||||
logger.info(f"{log_prefix} 开始普通聊天,随便水群...")
|
||||
@@ -134,27 +132,31 @@ class SubHeartflow:
|
||||
self.normal_chat_instance = None # 启动/初始化失败,清理实例
|
||||
return False
|
||||
|
||||
async def _handle_switch_to_focus_request(self) -> None:
|
||||
async def _handle_switch_to_focus_request(self) -> bool:
|
||||
"""
|
||||
处理来自NormalChat的切换到focus模式的请求
|
||||
|
||||
Args:
|
||||
stream_id: 请求切换的stream_id
|
||||
Returns:
|
||||
bool: 切换成功返回True,失败返回False
|
||||
"""
|
||||
logger.info(f"{self.log_prefix} 收到NormalChat请求切换到focus模式")
|
||||
|
||||
# 检查是否在focus冷却期内
|
||||
if self.is_in_focus_cooldown():
|
||||
logger.info(f"{self.log_prefix} 正在focus冷却期内,忽略切换到focus模式的请求")
|
||||
return
|
||||
return False
|
||||
|
||||
# 切换到focus模式
|
||||
current_state = self.chat_state.chat_status
|
||||
if current_state == ChatState.NORMAL:
|
||||
await self.change_chat_state(ChatState.FOCUSED)
|
||||
logger.info(f"{self.log_prefix} 已根据NormalChat请求从NORMAL切换到FOCUSED状态")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 当前状态为{current_state.value},无法切换到FOCUSED状态")
|
||||
return False
|
||||
|
||||
async def _handle_stop_focus_chat_request(self) -> None:
|
||||
"""
|
||||
@@ -205,10 +207,6 @@ class SubHeartflow:
|
||||
await asyncio.wait_for(self.heart_fc_instance.start(), timeout=15.0)
|
||||
logger.info(f"{log_prefix} HeartFChatting 循环已启动。")
|
||||
return True
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"{log_prefix} 启动现有 HeartFChatting 循环超时")
|
||||
# 超时时清理实例,准备重新创建
|
||||
self.heart_fc_instance = None
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 尝试启动现有 HeartFChatting 循环时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
@@ -225,7 +223,6 @@ class SubHeartflow:
|
||||
logger.debug(f"{log_prefix} 创建新的 HeartFChatting 实例")
|
||||
self.heart_fc_instance = HeartFChatting(
|
||||
chat_id=self.subheartflow_id,
|
||||
# observations=self.observations,
|
||||
on_stop_focus_chat=self._handle_stop_focus_chat_request,
|
||||
)
|
||||
|
||||
@@ -235,10 +232,6 @@ class SubHeartflow:
|
||||
logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
|
||||
return True
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"{log_prefix} 创建或启动新 HeartFChatting 实例超时")
|
||||
self.heart_fc_instance = None # 超时时清理实例
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"{log_prefix} 创建或启动 HeartFChatting 实例时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
@@ -249,8 +242,6 @@ class SubHeartflow:
|
||||
logger.error(f"{self.log_prefix} _start_heart_fc_chat 执行时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
logger.debug(f"{self.log_prefix} _start_heart_fc_chat 完成")
|
||||
|
||||
async def change_chat_state(self, new_state: ChatState) -> None:
|
||||
"""
|
||||
@@ -306,43 +297,6 @@ class SubHeartflow:
|
||||
f"{log_prefix} 尝试将状态从 {current_state.value} 变为 {new_state.value},但未成功或未执行更改。"
|
||||
)
|
||||
|
||||
def add_observation(self, observation: Observation):
|
||||
for existing_obs in self.observations:
|
||||
if existing_obs.observe_id == observation.observe_id:
|
||||
return
|
||||
self.observations.append(observation)
|
||||
|
||||
def remove_observation(self, observation: Observation):
|
||||
if observation in self.observations:
|
||||
self.observations.remove(observation)
|
||||
|
||||
def get_all_observations(self) -> list[Observation]:
|
||||
return self.observations
|
||||
|
||||
def _get_primary_observation(self) -> Optional[ChattingObservation]:
|
||||
if self.observations and isinstance(self.observations[0], ChattingObservation):
|
||||
return self.observations[0]
|
||||
logger.warning(f"SubHeartflow {self.subheartflow_id} 没有找到有效的 ChattingObservation")
|
||||
return None
|
||||
|
||||
def get_normal_chat_last_speak_time(self) -> float:
|
||||
if self.normal_chat_instance:
|
||||
return self.normal_chat_instance.last_speak_time
|
||||
return 0
|
||||
|
||||
def get_normal_chat_recent_replies(self, limit: int = 10) -> List[dict]:
|
||||
"""获取NormalChat实例的最近回复记录
|
||||
|
||||
Args:
|
||||
limit: 最大返回数量,默认10条
|
||||
|
||||
Returns:
|
||||
List[dict]: 最近的回复记录列表,如果没有NormalChat实例则返回空列表
|
||||
"""
|
||||
if self.normal_chat_instance:
|
||||
return self.normal_chat_instance.get_recent_replies(limit)
|
||||
return []
|
||||
|
||||
def add_message_to_normal_chat_cache(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
|
||||
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
|
||||
# 如果字典长度超过10,删除最旧的消息
|
||||
@@ -350,66 +304,6 @@ class SubHeartflow:
|
||||
oldest_key = next(iter(self.interest_dict))
|
||||
self.interest_dict.pop(oldest_key)
|
||||
|
||||
def get_normal_chat_action_manager(self):
|
||||
"""获取NormalChat的ActionManager实例
|
||||
|
||||
Returns:
|
||||
ActionManager: NormalChat的ActionManager实例,如果不存在则返回None
|
||||
"""
|
||||
if self.normal_chat_instance:
|
||||
return self.normal_chat_instance.get_action_manager()
|
||||
return None
|
||||
|
||||
def set_normal_chat_planner_enabled(self, enabled: bool):
|
||||
"""设置NormalChat的planner是否启用
|
||||
|
||||
Args:
|
||||
enabled: 是否启用planner
|
||||
"""
|
||||
if self.normal_chat_instance:
|
||||
self.normal_chat_instance.set_planner_enabled(enabled)
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} NormalChat实例不存在,无法设置planner状态")
|
||||
|
||||
async def get_full_state(self) -> dict:
|
||||
"""获取子心流的完整状态,包括兴趣、思维和聊天状态。"""
|
||||
return {
|
||||
"interest_state": "interest_state",
|
||||
"chat_state": self.chat_state.chat_status.value,
|
||||
"chat_state_changed_time": self.chat_state_changed_time,
|
||||
}
|
||||
|
||||
async def shutdown(self):
|
||||
"""安全地关闭子心流及其管理的任务"""
|
||||
if self.should_stop:
|
||||
logger.info(f"{self.log_prefix} 子心流已在关闭过程中。")
|
||||
return
|
||||
|
||||
logger.info(f"{self.log_prefix} 开始关闭子心流...")
|
||||
self.should_stop = True # 标记为停止,让后台任务退出
|
||||
|
||||
# 使用新的停止方法
|
||||
await self._stop_normal_chat()
|
||||
await self._stop_heart_fc_chat()
|
||||
|
||||
# 取消可能存在的旧后台任务 (self.task)
|
||||
if self.task and not self.task.done():
|
||||
logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
|
||||
self.task.cancel()
|
||||
try:
|
||||
await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。")
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 等待子心流主任务取消时发生错误 (Shutdown): {e}")
|
||||
|
||||
self.task = None # 清理任务引用
|
||||
self.chat_state.chat_status = ChatState.ABSENT # 状态重置为不参与
|
||||
|
||||
logger.info(f"{self.log_prefix} 子心流关闭完成。")
|
||||
|
||||
def is_in_focus_cooldown(self) -> bool:
|
||||
"""检查是否在focus模式的冷却期内
|
||||
|
||||
@@ -436,3 +330,26 @@ class SubHeartflow:
|
||||
)
|
||||
|
||||
return is_cooling
|
||||
|
||||
def get_cooldown_progress(self) -> float:
|
||||
"""获取冷却进度,返回0-1之间的值
|
||||
|
||||
Returns:
|
||||
float: 0表示刚开始冷却,1表示冷却完成
|
||||
"""
|
||||
if self.last_focus_exit_time == 0:
|
||||
return 1.0 # 没有冷却,返回1表示完全恢复
|
||||
|
||||
# 基础冷却时间10分钟,受auto_focus_threshold调控
|
||||
base_cooldown = 10 * 60 # 10分钟转换为秒
|
||||
cooldown_duration = base_cooldown / global_config.chat.auto_focus_threshold
|
||||
|
||||
current_time = time.time()
|
||||
elapsed_since_exit = current_time - self.last_focus_exit_time
|
||||
|
||||
if elapsed_since_exit >= cooldown_duration:
|
||||
return 1.0 # 冷却完成
|
||||
|
||||
# 计算进度:0表示刚开始冷却,1表示冷却完成
|
||||
progress = elapsed_since_exit / cooldown_duration
|
||||
return progress
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState
|
||||
|
||||
|
||||
# 初始化日志记录器
|
||||
|
||||
logger = get_logger("subheartflow_manager")
|
||||
|
||||
# 子心流管理相关常量
|
||||
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 = get_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 实例。"""
|
||||
|
||||
def __init__(self):
|
||||
self.subheartflows: Dict[Any, "SubHeartflow"] = {}
|
||||
self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问
|
||||
|
||||
async def force_change_state(self, subflow_id: Any, target_state: ChatState) -> bool:
|
||||
"""强制改变指定子心流的状态"""
|
||||
async with self._lock:
|
||||
subflow = self.subheartflows.get(subflow_id)
|
||||
if not subflow:
|
||||
logger.warning(f"[强制状态转换]尝试转换不存在的子心流{subflow_id} 到 {target_state.value}")
|
||||
return False
|
||||
await subflow.change_chat_state(target_state)
|
||||
logger.info(f"[强制状态转换]子心流 {subflow_id} 已转换到 {target_state.value}")
|
||||
return True
|
||||
|
||||
def get_all_subheartflows(self) -> List["SubHeartflow"]:
|
||||
"""获取所有当前管理的 SubHeartflow 实例列表 (快照)。"""
|
||||
return list(self.subheartflows.values())
|
||||
|
||||
async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
|
||||
"""获取或创建指定ID的子心流实例
|
||||
|
||||
Args:
|
||||
subheartflow_id: 子心流唯一标识符
|
||||
mai_states 参数已被移除,使用 self.mai_state_info
|
||||
|
||||
Returns:
|
||||
成功返回SubHeartflow实例,失败返回None
|
||||
"""
|
||||
async with self._lock:
|
||||
# 检查是否已存在该子心流
|
||||
if subheartflow_id in self.subheartflows:
|
||||
subflow = self.subheartflows[subheartflow_id]
|
||||
if subflow.should_stop:
|
||||
logger.warning(f"尝试获取已停止的子心流 {subheartflow_id},正在重新激活")
|
||||
subflow.should_stop = False # 重置停止标志
|
||||
return subflow
|
||||
|
||||
try:
|
||||
# 初始化子心流, 传入 mai_state_info
|
||||
new_subflow = SubHeartflow(
|
||||
subheartflow_id,
|
||||
)
|
||||
|
||||
# 首先创建并添加聊天观察者
|
||||
# observation = ChattingObservation(chat_id=subheartflow_id)
|
||||
# await observation.initialize()
|
||||
# new_subflow.add_observation(observation)
|
||||
|
||||
# 然后再进行异步初始化,此时 SubHeartflow 内部若需启动 HeartFChatting,就能拿到 observation
|
||||
await new_subflow.initialize()
|
||||
|
||||
# 注册子心流
|
||||
self.subheartflows[subheartflow_id] = new_subflow
|
||||
heartflow_name = get_chat_manager().get_stream_name(subheartflow_id) or subheartflow_id
|
||||
logger.info(f"[{heartflow_name}] 开始接收消息")
|
||||
|
||||
return new_subflow
|
||||
except Exception as e:
|
||||
logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def sleep_subheartflow(self, subheartflow_id: Any, reason: str) -> bool:
|
||||
"""停止指定的子心流并将其状态设置为 ABSENT"""
|
||||
log_prefix = "[子心流管理]"
|
||||
async with self._lock: # 加锁以安全访问字典
|
||||
subheartflow = self.subheartflows.get(subheartflow_id)
|
||||
|
||||
stream_name = get_chat_manager().get_stream_name(subheartflow_id) or subheartflow_id
|
||||
logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}")
|
||||
|
||||
# 调用内部方法处理状态变更
|
||||
success = await _try_set_subflow_absent_internal(subheartflow, log_prefix)
|
||||
|
||||
return success
|
||||
# 锁在此处自动释放
|
||||
|
||||
def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
|
||||
"""识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)"""
|
||||
_current_time = time.time()
|
||||
flows_to_stop = []
|
||||
|
||||
for subheartflow_id, subheartflow in list(self.subheartflows.items()):
|
||||
state = subheartflow.chat_state.chat_status
|
||||
if state != ChatState.ABSENT:
|
||||
continue
|
||||
subheartflow.update_last_chat_state_time()
|
||||
_absent_last_time = subheartflow.chat_state_last_time
|
||||
flows_to_stop.append(subheartflow_id)
|
||||
|
||||
return flows_to_stop
|
||||
|
||||
async def deactivate_all_subflows(self):
|
||||
"""将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)"""
|
||||
log_prefix = "[停用]"
|
||||
changed_count = 0
|
||||
processed_count = 0
|
||||
|
||||
async with self._lock: # 获取锁以安全迭代
|
||||
# 使用 list() 创建一个当前值的快照,防止在迭代时修改字典
|
||||
flows_to_update = list(self.subheartflows.values())
|
||||
processed_count = len(flows_to_update)
|
||||
if not flows_to_update:
|
||||
logger.debug(f"{log_prefix} 无活跃子心流,无需操作")
|
||||
return
|
||||
|
||||
for subflow in flows_to_update:
|
||||
# 记录原始状态,以便统计实际改变的数量
|
||||
original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT
|
||||
|
||||
success = await _try_set_subflow_absent_internal(subflow, log_prefix)
|
||||
|
||||
# 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数
|
||||
if success and not original_state_was_absent:
|
||||
if subflow.chat_state.chat_status == ChatState.ABSENT:
|
||||
changed_count += 1
|
||||
else:
|
||||
# 这种情况理论上不应发生,如果内部方法返回 True 的话
|
||||
stream_name = (
|
||||
get_chat_manager().get_stream_name(subflow.subheartflow_id) or subflow.subheartflow_id
|
||||
)
|
||||
logger.warning(f"{log_prefix} 内部方法声称成功但 {stream_name} 状态未变为 ABSENT。")
|
||||
# 锁在此处自动释放
|
||||
|
||||
logger.info(
|
||||
f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。"
|
||||
)
|
||||
|
||||
# async def sbhf_normal_into_focus(self):
|
||||
# """评估子心流兴趣度,满足条件则提升到FOCUSED状态(基于start_hfc_probability)"""
|
||||
# try:
|
||||
# for sub_hf in list(self.subheartflows.values()):
|
||||
# flow_id = sub_hf.subheartflow_id
|
||||
# stream_name = get_chat_manager().get_stream_name(flow_id) or flow_id
|
||||
|
||||
# # 跳过已经是FOCUSED状态的子心流
|
||||
# if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
|
||||
# continue
|
||||
|
||||
# if sub_hf.interest_chatting.start_hfc_probability == 0:
|
||||
# continue
|
||||
# else:
|
||||
# logger.debug(
|
||||
# f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}"
|
||||
# )
|
||||
|
||||
# if random.random() >= sub_hf.interest_chatting.start_hfc_probability:
|
||||
# continue
|
||||
|
||||
# # 获取最新状态并执行提升
|
||||
# current_subflow = self.subheartflows.get(flow_id)
|
||||
# if not current_subflow:
|
||||
# continue
|
||||
|
||||
# logger.info(
|
||||
# f"{stream_name} 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})"
|
||||
# )
|
||||
|
||||
# # 执行状态提升
|
||||
# await current_subflow.change_chat_state(ChatState.FOCUSED)
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
|
||||
|
||||
async def sbhf_focus_into_normal(self, subflow_id: Any):
|
||||
"""
|
||||
接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 NORMAL。
|
||||
通常在连续多次 "no_reply" 后被调用。
|
||||
对于私聊和群聊,都转换为 NORMAL。
|
||||
|
||||
Args:
|
||||
subflow_id: 需要转换状态的子心流 ID。
|
||||
"""
|
||||
async with self._lock:
|
||||
subflow = self.subheartflows.get(subflow_id)
|
||||
if not subflow:
|
||||
logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 NORMAL")
|
||||
return
|
||||
|
||||
stream_name = get_chat_manager().get_stream_name(subflow_id) or subflow_id
|
||||
current_state = subflow.chat_state.chat_status
|
||||
|
||||
if current_state == ChatState.FOCUSED:
|
||||
target_state = ChatState.NORMAL
|
||||
log_reason = "转为NORMAL"
|
||||
|
||||
logger.info(
|
||||
f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
|
||||
)
|
||||
try:
|
||||
# 从HFC到CHAT时,清空兴趣字典
|
||||
subflow.interest_dict.clear()
|
||||
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}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"[状态转换请求] 尝试将 {stream_name} 转换为 {target_state.value} 后,状态实际为 {final_state.value}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[状态转换请求] 转换 {stream_name} 到 {target_state.value} 时出错: {e}", exc_info=True
|
||||
)
|
||||
elif current_state == ChatState.ABSENT:
|
||||
logger.debug(f"[状态转换请求] {stream_name} 处于 ABSENT 状态,尝试转为 NORMAL")
|
||||
await subflow.change_chat_state(ChatState.NORMAL)
|
||||
else:
|
||||
logger.debug(f"[状态转换请求] {stream_name} 当前状态为 {current_state.value},无需转换")
|
||||
|
||||
async def delete_subflow(self, subheartflow_id: Any):
|
||||
"""删除指定的子心流。"""
|
||||
async with self._lock:
|
||||
subflow = self.subheartflows.pop(subheartflow_id, None)
|
||||
if subflow:
|
||||
logger.info(f"正在删除 SubHeartflow: {subheartflow_id}...")
|
||||
try:
|
||||
# 调用 shutdown 方法确保资源释放
|
||||
await subflow.shutdown()
|
||||
logger.info(f"SubHeartflow {subheartflow_id} 已成功删除。")
|
||||
except Exception as e:
|
||||
logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True)
|
||||
else:
|
||||
logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}")
|
||||
|
||||
# --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- #
|
||||
async def sbhf_absent_private_into_focus(self):
|
||||
"""检查 ABSENT 状态的私聊子心流是否有新活动,若有则直接转换为 FOCUSED。"""
|
||||
log_prefix_task = "[私聊激活检查]"
|
||||
transitioned_count = 0
|
||||
checked_count = 0
|
||||
|
||||
async with self._lock:
|
||||
# --- 筛选出所有 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:
|
||||
flow_id = sub_hf.subheartflow_id
|
||||
stream_name = get_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} 检测到新消息,标记为活跃。")
|
||||
else:
|
||||
logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。")
|
||||
|
||||
# --- 如果活跃,则尝试转换 --- #
|
||||
if is_active:
|
||||
await sub_hf.change_chat_state(ChatState.FOCUSED)
|
||||
# 确认转换成功
|
||||
if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
|
||||
transitioned_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。"
|
||||
)
|
||||
@@ -1,73 +0,0 @@
|
||||
from typing import Optional, Tuple, Dict
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
|
||||
|
||||
logger = get_logger("heartflow_utils")
|
||||
|
||||
|
||||
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 = get_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 = PersonInfoManager.get_person_id(platform, user_id)
|
||||
person_name = None
|
||||
if person_id:
|
||||
# get_value is async, so await it directly
|
||||
person_info_manager = get_person_info_manager()
|
||||
person_name = person_info_manager.get_value_sync(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
|
||||
@@ -5,11 +5,14 @@ from src.chat.knowledge.mem_active_manager import MemoryActiveManager
|
||||
from src.chat.knowledge.qa_manager import QAManager
|
||||
from src.chat.knowledge.kg_manager import KGManager
|
||||
from src.chat.knowledge.global_logger import logger
|
||||
from src.config.config import global_config as bot_global_config
|
||||
# try:
|
||||
# import quick_algo
|
||||
# except ImportError:
|
||||
# print("quick_algo not found, please install it first")
|
||||
|
||||
# 检查LPMM知识库是否启用
|
||||
if bot_global_config.lpmm_knowledge.enable:
|
||||
logger.info("正在初始化Mai-LPMM\n")
|
||||
logger.info("创建LLM客户端")
|
||||
llm_client_list = dict()
|
||||
@@ -41,7 +44,6 @@ logger.info("KG加载完成")
|
||||
logger.info(f"KG节点数量:{len(kg_manager.graph.get_node_list())}")
|
||||
logger.info(f"KG边数量:{len(kg_manager.graph.get_edge_list())}")
|
||||
|
||||
|
||||
# 数据比对:Embedding库与KG的段落hash集合
|
||||
for pg_hash in kg_manager.stored_paragraph_hashes:
|
||||
key = PG_NAMESPACE + "-" + pg_hash
|
||||
@@ -62,3 +64,8 @@ inspire_manager = MemoryActiveManager(
|
||||
embed_manager,
|
||||
llm_client_list[global_config["embedding"]["provider"]],
|
||||
)
|
||||
else:
|
||||
logger.info("LPMM知识库已禁用,跳过初始化")
|
||||
# 创建空的占位符对象,避免导入错误
|
||||
qa_manager = None
|
||||
inspire_manager = None
|
||||
|
||||
@@ -784,12 +784,12 @@ class Hippocampus:
|
||||
|
||||
# 计算激活节点数与总节点数的比值
|
||||
total_activation = sum(activate_map.values())
|
||||
logger.debug(f"总激活值: {total_activation:.2f}")
|
||||
# logger.debug(f"总激活值: {total_activation:.2f}")
|
||||
total_nodes = len(self.memory_graph.G.nodes())
|
||||
# activated_nodes = len(activate_map)
|
||||
activation_ratio = total_activation / total_nodes if total_nodes > 0 else 0
|
||||
activation_ratio = activation_ratio * 60
|
||||
logger.info(f"总激活值: {total_activation:.2f}, 总节点数: {total_nodes}, 激活: {activation_ratio}")
|
||||
logger.debug(f"总激活值: {total_activation:.2f}, 总节点数: {total_nodes}, 激活: {activation_ratio}")
|
||||
|
||||
return activation_ratio
|
||||
|
||||
|
||||
@@ -46,9 +46,12 @@ def init_prompt():
|
||||
# --- Group Chat Prompt ---
|
||||
memory_activator_prompt = """
|
||||
你是一个记忆分析器,你需要根据以下信息来进行回忆
|
||||
以下是一场聊天中的信息,请根据这些信息,总结出几个关键词作为记忆回忆的触发词
|
||||
以下是一段聊天记录,请根据这些信息,总结出几个关键词作为记忆回忆的触发词
|
||||
|
||||
聊天记录:
|
||||
{obs_info_text}
|
||||
你想要回复的消息:
|
||||
{target_message}
|
||||
|
||||
历史关键词(请避免重复提取这些关键词):
|
||||
{cached_keywords}
|
||||
@@ -66,51 +69,37 @@ def init_prompt():
|
||||
class MemoryActivator:
|
||||
def __init__(self):
|
||||
# TODO: API-Adapter修改标记
|
||||
self.summary_model = LLMRequest(
|
||||
model=global_config.model.memory_summary,
|
||||
temperature=0.7,
|
||||
request_type="focus.memory_activator",
|
||||
|
||||
self.key_words_model = LLMRequest(
|
||||
model=global_config.model.utils_small,
|
||||
temperature=0.5,
|
||||
request_type="memory_activator",
|
||||
)
|
||||
|
||||
self.running_memory = []
|
||||
self.cached_keywords = set() # 用于缓存历史关键词
|
||||
|
||||
async def activate_memory_with_chat_history(self, chat_id, target_message, chat_history_prompt) -> List[Dict]:
|
||||
async def activate_memory_with_chat_history(self, target_message, chat_history_prompt) -> List[Dict]:
|
||||
"""
|
||||
激活记忆
|
||||
|
||||
Args:
|
||||
observations: 现有的进行观察后的 观察列表
|
||||
|
||||
Returns:
|
||||
List[Dict]: 激活的记忆列表
|
||||
"""
|
||||
# 如果记忆系统被禁用,直接返回空列表
|
||||
if not global_config.memory.enable_memory:
|
||||
return []
|
||||
|
||||
# obs_info_text = ""
|
||||
# for observation in observations:
|
||||
# if isinstance(observation, ChattingObservation):
|
||||
# obs_info_text += observation.talking_message_str_truncate_short
|
||||
# elif isinstance(observation, StructureObservation):
|
||||
# working_info = observation.get_observe_info()
|
||||
# for working_info_item in working_info:
|
||||
# obs_info_text += f"{working_info_item['type']}: {working_info_item['content']}\n"
|
||||
|
||||
# logger.info(f"回忆待检索内容:obs_info_text: {obs_info_text}")
|
||||
|
||||
# 将缓存的关键词转换为字符串,用于prompt
|
||||
cached_keywords_str = ", ".join(self.cached_keywords) if self.cached_keywords else "暂无历史关键词"
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
"memory_activator_prompt",
|
||||
obs_info_text=chat_history_prompt,
|
||||
target_message=target_message,
|
||||
cached_keywords=cached_keywords_str,
|
||||
)
|
||||
|
||||
# logger.debug(f"prompt: {prompt}")
|
||||
|
||||
response, (reasoning_content, model_name) = await self.summary_model.generate_response_async(prompt)
|
||||
response, (reasoning_content, model_name) = await self.key_words_model.generate_response_async(prompt)
|
||||
|
||||
keywords = list(get_keywords_from_json(response))
|
||||
|
||||
@@ -124,17 +113,13 @@ class MemoryActivator:
|
||||
|
||||
# 添加新的关键词到缓存
|
||||
self.cached_keywords.update(keywords)
|
||||
logger.info(f"当前激活的记忆关键词: {self.cached_keywords}")
|
||||
|
||||
# 调用记忆系统获取相关记忆
|
||||
related_memory = await hippocampus_manager.get_memory_from_topic(
|
||||
valid_keywords=keywords, max_memory_num=3, max_memory_length=2, max_depth=3
|
||||
)
|
||||
# related_memory = await hippocampus_manager.get_memory_from_text(
|
||||
# text=obs_info_text, max_memory_num=5, max_memory_length=2, max_depth=3, fast_retrieval=False
|
||||
# )
|
||||
|
||||
logger.info(f"获取到的记忆: {related_memory}")
|
||||
logger.info(f"当前记忆关键词: {self.cached_keywords} 。获取到的记忆: {related_memory}")
|
||||
|
||||
# 激活时,所有已有记忆的duration+1,达到3则移除
|
||||
for m in self.running_memory[:]:
|
||||
@@ -1,6 +1,6 @@
|
||||
from src.chat.emoji_system.emoji_manager import get_emoji_manager
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.message_receive.message_sender import message_manager
|
||||
from src.chat.message_receive.normal_message_sender import message_manager
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import traceback
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
|
||||
from src.common.logger import get_logger
|
||||
@@ -8,18 +9,70 @@ from src.chat.message_receive.message import MessageRecv
|
||||
from src.experimental.only_message_process import MessageProcessor
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.experimental.PFC.pfc_manager import PFCManager
|
||||
from src.chat.focus_chat.heartflow_message_processor import HeartFCMessageReceiver
|
||||
from src.chat.heart_flow.heartflow_message_processor import HeartFCMessageReceiver
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.config.config import global_config
|
||||
from src.plugin_system.core.component_registry import component_registry # 导入新插件系统
|
||||
from src.plugin_system.base.base_command import BaseCommand
|
||||
from src.mais4u.mais4u_chat.s4u_msg_processor import S4UMessageProcessor
|
||||
from maim_message import UserInfo
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
import re
|
||||
# 定义日志配置
|
||||
|
||||
# 获取项目根目录(假设本文件在src/chat/message_receive/下,根目录为上上上级目录)
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
|
||||
ENABLE_S4U_CHAT = os.path.isfile(os.path.join(PROJECT_ROOT, "s4u.s4u"))
|
||||
|
||||
if ENABLE_S4U_CHAT:
|
||||
print("""\nS4U私聊模式已开启\n!!!!!!!!!!!!!!!!!\n""")
|
||||
# 仅内部开启
|
||||
|
||||
# 配置主程序日志格式
|
||||
logger = get_logger("chat")
|
||||
|
||||
|
||||
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
||||
"""检查消息是否包含过滤词
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否包含过滤词
|
||||
"""
|
||||
for word in global_config.message_receive.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: ChatStream, userinfo: UserInfo) -> bool:
|
||||
"""检查消息是否匹配过滤正则表达式
|
||||
|
||||
Args:
|
||||
text: 待检查的文本
|
||||
chat: 聊天对象
|
||||
userinfo: 用户信息
|
||||
|
||||
Returns:
|
||||
bool: 是否匹配过滤正则
|
||||
"""
|
||||
for pattern in global_config.message_receive.ban_msgs_regex:
|
||||
if re.search(pattern, text):
|
||||
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
|
||||
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
||||
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ChatBot:
|
||||
def __init__(self):
|
||||
self.bot = None # bot 实例引用
|
||||
@@ -30,6 +83,7 @@ class ChatBot:
|
||||
# 创建初始化PFC管理器的任务,会在_ensure_started时执行
|
||||
self.only_process_chat = MessageProcessor()
|
||||
self.pfc_manager = PFCManager.get_instance()
|
||||
self.s4u_message_processor = S4UMessageProcessor()
|
||||
|
||||
async def _ensure_started(self):
|
||||
"""确保所有任务已启动"""
|
||||
@@ -38,17 +92,6 @@ class ChatBot:
|
||||
|
||||
self._started = True
|
||||
|
||||
async def _create_pfc_chat(self, message: MessageRecv):
|
||||
try:
|
||||
if global_config.experimental.pfc_chatting:
|
||||
chat_id = str(message.chat_stream.stream_id)
|
||||
private_name = str(message.message_info.user_info.user_nickname)
|
||||
|
||||
await self.pfc_manager.get_or_create_conversation(chat_id, private_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建PFC聊天失败: {e}")
|
||||
|
||||
async def _process_commands_with_new_system(self, message: MessageRecv):
|
||||
# sourcery skip: use-named-expression
|
||||
"""使用新插件系统处理命令"""
|
||||
@@ -139,17 +182,23 @@ class ChatBot:
|
||||
|
||||
get_chat_manager().register_message(message)
|
||||
|
||||
# 创建聊天流
|
||||
chat = await get_chat_manager().get_or_create_stream(
|
||||
platform=message.message_info.platform,
|
||||
user_info=user_info,
|
||||
group_info=group_info,
|
||||
)
|
||||
|
||||
message.update_chat_stream(chat)
|
||||
|
||||
# 处理消息内容,生成纯文本
|
||||
await message.process()
|
||||
|
||||
# 过滤检查
|
||||
if _check_ban_words(message.processed_plain_text, chat, user_info) or _check_ban_regex(
|
||||
message.raw_message, chat, user_info
|
||||
):
|
||||
return
|
||||
|
||||
# 命令处理 - 使用新插件系统检查并处理命令
|
||||
is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message)
|
||||
|
||||
@@ -172,23 +221,11 @@ class ChatBot:
|
||||
template_group_name = None
|
||||
|
||||
async def preprocess():
|
||||
logger.debug("开始预处理消息...")
|
||||
# 如果在私聊中
|
||||
if group_info is None:
|
||||
logger.debug("检测到私聊消息")
|
||||
if global_config.experimental.pfc_chatting:
|
||||
logger.debug("进入PFC私聊处理流程")
|
||||
# 创建聊天流
|
||||
logger.debug(f"为{user_info.user_id}创建/获取聊天流")
|
||||
await self.only_process_chat.process_message(message)
|
||||
await self._create_pfc_chat(message)
|
||||
# 禁止PFC,进入普通的心流消息处理逻辑
|
||||
else:
|
||||
logger.debug("进入普通心流私聊处理")
|
||||
await self.heartflow_message_receiver.process_message(message)
|
||||
# 群聊默认进入心流消息处理逻辑
|
||||
else:
|
||||
logger.debug(f"检测到群聊消息,群ID: {group_info.group_id}")
|
||||
if ENABLE_S4U_CHAT:
|
||||
logger.info("进入S4U流程")
|
||||
await self.s4u_message_processor.process_message(message)
|
||||
return
|
||||
|
||||
await self.heartflow_message_receiver.process_message(message)
|
||||
|
||||
if template_group_name:
|
||||
|
||||
@@ -108,7 +108,7 @@ class MessageRecv(Message):
|
||||
self.detailed_plain_text = message_dict.get("detailed_plain_text", "")
|
||||
self.is_emoji = False
|
||||
self.is_picid = False
|
||||
self.is_mentioned = 0.0
|
||||
self.is_mentioned = None
|
||||
self.priority_mode = "interest"
|
||||
self.priority_info = None
|
||||
|
||||
@@ -152,14 +152,10 @@ class MessageRecv(Message):
|
||||
elif segment.type == "mention_bot":
|
||||
self.is_mentioned = float(segment.data)
|
||||
return ""
|
||||
elif segment.type == "set_priority_mode":
|
||||
# 处理设置优先级模式的消息段
|
||||
if isinstance(segment.data, str):
|
||||
self.priority_mode = segment.data
|
||||
return ""
|
||||
elif segment.type == "priority_info":
|
||||
if isinstance(segment.data, dict):
|
||||
# 处理优先级信息
|
||||
self.priority_mode = "priority"
|
||||
self.priority_info = segment.data
|
||||
"""
|
||||
{
|
||||
@@ -305,6 +301,7 @@ class MessageSending(MessageProcessBase):
|
||||
is_emoji: bool = False,
|
||||
thinking_start_time: float = 0,
|
||||
apply_set_reply_logic: bool = False,
|
||||
reply_to: str = None,
|
||||
):
|
||||
# 调用父类初始化
|
||||
super().__init__(
|
||||
@@ -323,6 +320,8 @@ class MessageSending(MessageProcessBase):
|
||||
self.is_emoji = is_emoji
|
||||
self.apply_set_reply_logic = apply_set_reply_logic
|
||||
|
||||
self.reply_to = reply_to
|
||||
|
||||
# 用于显示发送内容与显示不一致的情况
|
||||
self.display_message = display_message
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from src.common.message.api import get_global_api
|
||||
from .message import MessageSending, MessageThinking, MessageSet
|
||||
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from ...config.config import global_config
|
||||
from ..utils.utils import truncate_message, calculate_typing_time, count_messages_between
|
||||
|
||||
from src.common.logger import get_logger
|
||||
@@ -192,20 +191,6 @@ class MessageManager:
|
||||
container = await self.get_container(chat_stream.stream_id)
|
||||
container.add_message(message)
|
||||
|
||||
def check_if_sending_message_exist(self, chat_id, thinking_id):
|
||||
"""检查指定聊天流的容器中是否存在具有特定 thinking_id 的 MessageSending 消息 或 emoji 消息"""
|
||||
# 这个方法现在是非异步的,因为它只读取数据
|
||||
container = self.containers.get(chat_id) # 直接 get,因为读取不需要锁
|
||||
if container and container.has_messages():
|
||||
for message in container.get_all_messages():
|
||||
if isinstance(message, MessageSending):
|
||||
msg_id = getattr(message.message_info, "message_id", None)
|
||||
# 检查 message_id 是否匹配 thinking_id 或以 "me" 开头 (emoji)
|
||||
if msg_id == thinking_id or (msg_id and msg_id.startswith("me")):
|
||||
# logger.debug(f"检查到存在相同thinking_id或emoji的消息: {msg_id} for {thinking_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _handle_sending_message(self, container: MessageContainer, message: MessageSending):
|
||||
"""处理单个 MessageSending 消息 (包含 set_reply 逻辑)"""
|
||||
try:
|
||||
@@ -216,12 +201,7 @@ class MessageManager:
|
||||
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.is_head
|
||||
and (thinking_messages_count > 3 or thinking_messages_length > 200)
|
||||
@@ -277,14 +257,6 @@ class MessageManager:
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# 检查是否超时
|
||||
if thinking_time > global_config.normal_chat.thinking_timeout:
|
||||
logger.warning(
|
||||
f"[{chat_id}] 消息思考超时 ({thinking_time:.1f}秒),移除消息 {message_earliest.message_info.message_id}"
|
||||
)
|
||||
container.remove_message(message_earliest)
|
||||
print() # 超时后换行,避免覆盖下一条日志
|
||||
|
||||
elif isinstance(message_earliest, MessageSending):
|
||||
# --- 处理发送消息 ---
|
||||
await self._handle_sending_message(container, message_earliest)
|
||||
@@ -301,12 +273,6 @@ class MessageManager:
|
||||
logger.info(f"[{chat_id}] 处理超时发送消息: {msg.message_info.message_id}")
|
||||
await self._handle_sending_message(container, msg) # 复用处理逻辑
|
||||
|
||||
# 清理空容器 (可选)
|
||||
# async with self._container_lock:
|
||||
# if not container.has_messages() and chat_id in self.containers:
|
||||
# logger.debug(f"[{chat_id}] 容器已空,准备移除。")
|
||||
# del self.containers[chat_id]
|
||||
|
||||
async def _start_processor_loop(self):
|
||||
"""消息处理器主循环"""
|
||||
while self._running:
|
||||
@@ -4,7 +4,7 @@ from typing import Union
|
||||
# from ...common.database.database import db # db is now Peewee's SqliteDatabase instance
|
||||
from .message import MessageSending, MessageRecv
|
||||
from .chat_stream import ChatStream
|
||||
from ...common.database.database_model import Messages, RecalledMessages # Import Peewee models
|
||||
from ...common.database.database_model import Messages, RecalledMessages, Images # Import Peewee models
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("message_storage")
|
||||
@@ -25,6 +25,7 @@ class MessageStorage:
|
||||
# print(processed_plain_text)
|
||||
|
||||
if processed_plain_text:
|
||||
processed_plain_text = MessageStorage.replace_image_descriptions(processed_plain_text)
|
||||
filtered_processed_plain_text = re.sub(pattern, "", processed_plain_text, flags=re.DOTALL)
|
||||
else:
|
||||
filtered_processed_plain_text = ""
|
||||
@@ -35,9 +36,13 @@ class MessageStorage:
|
||||
filtered_display_message = re.sub(pattern, "", display_message, flags=re.DOTALL)
|
||||
else:
|
||||
filtered_display_message = ""
|
||||
|
||||
reply_to = message.reply_to
|
||||
else:
|
||||
filtered_display_message = ""
|
||||
|
||||
reply_to = ""
|
||||
|
||||
chat_info_dict = chat_stream.to_dict()
|
||||
user_info_dict = message.message_info.user_info.to_dict()
|
||||
|
||||
@@ -59,6 +64,7 @@ class MessageStorage:
|
||||
time=float(message.message_info.time),
|
||||
chat_id=chat_stream.stream_id,
|
||||
# Flattened chat_info
|
||||
reply_to=reply_to,
|
||||
chat_info_stream_id=chat_info_dict.get("stream_id"),
|
||||
chat_info_platform=chat_info_dict.get("platform"),
|
||||
chat_info_user_platform=user_info_from_chat.get("platform"),
|
||||
@@ -136,3 +142,29 @@ class MessageStorage:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新消息ID失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
def replace_image_descriptions(text: str) -> str:
|
||||
"""将[图片:描述]替换为[picid:image_id]"""
|
||||
# 先检查文本中是否有图片标记
|
||||
pattern = r"\[图片:([^\]]+)\]"
|
||||
matches = re.findall(pattern, text)
|
||||
|
||||
if not matches:
|
||||
logger.debug("文本中没有图片标记,直接返回原文本")
|
||||
return text
|
||||
|
||||
def replace_match(match):
|
||||
description = match.group(1).strip()
|
||||
try:
|
||||
image_record = (
|
||||
Images.select().where(Images.description == description).order_by(Images.timestamp.desc()).first()
|
||||
)
|
||||
if image_record:
|
||||
return f"[picid:{image_record.image_id}]"
|
||||
else:
|
||||
return match.group(0) # 保持原样
|
||||
except Exception:
|
||||
return match.group(0)
|
||||
|
||||
return re.sub(r"\[图片:([^\]]+)\]", replace_match, text)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,294 +0,0 @@
|
||||
from typing import List, Any, Dict
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
from src.config.config import global_config
|
||||
import random
|
||||
import time
|
||||
|
||||
logger = get_logger("normal_chat_action_modifier")
|
||||
|
||||
|
||||
class NormalChatActionModifier:
|
||||
"""Normal Chat动作修改器
|
||||
|
||||
负责根据Normal Chat的上下文和状态动态调整可用的动作集合
|
||||
实现与Focus Chat类似的动作激活策略,但将LLM_JUDGE转换为概率激活以提升性能
|
||||
"""
|
||||
|
||||
def __init__(self, action_manager: ActionManager, stream_id: str, stream_name: str):
|
||||
"""初始化动作修改器"""
|
||||
self.action_manager = action_manager
|
||||
self.stream_id = stream_id
|
||||
self.stream_name = stream_name
|
||||
self.log_prefix = f"[{stream_name}]动作修改器"
|
||||
|
||||
# 缓存所有注册的动作
|
||||
self.all_actions = self.action_manager.get_registered_actions()
|
||||
|
||||
async def modify_actions_for_normal_chat(
|
||||
self,
|
||||
chat_stream,
|
||||
recent_replies: List[dict],
|
||||
message_content: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""为Normal Chat修改可用动作集合
|
||||
|
||||
实现动作激活策略:
|
||||
1. 基于关联类型的动态过滤
|
||||
2. 基于激活类型的智能判定(LLM_JUDGE转为概率激活)
|
||||
|
||||
Args:
|
||||
chat_stream: 聊天流对象
|
||||
recent_replies: 最近的回复记录
|
||||
message_content: 当前消息内容
|
||||
**kwargs: 其他参数
|
||||
"""
|
||||
|
||||
reasons = []
|
||||
merged_action_changes = {"add": [], "remove": []}
|
||||
type_mismatched_actions = [] # 在外层定义避免作用域问题
|
||||
|
||||
self.action_manager.restore_default_actions()
|
||||
|
||||
# 第一阶段:基于关联类型的动态过滤
|
||||
if chat_stream:
|
||||
chat_context = chat_stream.context if hasattr(chat_stream, "context") else None
|
||||
if chat_context:
|
||||
# 获取Normal模式下的可用动作(已经过滤了mode_enable)
|
||||
current_using_actions = self.action_manager.get_using_actions_for_mode("normal")
|
||||
# print(f"current_using_actions: {current_using_actions}")
|
||||
for action_name in current_using_actions.keys():
|
||||
if action_name in self.all_actions:
|
||||
data = self.all_actions[action_name]
|
||||
if data.get("associated_types"):
|
||||
if not chat_context.check_types(data["associated_types"]):
|
||||
type_mismatched_actions.append(action_name)
|
||||
logger.debug(f"{self.log_prefix} 动作 {action_name} 关联类型不匹配,移除该动作")
|
||||
|
||||
if type_mismatched_actions:
|
||||
merged_action_changes["remove"].extend(type_mismatched_actions)
|
||||
reasons.append(f"移除{type_mismatched_actions}(关联类型不匹配)")
|
||||
|
||||
# 第二阶段:应用激活类型判定
|
||||
# 构建聊天内容 - 使用与planner一致的方式
|
||||
chat_content = ""
|
||||
if chat_stream and hasattr(chat_stream, "stream_id"):
|
||||
try:
|
||||
# 获取消息历史,使用与normal_chat_planner相同的方法
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.chat.max_context_size, # 使用相同的配置
|
||||
)
|
||||
|
||||
# 构建可读的聊天上下文
|
||||
chat_content = build_readable_messages(
|
||||
message_list_before_now,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
logger.debug(f"{self.log_prefix} 成功构建聊天内容,长度: {len(chat_content)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.log_prefix} 构建聊天内容失败: {e}")
|
||||
chat_content = ""
|
||||
|
||||
# 获取当前Normal模式下的动作集进行激活判定
|
||||
current_actions = self.action_manager.get_using_actions_for_mode("normal")
|
||||
|
||||
# print(f"current_actions: {current_actions}")
|
||||
# print(f"chat_content: {chat_content}")
|
||||
final_activated_actions = await self._apply_normal_activation_filtering(
|
||||
current_actions, chat_content, message_content, recent_replies
|
||||
)
|
||||
# print(f"final_activated_actions: {final_activated_actions}")
|
||||
|
||||
# 统一处理所有需要移除的动作,避免重复移除
|
||||
all_actions_to_remove = set() # 使用set避免重复
|
||||
|
||||
# 添加关联类型不匹配的动作
|
||||
if type_mismatched_actions:
|
||||
all_actions_to_remove.update(type_mismatched_actions)
|
||||
|
||||
# 添加激活类型判定未通过的动作
|
||||
for action_name in current_actions.keys():
|
||||
if action_name not in final_activated_actions:
|
||||
all_actions_to_remove.add(action_name)
|
||||
|
||||
# 统计移除原因(避免重复)
|
||||
activation_failed_actions = [
|
||||
name
|
||||
for name in current_actions.keys()
|
||||
if name not in final_activated_actions and name not in type_mismatched_actions
|
||||
]
|
||||
if activation_failed_actions:
|
||||
reasons.append(f"移除{activation_failed_actions}(激活类型判定未通过)")
|
||||
|
||||
# 统一执行移除操作
|
||||
for action_name in all_actions_to_remove:
|
||||
success = self.action_manager.remove_action_from_using(action_name)
|
||||
if success:
|
||||
logger.debug(f"{self.log_prefix} 移除动作: {action_name}")
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix} 动作 {action_name} 已经不在使用集中,跳过移除")
|
||||
|
||||
# 应用动作添加(如果有的话)
|
||||
for action_name in merged_action_changes["add"]:
|
||||
if action_name in self.all_actions:
|
||||
success = self.action_manager.add_action_to_using(action_name)
|
||||
if success:
|
||||
logger.debug(f"{self.log_prefix} 添加动作: {action_name}")
|
||||
|
||||
# 记录变更原因
|
||||
if reasons:
|
||||
logger.info(f"{self.log_prefix} 动作调整完成: {' | '.join(reasons)}")
|
||||
|
||||
# 获取最终的Normal模式可用动作并记录
|
||||
final_actions = self.action_manager.get_using_actions_for_mode("normal")
|
||||
logger.debug(f"{self.log_prefix} 当前Normal模式可用动作: {list(final_actions.keys())}")
|
||||
|
||||
async def _apply_normal_activation_filtering(
|
||||
self,
|
||||
actions_with_info: Dict[str, Any],
|
||||
chat_content: str = "",
|
||||
message_content: str = "",
|
||||
recent_replies: List[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
应用Normal模式的激活类型过滤逻辑
|
||||
|
||||
与Focus模式的区别:
|
||||
1. LLM_JUDGE类型转换为概率激活(避免LLM调用)
|
||||
2. RANDOM类型保持概率激活
|
||||
3. KEYWORD类型保持关键词匹配
|
||||
4. ALWAYS类型直接激活
|
||||
|
||||
Args:
|
||||
actions_with_info: 带完整信息的动作字典
|
||||
chat_content: 聊天内容
|
||||
message_content: 当前消息内容
|
||||
recent_replies: 最近的回复记录列表
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 过滤后激活的actions字典
|
||||
"""
|
||||
activated_actions = {}
|
||||
|
||||
# 分类处理不同激活类型的actions
|
||||
always_actions = {}
|
||||
random_actions = {}
|
||||
keyword_actions = {}
|
||||
|
||||
for action_name, action_info in actions_with_info.items():
|
||||
# 使用normal_activation_type
|
||||
activation_type = action_info.get("normal_activation_type", "always")
|
||||
|
||||
# 现在统一是字符串格式的激活类型值
|
||||
if activation_type == "always":
|
||||
always_actions[action_name] = action_info
|
||||
elif activation_type == "random" or activation_type == "llm_judge":
|
||||
random_actions[action_name] = action_info
|
||||
elif activation_type == "keyword":
|
||||
keyword_actions[action_name] = action_info
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix}未知的激活类型: {activation_type},跳过处理")
|
||||
|
||||
# 1. 处理ALWAYS类型(直接激活)
|
||||
for action_name, action_info in always_actions.items():
|
||||
activated_actions[action_name] = action_info
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: ALWAYS类型直接激活")
|
||||
|
||||
# 2. 处理RANDOM类型(概率激活)
|
||||
for action_name, action_info in random_actions.items():
|
||||
probability = action_info.get("random_activation_probability", ActionManager.DEFAULT_RANDOM_PROBABILITY)
|
||||
should_activate = random.random() < probability
|
||||
if should_activate:
|
||||
activated_actions[action_name] = action_info
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: RANDOM类型触发(概率{probability})")
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: RANDOM类型未触发(概率{probability})")
|
||||
|
||||
# 3. 处理KEYWORD类型(关键词匹配)
|
||||
for action_name, action_info in keyword_actions.items():
|
||||
should_activate = self._check_keyword_activation(action_name, action_info, chat_content, message_content)
|
||||
if should_activate:
|
||||
activated_actions[action_name] = action_info
|
||||
keywords = action_info.get("activation_keywords", [])
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: KEYWORD类型匹配关键词({keywords})")
|
||||
else:
|
||||
keywords = action_info.get("activation_keywords", [])
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: KEYWORD类型未匹配关键词({keywords})")
|
||||
|
||||
logger.debug(f"{self.log_prefix}Normal模式激活类型过滤完成: {list(activated_actions.keys())}")
|
||||
return activated_actions
|
||||
|
||||
def _check_keyword_activation(
|
||||
self,
|
||||
action_name: str,
|
||||
action_info: Dict[str, Any],
|
||||
chat_content: str = "",
|
||||
message_content: str = "",
|
||||
) -> bool:
|
||||
"""
|
||||
检查是否匹配关键词触发条件
|
||||
|
||||
Args:
|
||||
action_name: 动作名称
|
||||
action_info: 动作信息
|
||||
chat_content: 聊天内容(已经是格式化后的可读消息)
|
||||
|
||||
Returns:
|
||||
bool: 是否应该激活此action
|
||||
"""
|
||||
|
||||
activation_keywords = action_info.get("activation_keywords", [])
|
||||
case_sensitive = action_info.get("keyword_case_sensitive", False)
|
||||
|
||||
if not activation_keywords:
|
||||
logger.warning(f"{self.log_prefix}动作 {action_name} 设置为关键词触发但未配置关键词")
|
||||
return False
|
||||
|
||||
# 使用构建好的聊天内容作为检索文本
|
||||
search_text = chat_content + message_content
|
||||
|
||||
# 如果不区分大小写,转换为小写
|
||||
if not case_sensitive:
|
||||
search_text = search_text.lower()
|
||||
|
||||
# 检查每个关键词
|
||||
matched_keywords = []
|
||||
for keyword in activation_keywords:
|
||||
check_keyword = keyword if case_sensitive else keyword.lower()
|
||||
if check_keyword in search_text:
|
||||
matched_keywords.append(keyword)
|
||||
|
||||
# print(f"search_text: {search_text}")
|
||||
# print(f"activation_keywords: {activation_keywords}")
|
||||
|
||||
if matched_keywords:
|
||||
logger.debug(f"{self.log_prefix}动作 {action_name} 匹配到关键词: {matched_keywords}")
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix}动作 {action_name} 未匹配到任何关键词: {activation_keywords}")
|
||||
return False
|
||||
|
||||
def get_available_actions_count(self) -> int:
|
||||
"""获取当前可用动作数量(排除默认的no_action)"""
|
||||
current_actions = self.action_manager.get_using_actions_for_mode("normal")
|
||||
# 排除no_action(如果存在)
|
||||
filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"}
|
||||
return len(filtered_actions)
|
||||
|
||||
def should_skip_planning(self) -> bool:
|
||||
"""判断是否应该跳过规划过程"""
|
||||
available_count = self.get_available_actions_count()
|
||||
if available_count == 0:
|
||||
logger.debug(f"{self.log_prefix} 没有可用动作,跳过规划")
|
||||
return True
|
||||
return False
|
||||
@@ -1,262 +0,0 @@
|
||||
"""
|
||||
Normal Chat Expressor
|
||||
|
||||
为Normal Chat专门设计的表达器,不需要经过LLM风格化处理,
|
||||
直接发送消息,主要用于插件动作中需要发送消息的场景。
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from src.chat.message_receive.message import MessageRecv, MessageSending, MessageThinking, Seg
|
||||
from src.chat.message_receive.message import UserInfo
|
||||
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
|
||||
from src.chat.message_receive.message_sender import message_manager
|
||||
from src.config.config import global_config
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("normal_chat_expressor")
|
||||
|
||||
|
||||
class NormalChatExpressor:
|
||||
"""Normal Chat专用表达器
|
||||
|
||||
特点:
|
||||
1. 不经过LLM风格化,直接发送消息
|
||||
2. 支持文本和表情包发送
|
||||
3. 为插件动作提供简化的消息发送接口
|
||||
4. 保持与focus_chat expressor相似的API,但去掉复杂的风格化流程
|
||||
"""
|
||||
|
||||
def __init__(self, chat_stream: ChatStream):
|
||||
"""初始化Normal Chat表达器
|
||||
|
||||
Args:
|
||||
chat_stream: 聊天流对象
|
||||
stream_name: 流名称
|
||||
"""
|
||||
self.chat_stream = chat_stream
|
||||
self.stream_name = get_chat_manager().get_stream_name(self.chat_stream.stream_id) or self.chat_stream.stream_id
|
||||
self.log_prefix = f"[{self.stream_name}]Normal表达器"
|
||||
|
||||
logger.debug(f"{self.log_prefix} 初始化完成")
|
||||
|
||||
async def create_thinking_message(
|
||||
self, anchor_message: Optional[MessageRecv], thinking_id: str
|
||||
) -> Optional[MessageThinking]:
|
||||
"""创建思考消息
|
||||
|
||||
Args:
|
||||
anchor_message: 锚点消息
|
||||
thinking_id: 思考ID
|
||||
|
||||
Returns:
|
||||
MessageThinking: 创建的思考消息,如果失败返回None
|
||||
"""
|
||||
if not anchor_message or not anchor_message.chat_stream:
|
||||
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流")
|
||||
return None
|
||||
|
||||
messageinfo = anchor_message.message_info
|
||||
thinking_time_point = time.time()
|
||||
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.bot.qq_account,
|
||||
user_nickname=global_config.bot.nickname,
|
||||
platform=messageinfo.platform,
|
||||
)
|
||||
|
||||
thinking_message = MessageThinking(
|
||||
message_id=thinking_id,
|
||||
chat_stream=self.chat_stream,
|
||||
bot_user_info=bot_user_info,
|
||||
reply=anchor_message,
|
||||
thinking_start_time=thinking_time_point,
|
||||
)
|
||||
|
||||
await message_manager.add_message(thinking_message)
|
||||
logger.debug(f"{self.log_prefix} 创建思考消息: {thinking_id}")
|
||||
return thinking_message
|
||||
|
||||
async def send_response_messages(
|
||||
self,
|
||||
anchor_message: Optional[MessageRecv],
|
||||
response_set: List[Tuple[str, str]],
|
||||
thinking_id: str = "",
|
||||
display_message: str = "",
|
||||
) -> Optional[MessageSending]:
|
||||
"""发送回复消息
|
||||
|
||||
Args:
|
||||
anchor_message: 锚点消息
|
||||
response_set: 回复内容集合,格式为 [(type, content), ...]
|
||||
thinking_id: 思考ID
|
||||
display_message: 显示消息
|
||||
|
||||
Returns:
|
||||
MessageSending: 发送的第一条消息,如果失败返回None
|
||||
"""
|
||||
try:
|
||||
if not response_set:
|
||||
logger.warning(f"{self.log_prefix} 回复内容为空")
|
||||
return None
|
||||
|
||||
# 如果没有thinking_id,生成一个
|
||||
if not thinking_id:
|
||||
thinking_time_point = round(time.time(), 2)
|
||||
thinking_id = "mt" + str(thinking_time_point)
|
||||
|
||||
# 创建思考消息
|
||||
if anchor_message:
|
||||
await self.create_thinking_message(anchor_message, thinking_id)
|
||||
|
||||
# 创建消息集
|
||||
|
||||
mark_head = False
|
||||
is_emoji = False
|
||||
if len(response_set) == 0:
|
||||
return None
|
||||
message_id = f"{thinking_id}_{len(response_set)}"
|
||||
response_type, content = response_set[0]
|
||||
if len(response_set) > 1:
|
||||
message_segment = Seg(type="seglist", data=[Seg(type=t, data=c) for t, c in response_set])
|
||||
else:
|
||||
message_segment = Seg(type=response_type, data=content)
|
||||
if response_type == "emoji":
|
||||
is_emoji = True
|
||||
|
||||
bot_msg = await self._build_sending_message(
|
||||
message_id=message_id,
|
||||
message_segment=message_segment,
|
||||
thinking_id=thinking_id,
|
||||
anchor_message=anchor_message,
|
||||
thinking_start_time=time.time(),
|
||||
reply_to=mark_head,
|
||||
is_emoji=is_emoji,
|
||||
display_message=display_message,
|
||||
)
|
||||
logger.debug(f"{self.log_prefix} 添加{response_type}类型消息: {content}")
|
||||
|
||||
# 提交消息集
|
||||
if bot_msg:
|
||||
await message_manager.add_message(bot_msg)
|
||||
logger.info(
|
||||
f"{self.log_prefix} 成功发送 {response_type}类型消息: {str(content)[:200] + '...' if len(str(content)) > 200 else content}"
|
||||
)
|
||||
container = await message_manager.get_container(self.chat_stream.stream_id) # 使用 self.stream_id
|
||||
for msg in container.messages[:]:
|
||||
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
|
||||
container.messages.remove(msg)
|
||||
logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}")
|
||||
break
|
||||
return bot_msg
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} 没有有效的消息被创建")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 发送消息失败: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
async def _build_sending_message(
|
||||
self,
|
||||
message_id: str,
|
||||
message_segment: Seg,
|
||||
thinking_id: str,
|
||||
anchor_message: Optional[MessageRecv],
|
||||
thinking_start_time: float,
|
||||
reply_to: bool = False,
|
||||
is_emoji: bool = False,
|
||||
display_message: str = "",
|
||||
) -> MessageSending:
|
||||
"""构建发送消息
|
||||
|
||||
Args:
|
||||
message_id: 消息ID
|
||||
message_segment: 消息段
|
||||
thinking_id: 思考ID
|
||||
anchor_message: 锚点消息
|
||||
thinking_start_time: 思考开始时间
|
||||
reply_to: 是否回复
|
||||
is_emoji: 是否为表情包
|
||||
|
||||
Returns:
|
||||
MessageSending: 构建的发送消息
|
||||
"""
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.bot.qq_account,
|
||||
user_nickname=global_config.bot.nickname,
|
||||
platform=anchor_message.message_info.platform if anchor_message else "unknown",
|
||||
)
|
||||
|
||||
message_sending = MessageSending(
|
||||
message_id=message_id,
|
||||
chat_stream=self.chat_stream,
|
||||
bot_user_info=bot_user_info,
|
||||
message_segment=message_segment,
|
||||
sender_info=self.chat_stream.user_info,
|
||||
reply=anchor_message if reply_to else None,
|
||||
thinking_start_time=thinking_start_time,
|
||||
is_emoji=is_emoji,
|
||||
display_message=display_message,
|
||||
)
|
||||
|
||||
return message_sending
|
||||
|
||||
async def deal_reply(
|
||||
self,
|
||||
cycle_timers: dict,
|
||||
action_data: Dict[str, Any],
|
||||
reasoning: str,
|
||||
anchor_message: MessageRecv,
|
||||
thinking_id: str,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""处理回复动作 - 兼容focus_chat expressor API
|
||||
|
||||
Args:
|
||||
cycle_timers: 周期计时器(normal_chat中不使用)
|
||||
action_data: 动作数据,包含text、target、emojis等
|
||||
reasoning: 推理说明
|
||||
anchor_message: 锚点消息
|
||||
thinking_id: 思考ID
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (是否成功, 回复文本)
|
||||
"""
|
||||
try:
|
||||
response_set = []
|
||||
|
||||
# 处理文本内容
|
||||
text_content = action_data.get("text", "")
|
||||
if text_content:
|
||||
response_set.append(("text", text_content))
|
||||
|
||||
# 处理表情包
|
||||
emoji_content = action_data.get("emojis", "")
|
||||
if emoji_content:
|
||||
response_set.append(("emoji", emoji_content))
|
||||
|
||||
if not response_set:
|
||||
logger.warning(f"{self.log_prefix} deal_reply: 没有有效的回复内容")
|
||||
return False, None
|
||||
|
||||
# 发送消息
|
||||
result = await self.send_response_messages(
|
||||
anchor_message=anchor_message,
|
||||
response_set=response_set,
|
||||
thinking_id=thinking_id,
|
||||
)
|
||||
|
||||
if result:
|
||||
return True, text_content if text_content else "发送成功"
|
||||
else:
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} deal_reply执行失败: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False, None
|
||||
@@ -1,123 +0,0 @@
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.chat.message_receive.message import MessageThinking
|
||||
from src.common.logger import get_logger
|
||||
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
|
||||
from src.chat.utils.utils import process_llm_response
|
||||
from src.plugin_system.apis import generator_api
|
||||
from src.chat.focus_chat.memory_activator import MemoryActivator
|
||||
|
||||
|
||||
logger = get_logger("normal_chat_response")
|
||||
|
||||
|
||||
class NormalChatGenerator:
|
||||
def __init__(self):
|
||||
model_config_1 = global_config.model.replyer_1.copy()
|
||||
model_config_2 = global_config.model.replyer_2.copy()
|
||||
|
||||
prob_first = global_config.chat.replyer_random_probability
|
||||
|
||||
model_config_1["weight"] = prob_first
|
||||
model_config_2["weight"] = 1.0 - prob_first
|
||||
|
||||
self.model_configs = [model_config_1, model_config_2]
|
||||
|
||||
self.model_sum = LLMRequest(model=global_config.model.memory_summary, temperature=0.7, request_type="relation")
|
||||
self.memory_activator = MemoryActivator()
|
||||
|
||||
async def generate_response(
|
||||
self,
|
||||
message: MessageThinking,
|
||||
available_actions=None,
|
||||
):
|
||||
logger.info(
|
||||
f"NormalChat思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
|
||||
)
|
||||
person_id = PersonInfoManager.get_person_id(
|
||||
message.chat_stream.user_info.platform, message.chat_stream.user_info.user_id
|
||||
)
|
||||
person_info_manager = get_person_info_manager()
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
relation_info = await person_info_manager.get_value(person_id, "short_impression")
|
||||
reply_to_str = f"{person_name}:{message.processed_plain_text}"
|
||||
|
||||
try:
|
||||
success, reply_set, prompt = await generator_api.generate_reply(
|
||||
chat_stream=message.chat_stream,
|
||||
reply_to=reply_to_str,
|
||||
relation_info=relation_info,
|
||||
available_actions=available_actions,
|
||||
enable_tool=global_config.tool.enable_in_normal_chat,
|
||||
model_configs=self.model_configs,
|
||||
request_type="normal.replyer",
|
||||
return_prompt=True,
|
||||
)
|
||||
|
||||
if not success or not reply_set:
|
||||
logger.info(f"对 {message.processed_plain_text} 的回复生成失败")
|
||||
return None
|
||||
|
||||
content = " ".join([item[1] for item in reply_set if item[0] == "text"])
|
||||
logger.debug(f"对 {message.processed_plain_text} 的回复:{content}")
|
||||
|
||||
if content:
|
||||
logger.info(f"{global_config.bot.nickname}的备选回复是:{content}")
|
||||
content = process_llm_response(content)
|
||||
|
||||
return content
|
||||
|
||||
except Exception:
|
||||
logger.exception("生成回复时出错")
|
||||
return None
|
||||
|
||||
return content
|
||||
|
||||
async def _get_emotion_tags(self, content: str, processed_plain_text: str):
|
||||
"""提取情感标签,结合立场和情绪"""
|
||||
try:
|
||||
# 构建提示词,结合回复内容、被回复的内容以及立场分析
|
||||
prompt = f"""
|
||||
请严格根据以下对话内容,完成以下任务:
|
||||
1. 判断回复者对被回复者观点的直接立场:
|
||||
- "支持":明确同意或强化被回复者观点
|
||||
- "反对":明确反驳或否定被回复者观点
|
||||
- "中立":不表达明确立场或无关回应
|
||||
2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签
|
||||
3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒"
|
||||
4. 考虑回复者的人格设定为{global_config.personality.personality_core}
|
||||
|
||||
对话示例:
|
||||
被回复:「A就是笨」
|
||||
回复:「A明明很聪明」 → 反对-愤怒
|
||||
|
||||
当前对话:
|
||||
被回复:「{processed_plain_text}」
|
||||
回复:「{content}」
|
||||
|
||||
输出要求:
|
||||
- 只需输出"立场-情绪"结果,不要解释
|
||||
- 严格基于文字直接表达的对立关系判断
|
||||
"""
|
||||
|
||||
# 调用模型生成结果
|
||||
result, (reasoning_content, model_name) = await self.model_sum.generate_response_async(prompt)
|
||||
result = result.strip()
|
||||
|
||||
# 解析模型输出的结果
|
||||
if "-" in result:
|
||||
stance, emotion = result.split("-", 1)
|
||||
valid_stances = ["支持", "反对", "中立"]
|
||||
valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"]
|
||||
if stance in valid_stances and emotion in valid_emotions:
|
||||
return stance, emotion # 返回有效的立场-情绪组合
|
||||
else:
|
||||
logger.debug(f"无效立场-情感组合:{result}")
|
||||
return "中立", "平静" # 默认返回中立-平静
|
||||
else:
|
||||
logger.debug(f"立场-情感格式错误:{result}")
|
||||
return "中立", "平静" # 格式错误时返回默认值
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"获取情感标签时出错: {e}")
|
||||
return "中立", "平静" # 出错时返回默认值
|
||||
@@ -1,308 +0,0 @@
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from rich.traceback import install
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.individuality.individuality import get_individuality
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.chat.message_receive.message import MessageThinking
|
||||
from json_repair import repair_json
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
import time
|
||||
import traceback
|
||||
|
||||
logger = get_logger("normal_chat_planner")
|
||||
|
||||
install(extra_lines=3)
|
||||
|
||||
|
||||
def init_prompt():
|
||||
Prompt(
|
||||
"""
|
||||
你的自我认知是:
|
||||
{self_info_block}
|
||||
请记住你的性格,身份和特点。
|
||||
|
||||
你是群内的一员,你现在正在参与群内的闲聊,以下是群内的聊天内容:
|
||||
{chat_context}
|
||||
|
||||
基于以上聊天上下文和用户的最新消息,选择最合适的action。
|
||||
|
||||
注意,除了下面动作选项之外,你在聊天中不能做其他任何事情,这是你能力的边界,现在请你选择合适的action:
|
||||
|
||||
{action_options_text}
|
||||
|
||||
重要说明:
|
||||
- "no_action" 表示只进行普通聊天回复,不执行任何额外动作
|
||||
- 其他action表示在普通回复的基础上,执行相应的额外动作
|
||||
|
||||
你必须从上面列出的可用action中选择一个,并说明原因。
|
||||
{moderation_prompt}
|
||||
|
||||
请以动作的输出要求,以严格的 JSON 格式输出,且仅包含 JSON 内容。不要有任何其他文字或解释:
|
||||
""",
|
||||
"normal_chat_planner_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
动作:{action_name}
|
||||
该动作的描述:{action_description}
|
||||
使用该动作的场景:
|
||||
{action_require}
|
||||
输出要求:
|
||||
{{
|
||||
"action": "{action_name}",{action_parameters}
|
||||
}}
|
||||
""",
|
||||
"normal_chat_action_prompt",
|
||||
)
|
||||
|
||||
|
||||
class NormalChatPlanner:
|
||||
def __init__(self, log_prefix: str, action_manager: ActionManager):
|
||||
self.log_prefix = log_prefix
|
||||
# LLM规划器配置
|
||||
self.planner_llm = LLMRequest(
|
||||
model=global_config.model.planner,
|
||||
request_type="normal.planner", # 用于normal_chat动作规划
|
||||
)
|
||||
|
||||
self.action_manager = action_manager
|
||||
|
||||
async def plan(self, message: MessageThinking, sender_name: str = "某人") -> Dict[str, Any]:
|
||||
"""
|
||||
Normal Chat 规划器: 使用LLM根据上下文决定做出什么动作。
|
||||
|
||||
参数:
|
||||
message: 思考消息对象
|
||||
sender_name: 发送者名称
|
||||
"""
|
||||
|
||||
action = "no_action" # 默认动作改为no_action
|
||||
reasoning = "规划器初始化默认"
|
||||
action_data = {}
|
||||
|
||||
try:
|
||||
# 设置默认值
|
||||
nickname_str = ""
|
||||
for nicknames in global_config.bot.alias_names:
|
||||
nickname_str += f"{nicknames},"
|
||||
name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。"
|
||||
|
||||
personality_block = get_individuality().get_personality_prompt(x_person=2, level=2)
|
||||
identity_block = get_individuality().get_identity_prompt(x_person=2, level=2)
|
||||
|
||||
self_info = name_block + personality_block + identity_block
|
||||
|
||||
# 获取当前可用的动作,使用Normal模式过滤
|
||||
current_available_actions = self.action_manager.get_using_actions_for_mode("normal")
|
||||
|
||||
# 注意:动作的激活判定现在在 normal_chat_action_modifier 中完成
|
||||
# 这里直接使用经过 action_modifier 处理后的最终动作集
|
||||
# 符合职责分离原则:ActionModifier负责动作管理,Planner专注于决策
|
||||
|
||||
# 如果没有可用动作,直接返回no_action
|
||||
if not current_available_actions:
|
||||
logger.debug(f"{self.log_prefix}规划器: 没有可用动作,返回no_action")
|
||||
return {
|
||||
"action_result": {
|
||||
"action_type": action,
|
||||
"action_data": action_data,
|
||||
"reasoning": reasoning,
|
||||
"is_parallel": True,
|
||||
},
|
||||
"chat_context": "",
|
||||
"action_prompt": "",
|
||||
}
|
||||
|
||||
# 构建normal_chat的上下文 (使用与normal_chat相同的prompt构建方法)
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=message.chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.chat.max_context_size,
|
||||
)
|
||||
|
||||
chat_context = build_readable_messages(
|
||||
message_list_before_now,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
# 构建planner的prompt
|
||||
prompt = await self.build_planner_prompt(
|
||||
self_info_block=self_info,
|
||||
chat_context=chat_context,
|
||||
current_available_actions=current_available_actions,
|
||||
)
|
||||
|
||||
if not prompt:
|
||||
logger.warning(f"{self.log_prefix}规划器: 构建提示词失败")
|
||||
return {
|
||||
"action_result": {
|
||||
"action_type": action,
|
||||
"action_data": action_data,
|
||||
"reasoning": reasoning,
|
||||
"is_parallel": False,
|
||||
},
|
||||
"chat_context": chat_context,
|
||||
"action_prompt": "",
|
||||
}
|
||||
|
||||
# 使用LLM生成动作决策
|
||||
try:
|
||||
content, (reasoning_content, model_name) = await self.planner_llm.generate_response_async(prompt)
|
||||
|
||||
logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}")
|
||||
logger.info(f"{self.log_prefix}规划器原始响应: {content}")
|
||||
logger.info(f"{self.log_prefix}规划器推理: {reasoning_content}")
|
||||
logger.info(f"{self.log_prefix}规划器模型: {model_name}")
|
||||
|
||||
# 解析JSON响应
|
||||
try:
|
||||
# 尝试修复JSON
|
||||
fixed_json = repair_json(content)
|
||||
action_result = json.loads(fixed_json)
|
||||
|
||||
action = action_result.get("action", "no_action")
|
||||
reasoning = action_result.get("reasoning", "未提供原因")
|
||||
|
||||
# 提取其他参数作为action_data
|
||||
action_data = {k: v for k, v in action_result.items() if k not in ["action", "reasoning"]}
|
||||
|
||||
# 验证动作是否在可用动作列表中,或者是特殊动作
|
||||
if action not in current_available_actions:
|
||||
logger.warning(f"{self.log_prefix}规划器选择了不可用的动作: {action}, 回退到no_action")
|
||||
action = "no_action"
|
||||
reasoning = f"选择的动作{action}不在可用列表中,回退到no_action"
|
||||
action_data = {}
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"{self.log_prefix}规划器JSON解析失败: {e}, 内容: {content}")
|
||||
action = "no_action"
|
||||
reasoning = "JSON解析失败,使用默认动作"
|
||||
action_data = {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix}规划器LLM调用失败: {e}")
|
||||
action = "no_action"
|
||||
reasoning = "LLM调用失败,使用默认动作"
|
||||
action_data = {}
|
||||
|
||||
except Exception as outer_e:
|
||||
logger.error(f"{self.log_prefix}规划器异常: {outer_e}")
|
||||
# 设置异常时的默认值
|
||||
current_available_actions = {}
|
||||
chat_context = "无法获取聊天上下文"
|
||||
prompt = ""
|
||||
action = "no_action"
|
||||
reasoning = "规划器出现异常,使用默认动作"
|
||||
action_data = {}
|
||||
|
||||
# 检查动作是否支持并行执行
|
||||
is_parallel = False
|
||||
if action in current_available_actions:
|
||||
action_info = current_available_actions[action]
|
||||
is_parallel = action_info.get("parallel_action", False)
|
||||
|
||||
logger.debug(
|
||||
f"{self.log_prefix}规划器决策动作:{action}, 动作信息: '{action_data}', 理由: {reasoning}, 并行执行: {is_parallel}"
|
||||
)
|
||||
|
||||
# 恢复到默认动作集
|
||||
self.action_manager.restore_actions()
|
||||
logger.debug(
|
||||
f"{self.log_prefix}规划后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}"
|
||||
)
|
||||
|
||||
# 构建 action 记录
|
||||
action_record = {
|
||||
"action_type": action,
|
||||
"action_data": action_data,
|
||||
"reasoning": reasoning,
|
||||
"timestamp": time.time(),
|
||||
"model_name": model_name if "model_name" in locals() else None,
|
||||
}
|
||||
|
||||
action_result = {
|
||||
"action_type": action,
|
||||
"action_data": action_data,
|
||||
"reasoning": reasoning,
|
||||
"is_parallel": is_parallel,
|
||||
"action_record": json.dumps(action_record, ensure_ascii=False),
|
||||
}
|
||||
|
||||
plan_result = {
|
||||
"action_result": action_result,
|
||||
"chat_context": chat_context,
|
||||
"action_prompt": prompt,
|
||||
}
|
||||
|
||||
return plan_result
|
||||
|
||||
async def build_planner_prompt(
|
||||
self,
|
||||
self_info_block: str,
|
||||
chat_context: str,
|
||||
current_available_actions: Dict[str, Any],
|
||||
) -> str:
|
||||
"""构建 Normal Chat Planner LLM 的提示词"""
|
||||
try:
|
||||
# 构建动作选项文本
|
||||
action_options_text = ""
|
||||
|
||||
for action_name, action_info in current_available_actions.items():
|
||||
action_description = action_info.get("description", "")
|
||||
action_parameters = action_info.get("parameters", {})
|
||||
action_require = action_info.get("require", [])
|
||||
|
||||
if action_parameters:
|
||||
param_text = "\n"
|
||||
# print(action_parameters)
|
||||
for param_name, param_description in action_parameters.items():
|
||||
param_text += f' "{param_name}":"{param_description}"\n'
|
||||
param_text = param_text.rstrip("\n")
|
||||
else:
|
||||
param_text = ""
|
||||
|
||||
require_text = ""
|
||||
for require_item in action_require:
|
||||
require_text += f"- {require_item}\n"
|
||||
require_text = require_text.rstrip("\n")
|
||||
|
||||
# 构建单个动作的提示
|
||||
action_prompt = await global_prompt_manager.format_prompt(
|
||||
"normal_chat_action_prompt",
|
||||
action_name=action_name,
|
||||
action_description=action_description,
|
||||
action_parameters=param_text,
|
||||
action_require=require_text,
|
||||
)
|
||||
action_options_text += action_prompt + "\n\n"
|
||||
|
||||
# 审核提示
|
||||
moderation_prompt = "请确保你的回复符合平台规则,避免不当内容。"
|
||||
|
||||
# 使用模板构建最终提示词
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
"normal_chat_planner_prompt",
|
||||
self_info_block=self_info_block,
|
||||
action_options_text=action_options_text,
|
||||
moderation_prompt=moderation_prompt,
|
||||
chat_context=chat_context,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix}构建Planner提示词失败: {e}")
|
||||
traceback.print_exc()
|
||||
return ""
|
||||
|
||||
|
||||
init_prompt()
|
||||
@@ -1,30 +0,0 @@
|
||||
import time
|
||||
from src.config.config import global_config
|
||||
from src.common.message_repository import count_messages
|
||||
|
||||
|
||||
def get_recent_message_stats(minutes: int = 30, chat_id: str = None) -> dict:
|
||||
"""
|
||||
Args:
|
||||
minutes (int): 检索的分钟数,默认30分钟
|
||||
chat_id (str, optional): 指定的chat_id,仅统计该chat下的消息。为None时统计全部。
|
||||
Returns:
|
||||
dict: {"bot_reply_count": int, "total_message_count": int}
|
||||
"""
|
||||
|
||||
now = time.time()
|
||||
start_time = now - minutes * 60
|
||||
bot_id = global_config.bot.qq_account
|
||||
|
||||
filter_base = {"time": {"$gte": start_time}}
|
||||
if chat_id is not None:
|
||||
filter_base["chat_id"] = chat_id
|
||||
|
||||
# 总消息数
|
||||
total_message_count = count_messages(filter_base)
|
||||
# bot自身回复数
|
||||
bot_filter = filter_base.copy()
|
||||
bot_filter["user_id"] = bot_id
|
||||
bot_reply_count = count_messages(bot_filter)
|
||||
|
||||
return {"bot_reply_count": bot_reply_count, "total_message_count": total_message_count}
|
||||
@@ -33,28 +33,10 @@ class ClassicalWillingManager(BaseWillingManager):
|
||||
if willing_info.is_mentioned_bot:
|
||||
current_willing += 1 if current_willing < 1.0 else 0.05
|
||||
|
||||
is_emoji_not_reply = False
|
||||
if willing_info.is_emoji:
|
||||
if global_config.normal_chat.emoji_response_penalty != 0:
|
||||
current_willing *= global_config.normal_chat.emoji_response_penalty
|
||||
else:
|
||||
is_emoji_not_reply = True
|
||||
|
||||
# 处理picid格式消息,直接不回复
|
||||
is_picid_not_reply = False
|
||||
if willing_info.is_picid:
|
||||
is_picid_not_reply = True
|
||||
|
||||
self.chat_reply_willing[chat_id] = min(current_willing, 3.0)
|
||||
|
||||
reply_probability = min(max((current_willing - 0.5), 0.01) * 2, 1)
|
||||
|
||||
if is_emoji_not_reply:
|
||||
reply_probability = 0
|
||||
|
||||
if is_picid_not_reply:
|
||||
reply_probability = 0
|
||||
|
||||
return reply_probability
|
||||
|
||||
async def before_generate_reply_handle(self, message_id):
|
||||
@@ -71,8 +53,5 @@ class ClassicalWillingManager(BaseWillingManager):
|
||||
if current_willing < 1:
|
||||
self.chat_reply_willing[chat_id] = min(1.0, current_willing + 0.4)
|
||||
|
||||
async def bombing_buffer_message_handle(self, message_id):
|
||||
return await super().bombing_buffer_message_handle(message_id)
|
||||
|
||||
async def not_reply_handle(self, message_id):
|
||||
return await super().not_reply_handle(message_id)
|
||||
|
||||
@@ -17,8 +17,5 @@ class CustomWillingManager(BaseWillingManager):
|
||||
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__()
|
||||
|
||||
@@ -19,7 +19,6 @@ Mxp 模式:梦溪畔独家赞助
|
||||
下下策是询问一个菜鸟(@梦溪畔)
|
||||
"""
|
||||
|
||||
from src.config.config import global_config
|
||||
from .willing_manager import BaseWillingManager
|
||||
from typing import Dict
|
||||
import asyncio
|
||||
@@ -173,22 +172,10 @@ class MxpWillingManager(BaseWillingManager):
|
||||
|
||||
probability = self._willing_to_probability(current_willing)
|
||||
|
||||
if w_info.is_emoji:
|
||||
probability *= global_config.normal_chat.emoji_response_penalty
|
||||
|
||||
if w_info.is_picid:
|
||||
probability = 0 # picid格式消息直接不回复
|
||||
|
||||
self.temporary_willing = current_willing
|
||||
|
||||
return probability
|
||||
|
||||
async def bombing_buffer_message_handle(self, message_id: str):
|
||||
"""炸飞消息处理"""
|
||||
async with self.lock:
|
||||
w_info = self.ongoing_messages[message_id]
|
||||
self.chat_person_reply_willing[w_info.chat_id][w_info.person_id] += 0.1
|
||||
|
||||
async def _return_to_basic_willing(self):
|
||||
"""使每个人的意愿恢复到chat基础意愿"""
|
||||
while True:
|
||||
|
||||
@@ -20,7 +20,6 @@ before_generate_reply_handle 确定要回复后,在生成回复前的处理
|
||||
after_generate_reply_handle 确定要回复后,在生成回复后的处理
|
||||
not_reply_handle 确定不回复后的处理
|
||||
get_reply_probability 获取回复概率
|
||||
bombing_buffer_message_handle 缓冲器炸飞消息后的处理
|
||||
get_variable_parameters 暂不确定
|
||||
set_variable_parameters 暂不确定
|
||||
以下2个方法根据你的实现可以做调整:
|
||||
@@ -137,11 +136,6 @@ class BaseWillingManager(ABC):
|
||||
"""抽象方法:获取回复概率"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def bombing_buffer_message_handle(self, message_id: str):
|
||||
"""抽象方法:炸飞消息处理"""
|
||||
pass
|
||||
|
||||
async def get_willing(self, chat_id: str):
|
||||
"""获取指定聊天流的回复意愿"""
|
||||
async with self.lock:
|
||||
|
||||
@@ -292,10 +292,6 @@ class ActionManager:
|
||||
)
|
||||
self._using_actions = self._default_actions.copy()
|
||||
|
||||
def restore_default_actions(self) -> None:
|
||||
"""恢复默认动作集到使用集"""
|
||||
self._using_actions = self._default_actions.copy()
|
||||
|
||||
def add_system_action_if_needed(self, action_name: str) -> bool:
|
||||
"""
|
||||
根据需要添加系统动作到使用集
|
||||
@@ -1,8 +1,6 @@
|
||||
from typing import List, Optional, Any, Dict
|
||||
from src.chat.heart_flow.observation.observation import Observation
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.heart_flow.observation.hfcloop_observation import HFCloopObservation
|
||||
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
|
||||
from src.chat.focus_chat.focus_loop_info import FocusLoopInfo
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.config.config import global_config
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
@@ -10,7 +8,8 @@ import random
|
||||
import asyncio
|
||||
import hashlib
|
||||
import time
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.chat.planner_actions.action_manager import ActionManager
|
||||
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages
|
||||
|
||||
logger = get_logger("action_manager")
|
||||
|
||||
@@ -23,12 +22,13 @@ class ActionModifier:
|
||||
支持并行判定和智能缓存优化。
|
||||
"""
|
||||
|
||||
log_prefix = "动作处理"
|
||||
|
||||
def __init__(self, action_manager: ActionManager):
|
||||
def __init__(self, action_manager: ActionManager, chat_id: str):
|
||||
"""初始化动作处理器"""
|
||||
self.chat_id = chat_id
|
||||
self.chat_stream = get_chat_manager().get_stream(self.chat_id)
|
||||
self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]"
|
||||
|
||||
self.action_manager = action_manager
|
||||
self.all_actions = self.action_manager.get_using_actions_for_mode("focus")
|
||||
|
||||
# 用于LLM判定的小模型
|
||||
self.llm_judge = LLMRequest(
|
||||
@@ -43,11 +43,12 @@ class ActionModifier:
|
||||
|
||||
async def modify_actions(
|
||||
self,
|
||||
observations: Optional[List[Observation]] = None,
|
||||
**kwargs: Any,
|
||||
loop_info=None,
|
||||
mode: str = "focus",
|
||||
message_content: str = "",
|
||||
):
|
||||
"""
|
||||
完整的动作修改流程,整合传统观察处理和新的激活类型判定
|
||||
动作修改流程,整合传统观察处理和新的激活类型判定
|
||||
|
||||
这个方法处理完整的动作管理流程:
|
||||
1. 基于观察的传统动作修改(循环历史分析、类型匹配等)
|
||||
@@ -57,230 +58,150 @@ class ActionModifier:
|
||||
"""
|
||||
logger.debug(f"{self.log_prefix}开始完整动作修改流程")
|
||||
|
||||
# === 第一阶段:传统观察处理 ===
|
||||
chat_content = None
|
||||
removals_s1 = []
|
||||
removals_s2 = []
|
||||
|
||||
if observations:
|
||||
hfc_obs = None
|
||||
chat_obs = None
|
||||
self.action_manager.restore_actions()
|
||||
all_actions = self.action_manager.get_using_actions_for_mode(mode)
|
||||
|
||||
# 收集所有观察对象
|
||||
for obs in observations:
|
||||
if isinstance(obs, HFCloopObservation):
|
||||
hfc_obs = obs
|
||||
if isinstance(obs, ChattingObservation):
|
||||
chat_obs = obs
|
||||
chat_content = obs.talking_message_str_truncate_short
|
||||
|
||||
# 合并所有动作变更
|
||||
merged_action_changes = {"add": [], "remove": []}
|
||||
reasons = []
|
||||
|
||||
# 处理HFCloopObservation - 传统的循环历史分析
|
||||
if hfc_obs:
|
||||
obs = hfc_obs
|
||||
# 获取适用于FOCUS模式的动作
|
||||
all_actions = self.all_actions
|
||||
action_changes = await self.analyze_loop_actions(obs)
|
||||
if action_changes["add"] or action_changes["remove"]:
|
||||
# 合并动作变更
|
||||
merged_action_changes["add"].extend(action_changes["add"])
|
||||
merged_action_changes["remove"].extend(action_changes["remove"])
|
||||
reasons.append("基于循环历史分析")
|
||||
|
||||
# 详细记录循环历史分析的变更原因
|
||||
for action_name in action_changes["add"]:
|
||||
logger.info(f"{self.log_prefix}添加动作: {action_name},原因: 循环历史分析建议添加")
|
||||
for action_name in action_changes["remove"]:
|
||||
logger.info(f"{self.log_prefix}移除动作: {action_name},原因: 循环历史分析建议移除")
|
||||
|
||||
# 处理ChattingObservation - 传统的类型匹配检查
|
||||
if chat_obs:
|
||||
# 检查动作的关联类型
|
||||
chat_context = get_chat_manager().get_stream(chat_obs.chat_id).context
|
||||
type_mismatched_actions = []
|
||||
|
||||
for action_name in all_actions.keys():
|
||||
data = all_actions[action_name]
|
||||
if data.get("associated_types"):
|
||||
if not chat_context.check_types(data["associated_types"]):
|
||||
type_mismatched_actions.append(action_name)
|
||||
associated_types_str = ", ".join(data["associated_types"])
|
||||
logger.info(
|
||||
f"{self.log_prefix}移除动作: {action_name},原因: 关联类型不匹配(需要: {associated_types_str})"
|
||||
message_list_before_now_half = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=self.chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=int(global_config.chat.max_context_size * 0.5),
|
||||
)
|
||||
chat_content = build_readable_messages(
|
||||
message_list_before_now_half,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
if message_content:
|
||||
chat_content = chat_content + "\n" + f"现在,最新的消息是:{message_content}"
|
||||
|
||||
# === 第一阶段:传统观察处理 ===
|
||||
if loop_info:
|
||||
removals_from_loop = await self.analyze_loop_actions(loop_info)
|
||||
if removals_from_loop:
|
||||
removals_s1.extend(removals_from_loop)
|
||||
|
||||
# 检查动作的关联类型
|
||||
chat_context = self.chat_stream.context
|
||||
type_mismatched_actions = self._check_action_associated_types(all_actions, chat_context)
|
||||
|
||||
if type_mismatched_actions:
|
||||
# 合并到移除列表中
|
||||
merged_action_changes["remove"].extend(type_mismatched_actions)
|
||||
reasons.append("基于关联类型检查")
|
||||
removals_s1.extend(type_mismatched_actions)
|
||||
|
||||
# 应用传统的动作变更到ActionManager
|
||||
for action_name in merged_action_changes["add"]:
|
||||
if action_name in self.action_manager.get_registered_actions():
|
||||
self.action_manager.add_action_to_using(action_name)
|
||||
logger.debug(f"{self.log_prefix}应用添加动作: {action_name},原因集合: {reasons}")
|
||||
|
||||
for action_name in merged_action_changes["remove"]:
|
||||
# 应用第一阶段的移除
|
||||
for action_name, reason in removals_s1:
|
||||
self.action_manager.remove_action_from_using(action_name)
|
||||
logger.debug(f"{self.log_prefix}应用移除动作: {action_name},原因集合: {reasons}")
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix}传统动作修改完成,当前使用动作: {list(self.action_manager.get_using_actions().keys())}"
|
||||
)
|
||||
|
||||
# 注释:已移除exit_focus_chat动作,现在由no_reply动作处理频率检测退出专注模式
|
||||
logger.debug(f"{self.log_prefix}阶段一移除动作: {action_name},原因: {reason}")
|
||||
|
||||
# === 第二阶段:激活类型判定 ===
|
||||
# 如果提供了聊天上下文,则进行激活类型判定
|
||||
if chat_content is not None:
|
||||
logger.debug(f"{self.log_prefix}开始激活类型判定阶段")
|
||||
|
||||
# 获取当前使用的动作集(经过第一阶段处理,且适用于FOCUS模式)
|
||||
current_using_actions = self.action_manager.get_using_actions()
|
||||
all_registered_actions = self.action_manager.get_registered_actions()
|
||||
# 获取当前使用的动作集(经过第一阶段处理)
|
||||
current_using_actions = self.action_manager.get_using_actions_for_mode(mode)
|
||||
|
||||
# 构建完整的动作信息
|
||||
current_actions_with_info = {}
|
||||
for action_name in current_using_actions.keys():
|
||||
if action_name in all_registered_actions:
|
||||
current_actions_with_info[action_name] = all_registered_actions[action_name]
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到")
|
||||
|
||||
# 应用激活类型判定
|
||||
final_activated_actions = await self._apply_activation_type_filtering(
|
||||
current_actions_with_info,
|
||||
# 获取因激活类型判定而需要移除的动作
|
||||
removals_s2 = await self._get_deactivated_actions_by_type(
|
||||
current_using_actions,
|
||||
mode,
|
||||
chat_content,
|
||||
)
|
||||
|
||||
# 更新ActionManager,移除未激活的动作
|
||||
actions_to_remove = []
|
||||
removal_reasons = {}
|
||||
|
||||
for action_name in current_using_actions.keys():
|
||||
if action_name not in final_activated_actions:
|
||||
actions_to_remove.append(action_name)
|
||||
# 确定移除原因
|
||||
if action_name in all_registered_actions:
|
||||
action_info = all_registered_actions[action_name]
|
||||
activation_type = action_info.get("focus_activation_type", "always")
|
||||
|
||||
# 处理字符串格式的激活类型值
|
||||
if activation_type == "random":
|
||||
probability = action_info.get("random_probability", 0.3)
|
||||
removal_reasons[action_name] = f"RANDOM类型未触发(概率{probability})"
|
||||
elif activation_type == "llm_judge":
|
||||
removal_reasons[action_name] = "LLM判定未激活"
|
||||
elif activation_type == "keyword":
|
||||
keywords = action_info.get("activation_keywords", [])
|
||||
removal_reasons[action_name] = f"关键词未匹配(关键词: {keywords})"
|
||||
else:
|
||||
removal_reasons[action_name] = "激活判定未通过"
|
||||
else:
|
||||
removal_reasons[action_name] = "动作信息不完整"
|
||||
|
||||
for action_name in actions_to_remove:
|
||||
# 应用第二阶段的移除
|
||||
for action_name, reason in removals_s2:
|
||||
self.action_manager.remove_action_from_using(action_name)
|
||||
reason = removal_reasons.get(action_name, "未知原因")
|
||||
logger.info(f"{self.log_prefix}移除动作: {action_name},原因: {reason}")
|
||||
logger.debug(f"{self.log_prefix}阶段二移除动作: {action_name},原因: {reason}")
|
||||
|
||||
# 注释:已完全移除exit_focus_chat动作
|
||||
|
||||
logger.info(f"{self.log_prefix}激活类型判定完成,最终可用动作: {list(final_activated_actions.keys())}")
|
||||
# === 统一日志记录 ===
|
||||
all_removals = removals_s1 + removals_s2
|
||||
if all_removals:
|
||||
removals_summary = " | ".join([f"{name}({reason})" for name, reason in all_removals])
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix}完整动作修改流程结束,最终动作集: {list(self.action_manager.get_using_actions().keys())}"
|
||||
f"{self.log_prefix}{mode}模式动作修改流程结束,最终可用动作: {list(self.action_manager.get_using_actions_for_mode(mode).keys())}||移除记录: {removals_summary}"
|
||||
)
|
||||
|
||||
async def _apply_activation_type_filtering(
|
||||
def _check_action_associated_types(self, all_actions, chat_context):
|
||||
type_mismatched_actions = []
|
||||
for action_name, data in all_actions.items():
|
||||
if data.get("associated_types"):
|
||||
if not chat_context.check_types(data["associated_types"]):
|
||||
associated_types_str = ", ".join(data["associated_types"])
|
||||
reason = f"适配器不支持(需要: {associated_types_str})"
|
||||
type_mismatched_actions.append((action_name, reason))
|
||||
logger.debug(f"{self.log_prefix}决定移除动作: {action_name},原因: {reason}")
|
||||
return type_mismatched_actions
|
||||
|
||||
async def _get_deactivated_actions_by_type(
|
||||
self,
|
||||
actions_with_info: Dict[str, Any],
|
||||
mode: str = "focus",
|
||||
chat_content: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
) -> List[tuple[str, str]]:
|
||||
"""
|
||||
应用激活类型过滤逻辑,支持四种激活类型的并行处理
|
||||
根据激活类型过滤,返回需要停用的动作列表及原因
|
||||
|
||||
Args:
|
||||
actions_with_info: 带完整信息的动作字典
|
||||
chat_content: 聊天内容
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 过滤后激活的actions字典
|
||||
List[Tuple[str, str]]: 需要停用的 (action_name, reason) 元组列表
|
||||
"""
|
||||
activated_actions = {}
|
||||
deactivated_actions = []
|
||||
|
||||
# 分类处理不同激活类型的actions
|
||||
always_actions = {}
|
||||
random_actions = {}
|
||||
llm_judge_actions = {}
|
||||
keyword_actions = {}
|
||||
|
||||
for action_name, action_info in actions_with_info.items():
|
||||
activation_type = action_info.get("focus_activation_type", "always")
|
||||
actions_to_check = list(actions_with_info.items())
|
||||
random.shuffle(actions_to_check)
|
||||
|
||||
# print(f"action_name: {action_name}, activation_type: {activation_type}")
|
||||
for action_name, action_info in actions_to_check:
|
||||
activation_type = f"{mode}_activation_type"
|
||||
activation_type = action_info.get(activation_type, "always")
|
||||
|
||||
# 现在统一是字符串格式的激活类型值
|
||||
if activation_type == "always":
|
||||
always_actions[action_name] = action_info
|
||||
continue # 总是激活,无需处理
|
||||
|
||||
elif activation_type == "random":
|
||||
random_actions[action_name] = action_info
|
||||
probability = action_info.get("random_activation_probability", ActionManager.DEFAULT_RANDOM_PROBABILITY)
|
||||
if not (random.random() < probability):
|
||||
reason = f"RANDOM类型未触发(概率{probability})"
|
||||
deactivated_actions.append((action_name, reason))
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}")
|
||||
|
||||
elif activation_type == "keyword":
|
||||
if not self._check_keyword_activation(action_name, action_info, chat_content):
|
||||
keywords = action_info.get("activation_keywords", [])
|
||||
reason = f"关键词未匹配(关键词: {keywords})"
|
||||
deactivated_actions.append((action_name, reason))
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}")
|
||||
|
||||
elif activation_type == "llm_judge":
|
||||
llm_judge_actions[action_name] = action_info
|
||||
elif activation_type == "keyword":
|
||||
keyword_actions[action_name] = action_info
|
||||
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix}未知的激活类型: {activation_type},跳过处理")
|
||||
|
||||
# 1. 处理ALWAYS类型(直接激活)
|
||||
for action_name, action_info in always_actions.items():
|
||||
activated_actions[action_name] = action_info
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: ALWAYS类型直接激活")
|
||||
|
||||
# 2. 处理RANDOM类型
|
||||
for action_name, action_info in random_actions.items():
|
||||
probability = action_info.get("random_activation_probability", ActionManager.DEFAULT_RANDOM_PROBABILITY)
|
||||
should_activate = random.random() < probability
|
||||
if should_activate:
|
||||
activated_actions[action_name] = action_info
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: RANDOM类型触发(概率{probability})")
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: RANDOM类型未触发(概率{probability})")
|
||||
|
||||
# 3. 处理KEYWORD类型(快速判定)
|
||||
for action_name, action_info in keyword_actions.items():
|
||||
should_activate = self._check_keyword_activation(
|
||||
action_name,
|
||||
action_info,
|
||||
chat_content,
|
||||
)
|
||||
if should_activate:
|
||||
activated_actions[action_name] = action_info
|
||||
keywords = action_info.get("activation_keywords", [])
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: KEYWORD类型匹配关键词({keywords})")
|
||||
else:
|
||||
keywords = action_info.get("activation_keywords", [])
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: KEYWORD类型未匹配关键词({keywords})")
|
||||
|
||||
# 4. 处理LLM_JUDGE类型(并行判定)
|
||||
# 并行处理LLM_JUDGE类型
|
||||
if llm_judge_actions:
|
||||
# 直接并行处理所有LLM判定actions
|
||||
llm_results = await self._process_llm_judge_actions_parallel(
|
||||
llm_judge_actions,
|
||||
chat_content,
|
||||
)
|
||||
|
||||
# 添加激活的LLM判定actions
|
||||
for action_name, should_activate in llm_results.items():
|
||||
if should_activate:
|
||||
activated_actions[action_name] = llm_judge_actions[action_name]
|
||||
logger.debug(f"{self.log_prefix}激活动作: {action_name},原因: LLM_JUDGE类型判定通过")
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: LLM_JUDGE类型判定未通过")
|
||||
if not should_activate:
|
||||
reason = "LLM判定未激活"
|
||||
deactivated_actions.append((action_name, reason))
|
||||
logger.debug(f"{self.log_prefix}未激活动作: {action_name},原因: {reason}")
|
||||
|
||||
logger.debug(f"{self.log_prefix}激活类型过滤完成: {list(activated_actions.keys())}")
|
||||
return activated_actions
|
||||
return deactivated_actions
|
||||
|
||||
async def process_actions_for_planner(
|
||||
self, observed_messages_str: str = "", chat_context: Optional[str] = None, extra_context: Optional[str] = None
|
||||
@@ -538,22 +459,19 @@ class ActionModifier:
|
||||
logger.debug(f"{self.log_prefix}动作 {action_name} 未匹配到任何关键词: {activation_keywords}")
|
||||
return False
|
||||
|
||||
async def analyze_loop_actions(self, obs: HFCloopObservation) -> Dict[str, List[str]]:
|
||||
"""分析最近的循环内容并决定动作的增减
|
||||
async def analyze_loop_actions(self, obs: FocusLoopInfo) -> List[tuple[str, str]]:
|
||||
"""分析最近的循环内容并决定动作的移除
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: 包含要增加和删除的动作
|
||||
{
|
||||
"add": ["action1", "action2"],
|
||||
"remove": ["action3"]
|
||||
}
|
||||
List[Tuple[str, str]]: 包含要删除的动作及原因的元组列表
|
||||
[("action3", "some reason")]
|
||||
"""
|
||||
result = {"add": [], "remove": []}
|
||||
removals = []
|
||||
|
||||
# 获取最近10次循环
|
||||
recent_cycles = obs.history_loop[-10:] if len(obs.history_loop) > 10 else obs.history_loop
|
||||
if not recent_cycles:
|
||||
return result
|
||||
return removals
|
||||
|
||||
reply_sequence = [] # 记录最近的动作序列
|
||||
|
||||
@@ -584,36 +502,41 @@ class ActionModifier:
|
||||
# 根据最近的reply情况决定是否移除reply动作
|
||||
if len(last_max_reply_num) >= max_reply_num and all(last_max_reply_num):
|
||||
# 如果最近max_reply_num次都是reply,直接移除
|
||||
result["remove"].append("reply")
|
||||
reason = f"连续回复过多(最近{len(last_max_reply_num)}次全是reply,超过阈值{max_reply_num})"
|
||||
removals.append(("reply", reason))
|
||||
# reply_count = len(last_max_reply_num) - no_reply_count
|
||||
logger.info(
|
||||
f"{self.log_prefix}移除reply动作,原因: 连续回复过多(最近{len(last_max_reply_num)}次全是reply,超过阈值{max_reply_num})"
|
||||
)
|
||||
elif len(last_max_reply_num) >= sec_thres_reply_num and all(last_max_reply_num[-sec_thres_reply_num:]):
|
||||
# 如果最近sec_thres_reply_num次都是reply,40%概率移除
|
||||
removal_probability = 0.4 / global_config.focus_chat.consecutive_replies
|
||||
if random.random() < removal_probability:
|
||||
result["remove"].append("reply")
|
||||
logger.info(
|
||||
f"{self.log_prefix}移除reply动作,原因: 连续回复较多(最近{sec_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"{self.log_prefix}连续回复检测:最近{sec_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,未触发"
|
||||
reason = (
|
||||
f"连续回复较多(最近{sec_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)"
|
||||
)
|
||||
removals.append(("reply", reason))
|
||||
elif len(last_max_reply_num) >= one_thres_reply_num and all(last_max_reply_num[-one_thres_reply_num:]):
|
||||
# 如果最近one_thres_reply_num次都是reply,20%概率移除
|
||||
removal_probability = 0.2 / global_config.focus_chat.consecutive_replies
|
||||
if random.random() < removal_probability:
|
||||
result["remove"].append("reply")
|
||||
logger.info(
|
||||
f"{self.log_prefix}移除reply动作,原因: 连续回复检测(最近{one_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"{self.log_prefix}连续回复检测:最近{one_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,未触发"
|
||||
reason = (
|
||||
f"连续回复检测(最近{one_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)"
|
||||
)
|
||||
removals.append(("reply", reason))
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix}连续回复检测:无需移除reply动作,最近回复模式正常")
|
||||
|
||||
return result
|
||||
return removals
|
||||
|
||||
def get_available_actions_count(self) -> int:
|
||||
"""获取当前可用动作数量(排除默认的no_action)"""
|
||||
current_actions = self.action_manager.get_using_actions_for_mode("normal")
|
||||
# 排除no_action(如果存在)
|
||||
filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"}
|
||||
return len(filtered_actions)
|
||||
|
||||
def should_skip_planning(self) -> bool:
|
||||
"""判断是否应该跳过规划过程"""
|
||||
available_count = self.get_available_actions_count()
|
||||
if available_count == 0:
|
||||
logger.debug(f"{self.log_prefix} 没有可用动作,跳过规划")
|
||||
return True
|
||||
return False
|
||||
@@ -1,19 +1,18 @@
|
||||
import json # <--- 确保导入 json
|
||||
import traceback
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional
|
||||
from rich.traceback import install
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.chat.focus_chat.info.info_base import InfoBase
|
||||
from src.chat.focus_chat.info.obs_info import ObsInfo
|
||||
from src.chat.focus_chat.info.action_info import ActionInfo
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.chat.focus_chat.planners.action_manager import ActionManager
|
||||
from src.chat.planner_actions.action_manager import ActionManager
|
||||
from json_repair import repair_json
|
||||
from src.chat.focus_chat.planners.base_planner import BasePlanner
|
||||
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
|
||||
from src.chat.utils.utils import get_chat_type_and_target_info
|
||||
from datetime import datetime
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
import time
|
||||
|
||||
logger = get_logger("planner")
|
||||
|
||||
@@ -29,34 +28,22 @@ def init_prompt():
|
||||
{chat_context_description},以下是具体的聊天内容:
|
||||
{chat_content_block}
|
||||
{moderation_prompt}
|
||||
现在请你根据聊天内容选择合适的action:
|
||||
|
||||
现在请你根据{by_what}选择合适的action:
|
||||
{no_action_block}
|
||||
{action_options_text}
|
||||
|
||||
你必须从上面列出的可用action中选择一个,并说明原因。
|
||||
|
||||
请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
|
||||
""",
|
||||
"simple_planner_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
{time_block}
|
||||
{indentify_block}
|
||||
你现在需要根据聊天内容,选择的合适的action来参与聊天。
|
||||
{chat_context_description},以下是具体的聊天内容:
|
||||
{chat_content_block}
|
||||
{moderation_prompt}
|
||||
现在请你选择合适的action:
|
||||
|
||||
{action_options_text}
|
||||
|
||||
请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
|
||||
""",
|
||||
"simple_planner_prompt_private",
|
||||
"planner_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
动作:{action_name}
|
||||
动作描述:{action_description}
|
||||
{action_require}
|
||||
{{
|
||||
"action": "{action_name}",{action_parameters}
|
||||
@@ -65,41 +52,24 @@ def init_prompt():
|
||||
"action_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
{action_require}
|
||||
{{
|
||||
"action": "{action_name}",{action_parameters}
|
||||
}}
|
||||
""",
|
||||
"action_prompt_private",
|
||||
)
|
||||
|
||||
|
||||
class ActionPlanner(BasePlanner):
|
||||
def __init__(self, log_prefix: str, action_manager: ActionManager):
|
||||
super().__init__(log_prefix, action_manager)
|
||||
class ActionPlanner:
|
||||
def __init__(self, chat_id: str, action_manager: ActionManager, mode: str = "focus"):
|
||||
self.chat_id = chat_id
|
||||
self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]"
|
||||
self.mode = mode
|
||||
self.action_manager = action_manager
|
||||
# LLM规划器配置
|
||||
self.planner_llm = LLMRequest(
|
||||
model=global_config.model.planner,
|
||||
request_type="focus.planner", # 用于动作规划
|
||||
request_type=f"{self.mode}.planner", # 用于动作规划
|
||||
)
|
||||
|
||||
self.utils_llm = LLMRequest(
|
||||
model=global_config.model.utils_small,
|
||||
request_type="focus.planner", # 用于动作规划
|
||||
)
|
||||
self.last_obs_time_mark = 0.0
|
||||
|
||||
async def plan(
|
||||
self, all_plan_info: List[InfoBase], running_memorys: List[Dict[str, Any]], loop_start_time: float
|
||||
) -> Dict[str, Any]:
|
||||
async def plan(self) -> Dict[str, Any]:
|
||||
"""
|
||||
规划器 (Planner): 使用LLM根据上下文决定做出什么动作。
|
||||
|
||||
参数:
|
||||
all_plan_info: 所有计划信息
|
||||
running_memorys: 回忆信息
|
||||
loop_start_time: 循环开始时间
|
||||
"""
|
||||
|
||||
action = "no_reply" # 默认动作
|
||||
@@ -107,47 +77,12 @@ class ActionPlanner(BasePlanner):
|
||||
action_data = {}
|
||||
|
||||
try:
|
||||
# 获取观察信息
|
||||
extra_info: list[str] = []
|
||||
|
||||
extra_info = []
|
||||
observed_messages = []
|
||||
observed_messages_str = ""
|
||||
chat_type = "group"
|
||||
is_group_chat = True
|
||||
chat_id = None # 添加chat_id变量
|
||||
|
||||
for info in all_plan_info:
|
||||
if isinstance(info, ObsInfo):
|
||||
observed_messages = info.get_talking_message()
|
||||
observed_messages_str = info.get_talking_message_str_truncate_short()
|
||||
chat_type = info.get_chat_type()
|
||||
is_group_chat = chat_type == "group"
|
||||
# 从ObsInfo中获取chat_id
|
||||
chat_id = info.get_chat_id()
|
||||
else:
|
||||
extra_info.append(info.get_processed_info())
|
||||
is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id)
|
||||
logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}")
|
||||
|
||||
# 获取聊天类型和目标信息
|
||||
chat_target_info = None
|
||||
if chat_id:
|
||||
try:
|
||||
# 重新获取更准确的聊天信息
|
||||
is_group_chat_updated, chat_target_info = get_chat_type_and_target_info(chat_id)
|
||||
# 如果获取成功,更新is_group_chat
|
||||
if is_group_chat_updated is not None:
|
||||
is_group_chat = is_group_chat_updated
|
||||
logger.debug(
|
||||
f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"{self.log_prefix}获取聊天目标信息失败: {e}")
|
||||
chat_target_info = None
|
||||
|
||||
# 获取经过modify_actions处理后的最终可用动作集
|
||||
# 注意:动作的激活判定现在在主循环的modify_actions中完成
|
||||
# 使用Focus模式过滤动作
|
||||
current_available_actions_dict = self.action_manager.get_using_actions_for_mode("focus")
|
||||
current_available_actions_dict = self.action_manager.get_using_actions_for_mode(self.mode)
|
||||
|
||||
# 获取完整的动作信息
|
||||
all_registered_actions = self.action_manager.get_registered_actions()
|
||||
@@ -165,31 +100,29 @@ class ActionPlanner(BasePlanner):
|
||||
action = "no_reply"
|
||||
reasoning = "没有可用的动作" if not current_available_actions else "只有no_reply动作可用,跳过规划"
|
||||
logger.info(f"{self.log_prefix}{reasoning}")
|
||||
self.action_manager.restore_actions()
|
||||
logger.debug(
|
||||
f"{self.log_prefix}[focus]沉默后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}"
|
||||
)
|
||||
return {
|
||||
"action_result": {"action_type": action, "action_data": action_data, "reasoning": reasoning},
|
||||
"observed_messages": observed_messages,
|
||||
}
|
||||
|
||||
# --- 构建提示词 (调用修改后的 PromptBuilder 方法) ---
|
||||
prompt = await self.build_planner_prompt(
|
||||
is_group_chat=is_group_chat, # <-- Pass HFC state
|
||||
chat_target_info=chat_target_info, # <-- 传递获取到的聊天目标信息
|
||||
observed_messages_str=observed_messages_str, # <-- Pass local variable
|
||||
current_available_actions=current_available_actions, # <-- Pass determined actions
|
||||
)
|
||||
|
||||
# --- 调用 LLM (普通文本生成) ---
|
||||
llm_content = None
|
||||
try:
|
||||
prompt = f"{prompt}"
|
||||
llm_content, (reasoning_content, _) = await self.planner_llm.generate_response_async(prompt=prompt)
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}")
|
||||
logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}")
|
||||
if reasoning_content:
|
||||
logger.info(f"{self.log_prefix}规划器推理: {reasoning_content}")
|
||||
|
||||
except Exception as req_e:
|
||||
@@ -199,34 +132,21 @@ class ActionPlanner(BasePlanner):
|
||||
|
||||
if llm_content:
|
||||
try:
|
||||
fixed_json_string = repair_json(llm_content)
|
||||
if isinstance(fixed_json_string, str):
|
||||
try:
|
||||
parsed_json = json.loads(fixed_json_string)
|
||||
except json.JSONDecodeError as decode_error:
|
||||
logger.error(f"JSON解析错误: {str(decode_error)}")
|
||||
parsed_json = {}
|
||||
else:
|
||||
# 如果repair_json直接返回了字典对象,直接使用
|
||||
parsed_json = fixed_json_string
|
||||
parsed_json = json.loads(repair_json(llm_content))
|
||||
|
||||
# 处理repair_json可能返回列表的情况
|
||||
if isinstance(parsed_json, list):
|
||||
if parsed_json:
|
||||
# 取列表中最后一个元素(通常是最完整的)
|
||||
parsed_json = parsed_json[-1]
|
||||
logger.warning(f"{self.log_prefix}LLM返回了多个JSON对象,使用最后一个: {parsed_json}")
|
||||
else:
|
||||
parsed_json = {}
|
||||
|
||||
# 确保parsed_json是字典
|
||||
if not isinstance(parsed_json, dict):
|
||||
logger.error(f"{self.log_prefix}解析后的JSON不是字典类型: {type(parsed_json)}")
|
||||
parsed_json = {}
|
||||
|
||||
# 提取决策,提供默认值
|
||||
extracted_action = parsed_json.get("action", "no_reply")
|
||||
extracted_reasoning = ""
|
||||
action = parsed_json.get("action", "no_reply")
|
||||
reasoning = parsed_json.get("reasoning", "未提供原因")
|
||||
|
||||
# 将所有其他属性添加到action_data
|
||||
action_data = {}
|
||||
@@ -234,20 +154,14 @@ class ActionPlanner(BasePlanner):
|
||||
if key not in ["action", "reasoning"]:
|
||||
action_data[key] = value
|
||||
|
||||
action_data["loop_start_time"] = loop_start_time
|
||||
|
||||
# 对于reply动作不需要额外处理,因为相关字段已经在上面的循环中添加到action_data
|
||||
|
||||
if extracted_action not in current_available_actions:
|
||||
if action == "no_action":
|
||||
reasoning = "normal决定不使用额外动作"
|
||||
elif action not in current_available_actions:
|
||||
logger.warning(
|
||||
f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
|
||||
f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
|
||||
)
|
||||
action = "no_reply"
|
||||
reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
|
||||
else:
|
||||
# 动作有效且可用
|
||||
action = extracted_action
|
||||
reasoning = extracted_reasoning
|
||||
reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {list(current_available_actions.keys())})。原始理由: {reasoning}"
|
||||
|
||||
except Exception as json_e:
|
||||
logger.warning(f"{self.log_prefix}解析LLM响应JSON失败 {json_e}. LLM原始输出: '{llm_content}'")
|
||||
@@ -261,17 +175,21 @@ class ActionPlanner(BasePlanner):
|
||||
action = "no_reply"
|
||||
reasoning = f"Planner 内部处理错误: {outer_e}"
|
||||
|
||||
# 恢复到默认动作集
|
||||
self.action_manager.restore_actions()
|
||||
logger.debug(
|
||||
f"{self.log_prefix}规划后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}"
|
||||
)
|
||||
is_parallel = False
|
||||
if action in current_available_actions:
|
||||
action_info = current_available_actions[action]
|
||||
is_parallel = action_info.get("parallel_action", False)
|
||||
|
||||
action_result = {"action_type": action, "action_data": action_data, "reasoning": reasoning}
|
||||
action_result = {
|
||||
"action_type": action,
|
||||
"action_data": action_data,
|
||||
"reasoning": reasoning,
|
||||
"timestamp": time.time(),
|
||||
"is_parallel": is_parallel,
|
||||
}
|
||||
|
||||
plan_result = {
|
||||
"action_result": action_result,
|
||||
"observed_messages": observed_messages,
|
||||
"action_prompt": prompt,
|
||||
}
|
||||
|
||||
@@ -281,11 +199,35 @@ class ActionPlanner(BasePlanner):
|
||||
self,
|
||||
is_group_chat: bool, # Now passed as argument
|
||||
chat_target_info: Optional[dict], # Now passed as argument
|
||||
observed_messages_str: str,
|
||||
current_available_actions: Dict[str, ActionInfo],
|
||||
current_available_actions,
|
||||
) -> str:
|
||||
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
|
||||
try:
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=self.chat_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.chat.max_context_size,
|
||||
)
|
||||
|
||||
chat_content_block = build_readable_messages(
|
||||
messages=message_list_before_now,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
read_mark=self.last_obs_time_mark,
|
||||
truncate=True,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
self.last_obs_time_mark = time.time()
|
||||
|
||||
if self.mode == "focus":
|
||||
by_what = "聊天内容"
|
||||
no_action_block = ""
|
||||
else:
|
||||
by_what = "聊天内容和用户的最新消息"
|
||||
no_action_block = """重要说明:
|
||||
- 'no_action' 表示只进行普通聊天回复,不执行任何额外动作
|
||||
- 其他action表示在普通回复的基础上,执行相应的额外动作"""
|
||||
|
||||
chat_context_description = "你现在正在一个群聊中"
|
||||
chat_target_name = None # Only relevant for private
|
||||
if not is_group_chat and chat_target_info:
|
||||
@@ -294,19 +236,9 @@ class ActionPlanner(BasePlanner):
|
||||
)
|
||||
chat_context_description = f"你正在和 {chat_target_name} 私聊"
|
||||
|
||||
chat_content_block = ""
|
||||
if observed_messages_str:
|
||||
chat_content_block = f"\n{observed_messages_str}"
|
||||
else:
|
||||
chat_content_block = "你还未开始聊天"
|
||||
|
||||
action_options_block = ""
|
||||
# 根据聊天类型选择不同的动作prompt模板
|
||||
action_template_name = "action_prompt_private" if not is_group_chat else "action_prompt"
|
||||
|
||||
for using_actions_name, using_actions_info in current_available_actions.items():
|
||||
using_action_prompt = await global_prompt_manager.get_prompt_async(action_template_name)
|
||||
|
||||
if using_actions_info["parameters"]:
|
||||
param_text = "\n"
|
||||
for param_name, param_description in using_actions_info["parameters"].items():
|
||||
@@ -320,16 +252,7 @@ class ActionPlanner(BasePlanner):
|
||||
require_text += f"- {require_item}\n"
|
||||
require_text = require_text.rstrip("\n")
|
||||
|
||||
# 根据模板类型决定是否包含description参数
|
||||
if action_template_name == "action_prompt_private":
|
||||
# 私聊模板不包含description参数
|
||||
using_action_prompt = using_action_prompt.format(
|
||||
action_name=using_actions_name,
|
||||
action_parameters=param_text,
|
||||
action_require=require_text,
|
||||
)
|
||||
else:
|
||||
# 群聊模板包含description参数
|
||||
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
|
||||
using_action_prompt = using_action_prompt.format(
|
||||
action_name=using_actions_name,
|
||||
action_description=using_actions_info["description"],
|
||||
@@ -339,10 +262,8 @@ class ActionPlanner(BasePlanner):
|
||||
|
||||
action_options_block += using_action_prompt
|
||||
|
||||
# moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
|
||||
moderation_prompt_block = ""
|
||||
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
|
||||
|
||||
# 获取当前时间
|
||||
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
bot_name = global_config.bot.nickname
|
||||
@@ -353,13 +274,13 @@ class ActionPlanner(BasePlanner):
|
||||
bot_core_personality = global_config.personality.personality_core
|
||||
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:"
|
||||
|
||||
# 根据聊天类型选择不同的prompt模板
|
||||
template_name = "simple_planner_prompt_private" if not is_group_chat else "simple_planner_prompt"
|
||||
planner_prompt_template = await global_prompt_manager.get_prompt_async(template_name)
|
||||
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
|
||||
prompt = planner_prompt_template.format(
|
||||
time_block=time_block,
|
||||
by_what=by_what,
|
||||
chat_context_description=chat_context_description,
|
||||
chat_content_block=chat_content_block,
|
||||
no_action_block=no_action_block,
|
||||
action_options_text=action_options_block,
|
||||
moderation_prompt=moderation_prompt_block,
|
||||
indentify_block=indentify_block,
|
||||
@@ -9,13 +9,12 @@ from src.common.logger import get_logger
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.chat.utils.timer_calculator import Timer # <--- Import Timer
|
||||
from src.chat.focus_chat.heartFC_sender import HeartFCSender
|
||||
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
|
||||
from src.chat.message_receive.uni_message_sender import HeartFCSender
|
||||
from src.chat.utils.utils import get_chat_type_and_target_info
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
from src.chat.express.exprssion_learner import get_expression_learner
|
||||
import time
|
||||
import asyncio
|
||||
from src.chat.express.expression_selector import expression_selector
|
||||
@@ -27,7 +26,7 @@ from src.person_info.person_info import get_person_info_manager
|
||||
from datetime import datetime
|
||||
import re
|
||||
from src.chat.knowledge.knowledge_lib import qa_manager
|
||||
from src.chat.focus_chat.memory_activator import MemoryActivator
|
||||
from src.chat.memory_system.memory_activator import MemoryActivator
|
||||
from src.tools.tool_executor import ToolExecutor
|
||||
|
||||
logger = get_logger("replyer")
|
||||
@@ -37,13 +36,14 @@ def init_prompt():
|
||||
Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1")
|
||||
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
|
||||
Prompt("在群里聊天", "chat_target_group2")
|
||||
Prompt("和{sender_name}私聊", "chat_target_private2")
|
||||
Prompt("和{sender_name}聊天", "chat_target_private2")
|
||||
Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
{expression_habits_block}
|
||||
{tool_info_block}
|
||||
{knowledge_prompt}
|
||||
{memory_block}
|
||||
{relation_info_block}
|
||||
{extra_info_block}
|
||||
@@ -68,84 +68,36 @@ def init_prompt():
|
||||
Prompt(
|
||||
"""
|
||||
{expression_habits_block}
|
||||
{tool_info_block}
|
||||
{memory_block}
|
||||
{relation_info_block}
|
||||
{extra_info_block}
|
||||
|
||||
{chat_target}
|
||||
{time_block}
|
||||
{chat_target}
|
||||
{chat_info}
|
||||
现在"{sender_name}"说:{target_message}。你想要回复对方的这条消息。
|
||||
{identity},
|
||||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。注意不要复读你说过的话。
|
||||
{identity}
|
||||
|
||||
{config_expression_style}。回复不要浮夸,不要用夸张修辞,平淡一些。
|
||||
{keywords_reaction_prompt}
|
||||
请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。
|
||||
不要浮夸,不要夸张修辞,请注意不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出一条回复就好。
|
||||
现在,你说:
|
||||
""",
|
||||
"default_generator_private_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
你可以参考你的以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
|
||||
{style_habbits}
|
||||
|
||||
你现在正在群里聊天,以下是群里正在进行的聊天内容:
|
||||
{chat_info}
|
||||
|
||||
以上是聊天内容,你需要了解聊天记录中的内容
|
||||
|
||||
{chat_target}
|
||||
你的名字是{bot_name},{prompt_personality},在这聊天中,"{sender_name}"说的"{target_message}"引起了你的注意,对这句话,你想表达:{raw_reply},原因是:{reason}。你现在要思考怎么回复
|
||||
你正在{chat_target_2},{reply_target_block}
|
||||
对这句话,你想表达,原句:{raw_reply},原因是:{reason}。你现在要思考怎么组织回复
|
||||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯
|
||||
请你根据情景使用以下句法:
|
||||
{grammar_habbits}
|
||||
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
|
||||
{keywords_reaction_prompt}
|
||||
{moderation_prompt}
|
||||
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。
|
||||
现在,你说:
|
||||
""",
|
||||
"default_expressor_prompt",
|
||||
)
|
||||
|
||||
Prompt(
|
||||
"""
|
||||
你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
|
||||
{style_habbits}
|
||||
|
||||
你现在正在群里聊天,以下是群里正在进行的聊天内容:
|
||||
{chat_info}
|
||||
|
||||
以上是聊天内容,你需要了解聊天记录中的内容
|
||||
|
||||
{chat_target}
|
||||
你的名字是{bot_name},{prompt_personality},在这聊天中,"{sender_name}"说的"{target_message}"引起了你的注意,对这句话,你想表达:{raw_reply},原因是:{reason}。你现在要思考怎么回复
|
||||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。
|
||||
请你根据情景使用以下句法:
|
||||
{grammar_habbits}
|
||||
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
|
||||
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。
|
||||
现在,你说:
|
||||
""",
|
||||
"default_expressor_private_prompt", # New template for private FOCUSED chat
|
||||
)
|
||||
|
||||
|
||||
class DefaultReplyer:
|
||||
def __init__(
|
||||
self,
|
||||
chat_stream: ChatStream,
|
||||
enable_tool: bool = False,
|
||||
model_configs: Optional[List[Dict[str, Any]]] = None,
|
||||
request_type: str = "focus.replyer",
|
||||
):
|
||||
self.log_prefix = "replyer"
|
||||
self.request_type = request_type
|
||||
|
||||
self.enable_tool = enable_tool
|
||||
|
||||
if model_configs:
|
||||
self.express_model_configs = model_configs
|
||||
else:
|
||||
@@ -215,9 +167,10 @@ class DefaultReplyer:
|
||||
self,
|
||||
reply_data: Dict[str, Any] = None,
|
||||
reply_to: str = "",
|
||||
relation_info: str = "",
|
||||
extra_info: str = "",
|
||||
available_actions: List[str] = None,
|
||||
enable_tool: bool = True,
|
||||
enable_timeout: bool = False,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
回复器 (Replier): 核心逻辑,负责生成回复文本。
|
||||
@@ -231,7 +184,6 @@ class DefaultReplyer:
|
||||
if not reply_data:
|
||||
reply_data = {
|
||||
"reply_to": reply_to,
|
||||
"relation_info": relation_info,
|
||||
"extra_info": extra_info,
|
||||
}
|
||||
for key, value in reply_data.items():
|
||||
@@ -243,6 +195,8 @@ class DefaultReplyer:
|
||||
prompt = await self.build_prompt_reply_context(
|
||||
reply_data=reply_data, # 传递action_data
|
||||
available_actions=available_actions,
|
||||
enable_timeout=enable_timeout,
|
||||
enable_tool=enable_tool,
|
||||
)
|
||||
|
||||
# 4. 调用 LLM 生成回复
|
||||
@@ -255,7 +209,7 @@ class DefaultReplyer:
|
||||
# 加权随机选择一个模型配置
|
||||
selected_model_config = self._select_weighted_model_config()
|
||||
logger.info(
|
||||
f"{self.log_prefix} 使用模型配置: {selected_model_config.get('model_name', 'N/A')} (权重: {selected_model_config.get('weight', 1.0)})"
|
||||
f"{self.log_prefix} 使用模型配置: {selected_model_config.get('name', 'N/A')} (权重: {selected_model_config.get('weight', 1.0)})"
|
||||
)
|
||||
|
||||
express_model = LLMRequest(
|
||||
@@ -263,7 +217,9 @@ class DefaultReplyer:
|
||||
request_type=self.request_type,
|
||||
)
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n")
|
||||
|
||||
content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt)
|
||||
|
||||
logger.info(f"最终回复: {content}")
|
||||
@@ -280,20 +236,27 @@ class DefaultReplyer:
|
||||
traceback.print_exc()
|
||||
return False, None
|
||||
|
||||
async def rewrite_reply_with_context(self, reply_data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
||||
async def rewrite_reply_with_context(
|
||||
self,
|
||||
reply_data: Dict[str, Any],
|
||||
raw_reply: str = "",
|
||||
reason: str = "",
|
||||
reply_to: str = "",
|
||||
relation_info: str = "",
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
表达器 (Expressor): 核心逻辑,负责生成回复文本。
|
||||
"""
|
||||
try:
|
||||
reply_to = reply_data.get("reply_to", "")
|
||||
raw_reply = reply_data.get("raw_reply", "")
|
||||
reason = reply_data.get("reason", "")
|
||||
if not reply_data:
|
||||
reply_data = {
|
||||
"reply_to": reply_to,
|
||||
"relation_info": relation_info,
|
||||
}
|
||||
|
||||
with Timer("构建Prompt", {}): # 内部计时器,可选保留
|
||||
prompt = await self.build_prompt_rewrite_context(
|
||||
raw_reply=raw_reply,
|
||||
reason=reason,
|
||||
reply_to=reply_to,
|
||||
reply_data=reply_data,
|
||||
)
|
||||
|
||||
content = None
|
||||
@@ -318,8 +281,7 @@ class DefaultReplyer:
|
||||
|
||||
content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt)
|
||||
|
||||
logger.info(f"想要表达:{raw_reply}||理由:{reason}")
|
||||
logger.info(f"最终回复: {content}\n")
|
||||
logger.info(f"想要表达:{raw_reply}||理由:{reason}||生成回复: {content}\n")
|
||||
|
||||
except Exception as llm_e:
|
||||
# 精简报错信息
|
||||
@@ -334,6 +296,9 @@ class DefaultReplyer:
|
||||
return False, None
|
||||
|
||||
async def build_relation_info(self, reply_data=None, chat_history=None):
|
||||
if not global_config.relationship.enable_relationship:
|
||||
return ""
|
||||
|
||||
relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_stream.stream_id)
|
||||
if not reply_data:
|
||||
return ""
|
||||
@@ -347,12 +312,15 @@ class DefaultReplyer:
|
||||
person_id = person_info_manager.get_person_id_by_person_name(sender)
|
||||
if not person_id:
|
||||
logger.warning(f"{self.log_prefix} 未找到用户 {sender} 的ID,跳过信息提取")
|
||||
return None
|
||||
return f"你完全不认识{sender},不理解ta的相关信息。"
|
||||
|
||||
relation_info = await relationship_fetcher.build_relation_info(person_id, text, chat_history)
|
||||
return relation_info
|
||||
|
||||
async def build_expression_habits(self, chat_history, target):
|
||||
if not global_config.expression.enable_expression:
|
||||
return ""
|
||||
|
||||
style_habbits = []
|
||||
grammar_habbits = []
|
||||
|
||||
@@ -388,8 +356,11 @@ class DefaultReplyer:
|
||||
return expression_habits_block
|
||||
|
||||
async def build_memory_block(self, chat_history, target):
|
||||
if not global_config.memory.enable_memory:
|
||||
return ""
|
||||
|
||||
running_memorys = await self.memory_activator.activate_memory_with_chat_history(
|
||||
chat_id=self.chat_stream.stream_id, target_message=target, chat_history_prompt=chat_history
|
||||
target_message=target, chat_history_prompt=chat_history
|
||||
)
|
||||
|
||||
if running_memorys:
|
||||
@@ -397,13 +368,12 @@ class DefaultReplyer:
|
||||
for running_memory in running_memorys:
|
||||
memory_str += f"- {running_memory['content']}\n"
|
||||
memory_block = memory_str
|
||||
logger.info(f"{self.log_prefix} 添加了 {len(running_memorys)} 个激活的记忆到prompt")
|
||||
else:
|
||||
memory_block = ""
|
||||
|
||||
return memory_block
|
||||
|
||||
async def build_tool_info(self, reply_data=None, chat_history=None):
|
||||
async def build_tool_info(self, reply_data=None, chat_history=None, enable_tool: bool = True):
|
||||
"""构建工具信息块
|
||||
|
||||
Args:
|
||||
@@ -413,6 +383,10 @@ class DefaultReplyer:
|
||||
Returns:
|
||||
str: 工具信息字符串
|
||||
"""
|
||||
|
||||
if not enable_tool:
|
||||
return ""
|
||||
|
||||
if not reply_data:
|
||||
return ""
|
||||
|
||||
@@ -489,7 +463,21 @@ class DefaultReplyer:
|
||||
|
||||
return keywords_reaction_prompt
|
||||
|
||||
async def build_prompt_reply_context(self, reply_data=None, available_actions: List[str] = None) -> str:
|
||||
async def _time_and_run_task(self, coro, name: str):
|
||||
"""一个简单的帮助函数,用于计时和运行异步任务,返回任务名、结果和耗时"""
|
||||
start_time = time.time()
|
||||
result = await coro
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
return name, result, duration
|
||||
|
||||
async def build_prompt_reply_context(
|
||||
self,
|
||||
reply_data=None,
|
||||
available_actions: List[str] = None,
|
||||
enable_timeout: bool = False,
|
||||
enable_tool: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
构建回复器上下文
|
||||
|
||||
@@ -555,13 +543,34 @@ class DefaultReplyer:
|
||||
)
|
||||
|
||||
# 并行执行四个构建任务
|
||||
expression_habits_block, relation_info, memory_block, tool_info = await asyncio.gather(
|
||||
self.build_expression_habits(chat_talking_prompt_half, target),
|
||||
self.build_relation_info(reply_data, chat_talking_prompt_half),
|
||||
self.build_memory_block(chat_talking_prompt_half, target),
|
||||
self.build_tool_info(reply_data, chat_talking_prompt_half),
|
||||
task_results = await asyncio.gather(
|
||||
self._time_and_run_task(
|
||||
self.build_expression_habits(chat_talking_prompt_half, target), "build_expression_habits"
|
||||
),
|
||||
self._time_and_run_task(
|
||||
self.build_relation_info(reply_data, chat_talking_prompt_half), "build_relation_info"
|
||||
),
|
||||
self._time_and_run_task(self.build_memory_block(chat_talking_prompt_half, target), "build_memory_block"),
|
||||
self._time_and_run_task(
|
||||
self.build_tool_info(reply_data, chat_talking_prompt_half, enable_tool=enable_tool), "build_tool_info"
|
||||
),
|
||||
)
|
||||
|
||||
# 处理结果
|
||||
timing_logs = []
|
||||
results_dict = {}
|
||||
for name, result, duration in task_results:
|
||||
results_dict[name] = result
|
||||
timing_logs.append(f"{name}: {duration:.4f}s")
|
||||
if duration > 8:
|
||||
logger.warning(f"回复生成前信息获取耗时过长: {name} 耗时: {duration:.4f}s,请使用更快的模型")
|
||||
logger.info(f"回复生成前信息获取耗时: {'; '.join(timing_logs)}")
|
||||
|
||||
expression_habits_block = results_dict["build_expression_habits"]
|
||||
relation_info = results_dict["build_relation_info"]
|
||||
memory_block = results_dict["build_memory_block"]
|
||||
tool_info = results_dict["build_tool_info"]
|
||||
|
||||
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
|
||||
|
||||
if tool_info:
|
||||
@@ -608,9 +617,12 @@ class DefaultReplyer:
|
||||
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
|
||||
)
|
||||
|
||||
if sender and target:
|
||||
if is_group_chat:
|
||||
if sender:
|
||||
reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。"
|
||||
reply_target_block = (
|
||||
f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。"
|
||||
)
|
||||
elif target:
|
||||
reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。"
|
||||
else:
|
||||
@@ -622,6 +634,8 @@ class DefaultReplyer:
|
||||
reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。"
|
||||
else:
|
||||
reply_target_block = "现在,你想要回复。"
|
||||
else:
|
||||
reply_target_block = ""
|
||||
|
||||
mood_prompt = mood_manager.get_mood_prompt()
|
||||
|
||||
@@ -629,12 +643,22 @@ class DefaultReplyer:
|
||||
if prompt_info:
|
||||
prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info)
|
||||
|
||||
# --- Choose template based on chat type ---
|
||||
if is_group_chat:
|
||||
template_name = "default_generator_prompt"
|
||||
# Group specific formatting variables (already fetched or default)
|
||||
if is_group_chat:
|
||||
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")
|
||||
else:
|
||||
chat_target_name = "对方"
|
||||
if self.chat_target_info:
|
||||
chat_target_name = (
|
||||
self.chat_target_info.get("person_name") or self.chat_target_info.get("user_nickname") or "对方"
|
||||
)
|
||||
chat_target_1 = await global_prompt_manager.format_prompt(
|
||||
"chat_target_private1", sender_name=chat_target_name
|
||||
)
|
||||
chat_target_2 = await global_prompt_manager.format_prompt(
|
||||
"chat_target_private2", sender_name=chat_target_name
|
||||
)
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
@@ -643,6 +667,7 @@ class DefaultReplyer:
|
||||
chat_info=chat_talking_prompt,
|
||||
memory_block=memory_block,
|
||||
tool_info_block=tool_info_block,
|
||||
knowledge_prompt=prompt_info,
|
||||
extra_info_block=extra_info_block,
|
||||
relation_info_block=relation_info,
|
||||
time_block=time_block,
|
||||
@@ -657,144 +682,131 @@ class DefaultReplyer:
|
||||
chat_target_2=chat_target_2,
|
||||
mood_prompt=mood_prompt,
|
||||
)
|
||||
else: # Private chat
|
||||
template_name = "default_generator_private_prompt"
|
||||
# 在私聊时获取对方的昵称信息
|
||||
chat_target_name = "对方"
|
||||
if self.chat_target_info:
|
||||
chat_target_name = (
|
||||
self.chat_target_info.get("person_name") or self.chat_target_info.get("user_nickname") or "对方"
|
||||
)
|
||||
chat_target_1 = f"你正在和 {chat_target_name} 聊天"
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
expression_habits_block=expression_habits_block,
|
||||
chat_target=chat_target_1,
|
||||
chat_info=chat_talking_prompt,
|
||||
memory_block=memory_block,
|
||||
tool_info_block=tool_info_block,
|
||||
relation_info_block=relation_info,
|
||||
extra_info_block=extra_info_block,
|
||||
time_block=time_block,
|
||||
keywords_reaction_prompt=keywords_reaction_prompt,
|
||||
identity=indentify_block,
|
||||
target_message=target,
|
||||
sender_name=sender,
|
||||
config_expression_style=global_config.expression.expression_style,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
async def build_prompt_rewrite_context(
|
||||
self,
|
||||
reason,
|
||||
raw_reply,
|
||||
reply_to,
|
||||
reply_data: Dict[str, Any],
|
||||
) -> str:
|
||||
sender = ""
|
||||
target = ""
|
||||
if ":" in reply_to or ":" in reply_to:
|
||||
# 使用正则表达式匹配中文或英文冒号
|
||||
parts = re.split(pattern=r"[::]", string=reply_to, maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
sender = parts[0].strip()
|
||||
target = parts[1].strip()
|
||||
|
||||
chat_stream = self.chat_stream
|
||||
|
||||
chat_id = chat_stream.stream_id
|
||||
person_info_manager = get_person_info_manager()
|
||||
bot_person_id = person_info_manager.get_person_id("system", "bot_id")
|
||||
is_group_chat = bool(chat_stream.group_info)
|
||||
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
reply_to = reply_data.get("reply_to", "none")
|
||||
raw_reply = reply_data.get("raw_reply", "")
|
||||
reason = reply_data.get("reason", "")
|
||||
sender, target = self._parse_reply_target(reply_to)
|
||||
|
||||
message_list_before_now_half = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_id,
|
||||
timestamp=time.time(),
|
||||
limit=global_config.chat.max_context_size,
|
||||
limit=int(global_config.chat.max_context_size * 0.5),
|
||||
)
|
||||
chat_talking_prompt = build_readable_messages(
|
||||
message_list_before_now,
|
||||
chat_talking_prompt_half = build_readable_messages(
|
||||
message_list_before_now_half,
|
||||
replace_bot_name=True,
|
||||
merge_messages=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
truncate=True,
|
||||
show_actions=True,
|
||||
)
|
||||
|
||||
expression_learner = get_expression_learner()
|
||||
(
|
||||
learnt_style_expressions,
|
||||
learnt_grammar_expressions,
|
||||
personality_expressions,
|
||||
) = expression_learner.get_expression_by_chat_id(chat_stream.stream_id)
|
||||
# 并行执行2个构建任务
|
||||
expression_habits_block, relation_info = await asyncio.gather(
|
||||
self.build_expression_habits(chat_talking_prompt_half, target),
|
||||
self.build_relation_info(reply_data, chat_talking_prompt_half),
|
||||
)
|
||||
|
||||
style_habbits = []
|
||||
grammar_habbits = []
|
||||
# 1. learnt_expressions加权随机选3条
|
||||
if learnt_style_expressions:
|
||||
weights = [expr["count"] for expr in learnt_style_expressions]
|
||||
selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 3)
|
||||
for expr in selected_learnt:
|
||||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||||
style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||||
# 2. learnt_grammar_expressions加权随机选3条
|
||||
if learnt_grammar_expressions:
|
||||
weights = [expr["count"] for expr in learnt_grammar_expressions]
|
||||
selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 3)
|
||||
for expr in selected_learnt:
|
||||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||||
grammar_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||||
# 3. personality_expressions随机选1条
|
||||
if personality_expressions:
|
||||
expr = random.choice(personality_expressions)
|
||||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||||
style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||||
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
|
||||
|
||||
style_habbits_str = "\n".join(style_habbits)
|
||||
grammar_habbits_str = "\n".join(grammar_habbits)
|
||||
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
logger.debug("开始构建 focus prompt")
|
||||
bot_name = global_config.bot.nickname
|
||||
if global_config.bot.alias_names:
|
||||
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
|
||||
else:
|
||||
bot_nickname = ""
|
||||
short_impression = await person_info_manager.get_value(bot_person_id, "short_impression")
|
||||
try:
|
||||
if isinstance(short_impression, str) and short_impression.strip():
|
||||
short_impression = ast.literal_eval(short_impression)
|
||||
elif not short_impression:
|
||||
logger.warning("short_impression为空,使用默认值")
|
||||
short_impression = ["友好活泼", "人类"]
|
||||
except (ValueError, SyntaxError) as e:
|
||||
logger.error(f"解析short_impression失败: {e}, 原始值: {short_impression}")
|
||||
short_impression = ["友好活泼", "人类"]
|
||||
# 确保short_impression是列表格式且有足够的元素
|
||||
if not isinstance(short_impression, list) or len(short_impression) < 2:
|
||||
logger.warning(f"short_impression格式不正确: {short_impression}, 使用默认值")
|
||||
short_impression = ["友好活泼", "人类"]
|
||||
personality = short_impression[0]
|
||||
identity = short_impression[1]
|
||||
prompt_personality = personality + "," + identity
|
||||
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}:"
|
||||
|
||||
# --- Choose template based on chat type ---
|
||||
moderation_prompt_block = (
|
||||
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
|
||||
)
|
||||
|
||||
if sender and target:
|
||||
if is_group_chat:
|
||||
template_name = "default_expressor_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,
|
||||
style_habbits=style_habbits_str,
|
||||
grammar_habbits=grammar_habbits_str,
|
||||
chat_target=chat_target_1,
|
||||
chat_info=chat_talking_prompt,
|
||||
bot_name=global_config.bot.nickname,
|
||||
prompt_personality="",
|
||||
reason=reason,
|
||||
raw_reply=raw_reply,
|
||||
sender_name=sender,
|
||||
target_message=target,
|
||||
config_expression_style=global_config.expression.expression_style,
|
||||
if sender:
|
||||
reply_target_block = (
|
||||
f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。"
|
||||
)
|
||||
else: # Private chat
|
||||
template_name = "default_expressor_private_prompt"
|
||||
# 在私聊时获取对方的昵称信息
|
||||
elif target:
|
||||
reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。"
|
||||
else:
|
||||
reply_target_block = "现在,你想要在群里发言或者回复消息。"
|
||||
else: # private chat
|
||||
if sender:
|
||||
reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,针对这条消息回复。"
|
||||
elif target:
|
||||
reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。"
|
||||
else:
|
||||
reply_target_block = "现在,你想要回复。"
|
||||
else:
|
||||
reply_target_block = ""
|
||||
|
||||
mood_manager.get_mood_prompt()
|
||||
|
||||
if is_group_chat:
|
||||
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")
|
||||
else:
|
||||
chat_target_name = "对方"
|
||||
if self.chat_target_info:
|
||||
chat_target_name = (
|
||||
self.chat_target_info.get("person_name") or self.chat_target_info.get("user_nickname") or "对方"
|
||||
)
|
||||
chat_target_1 = f"你正在和 {chat_target_name} 聊天"
|
||||
chat_target_1 = await global_prompt_manager.format_prompt(
|
||||
"chat_target_private1", sender_name=chat_target_name
|
||||
)
|
||||
chat_target_2 = await global_prompt_manager.format_prompt(
|
||||
"chat_target_private2", sender_name=chat_target_name
|
||||
)
|
||||
|
||||
template_name = "default_expressor_prompt"
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
style_habbits=style_habbits_str,
|
||||
grammar_habbits=grammar_habbits_str,
|
||||
expression_habits_block=expression_habits_block,
|
||||
relation_info_block=relation_info,
|
||||
chat_target=chat_target_1,
|
||||
chat_info=chat_talking_prompt,
|
||||
bot_name=global_config.bot.nickname,
|
||||
prompt_personality="",
|
||||
reason=reason,
|
||||
time_block=time_block,
|
||||
chat_info=chat_talking_prompt_half,
|
||||
identity=indentify_block,
|
||||
chat_target_2=chat_target_2,
|
||||
reply_target_block=reply_target_block,
|
||||
raw_reply=raw_reply,
|
||||
sender_name=sender,
|
||||
target_message=target,
|
||||
reason=reason,
|
||||
config_expression_style=global_config.expression.expression_style,
|
||||
keywords_reaction_prompt=keywords_reaction_prompt,
|
||||
moderation_prompt=moderation_prompt_block,
|
||||
)
|
||||
|
||||
return prompt
|
||||
@@ -842,7 +854,7 @@ class DefaultReplyer:
|
||||
type = msg_text[0]
|
||||
data = msg_text[1]
|
||||
|
||||
if global_config.experimental.debug_show_chat_mode and type == "text":
|
||||
if global_config.debug.debug_show_chat_mode and type == "text":
|
||||
data += "ᶠ"
|
||||
|
||||
part_message_id = f"{thinking_id}_{i}"
|
||||
@@ -982,6 +994,11 @@ async def get_prompt_info(message: str, threshold: float):
|
||||
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
|
||||
# 从LPMM知识库获取知识
|
||||
try:
|
||||
# 检查LPMM知识库是否启用
|
||||
if qa_manager is None:
|
||||
logger.debug("LPMM知识库已禁用,跳过知识获取")
|
||||
return ""
|
||||
|
||||
found_knowledge_from_lpmm = qa_manager.get_knowledge(message)
|
||||
|
||||
end_time = time.time()
|
||||
|
||||
@@ -14,7 +14,6 @@ class ReplyerManager:
|
||||
self,
|
||||
chat_stream: Optional[ChatStream] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
enable_tool: bool = False,
|
||||
model_configs: Optional[List[Dict[str, Any]]] = None,
|
||||
request_type: str = "replyer",
|
||||
) -> Optional[DefaultReplyer]:
|
||||
@@ -50,7 +49,6 @@ class ReplyerManager:
|
||||
# model_configs 只在此时(初始化时)生效
|
||||
replyer = DefaultReplyer(
|
||||
chat_stream=target_stream,
|
||||
enable_tool=enable_tool,
|
||||
model_configs=model_configs, # 可以是None,此时使用默认模型
|
||||
request_type=request_type,
|
||||
)
|
||||
|
||||
@@ -174,6 +174,7 @@ def _build_readable_messages_internal(
|
||||
truncate: bool = False,
|
||||
pic_id_mapping: Dict[str, str] = None,
|
||||
pic_counter: int = 1,
|
||||
show_pic: bool = True,
|
||||
) -> Tuple[str, List[Tuple[float, str, str]], Dict[str, str], int]:
|
||||
"""
|
||||
内部辅助函数,构建可读消息字符串和原始消息详情列表。
|
||||
@@ -260,6 +261,7 @@ def _build_readable_messages_internal(
|
||||
content = content.replace("ⁿ", "")
|
||||
|
||||
# 处理图片ID
|
||||
if show_pic:
|
||||
content = process_pic_ids(content)
|
||||
|
||||
# 检查必要信息是否存在
|
||||
@@ -532,6 +534,7 @@ def build_readable_messages(
|
||||
read_mark: float = 0.0,
|
||||
truncate: bool = False,
|
||||
show_actions: bool = False,
|
||||
show_pic: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
将消息列表转换为可读的文本格式。
|
||||
@@ -601,7 +604,7 @@ def build_readable_messages(
|
||||
if read_mark <= 0:
|
||||
# 没有有效的 read_mark,直接格式化所有消息
|
||||
formatted_string, _, pic_id_mapping, _ = _build_readable_messages_internal(
|
||||
copy_messages, replace_bot_name, merge_messages, timestamp_mode, truncate
|
||||
copy_messages, replace_bot_name, merge_messages, timestamp_mode, truncate, show_pic=show_pic
|
||||
)
|
||||
|
||||
# 生成图片映射信息并添加到最前面
|
||||
@@ -628,9 +631,17 @@ def build_readable_messages(
|
||||
truncate,
|
||||
pic_id_mapping,
|
||||
pic_counter,
|
||||
show_pic=show_pic,
|
||||
)
|
||||
formatted_after, _, pic_id_mapping, _ = _build_readable_messages_internal(
|
||||
messages_after_mark, replace_bot_name, merge_messages, timestamp_mode, False, pic_id_mapping, pic_counter
|
||||
messages_after_mark,
|
||||
replace_bot_name,
|
||||
merge_messages,
|
||||
timestamp_mode,
|
||||
False,
|
||||
pic_id_mapping,
|
||||
pic_counter,
|
||||
show_pic=show_pic,
|
||||
)
|
||||
|
||||
read_mark_line = "\n--- 以上消息是你已经看过,请关注以下未读的新消息---\n"
|
||||
|
||||
@@ -1243,7 +1243,7 @@ class StatisticOutputTask(AsyncTask):
|
||||
focus_chat_rows = ""
|
||||
if stat_data[FOCUS_AVG_TIMES_BY_CHAT_ACTION]:
|
||||
# 获取前三个阶段(不包括执行动作)
|
||||
basic_stages = ["观察", "并行调整动作、处理", "规划器"]
|
||||
basic_stages = ["观察", "规划器"]
|
||||
existing_basic_stages = []
|
||||
for stage in basic_stages:
|
||||
# 检查是否有任何聊天流在这个阶段有数据
|
||||
@@ -1352,7 +1352,7 @@ class StatisticOutputTask(AsyncTask):
|
||||
focus_action_stage_rows = ""
|
||||
if stat_data[FOCUS_AVG_TIMES_BY_ACTION]:
|
||||
# 获取所有阶段(按固定顺序)
|
||||
stage_order = ["观察", "并行调整动作、处理", "规划器", "执行动作"]
|
||||
stage_order = ["观察", "规划器", "执行动作"]
|
||||
all_stages = []
|
||||
for stage in stage_order:
|
||||
if any(stage in stage_times for stage_times in stat_data[FOCUS_AVG_TIMES_BY_ACTION].values()):
|
||||
@@ -1618,7 +1618,7 @@ class StatisticOutputTask(AsyncTask):
|
||||
focus_version_stage_rows = ""
|
||||
if stat_data[FOCUS_AVG_TIMES_BY_VERSION]:
|
||||
# 基础三个阶段
|
||||
basic_stages = ["观察", "并行调整动作、处理", "规划器"]
|
||||
basic_stages = ["观察", "规划器"]
|
||||
|
||||
# 获取所有action类型用于执行时间列
|
||||
all_action_types_for_exec = set()
|
||||
|
||||
@@ -14,6 +14,9 @@ from src.llm_models.utils_model import LLMRequest
|
||||
from .typo_generator import ChineseTypoGenerator
|
||||
from ...config.config import global_config
|
||||
from ...common.message_repository import find_messages, count_messages
|
||||
from typing import Optional, Tuple, Dict
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
|
||||
|
||||
logger = get_logger("chat_utils")
|
||||
|
||||
@@ -47,7 +50,8 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
|
||||
reply_probability = 0.0
|
||||
is_at = False
|
||||
is_mentioned = False
|
||||
|
||||
if message.is_mentioned is not None:
|
||||
return bool(message.is_mentioned), message.is_mentioned
|
||||
if (
|
||||
message.message_info.additional_config is not None
|
||||
and message.message_info.additional_config.get("is_mentioned") is not None
|
||||
@@ -80,7 +84,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
|
||||
|
||||
if is_at and global_config.normal_chat.at_bot_inevitable_reply:
|
||||
reply_probability = 1.0
|
||||
logger.info("被@,回复概率设置为100%")
|
||||
logger.debug("被@,回复概率设置为100%")
|
||||
else:
|
||||
if not is_mentioned:
|
||||
# 判断是否被回复
|
||||
@@ -105,7 +109,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
|
||||
is_mentioned = True
|
||||
if is_mentioned and global_config.normal_chat.mentioned_bot_inevitable_reply:
|
||||
reply_probability = 1.0
|
||||
logger.info("被提及,回复概率设置为100%")
|
||||
logger.debug("被提及,回复概率设置为100%")
|
||||
return is_mentioned, reply_probability
|
||||
|
||||
|
||||
@@ -637,3 +641,70 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
|
||||
else: # mode = "lite" or unknown
|
||||
# 只返回时分秒格式,喵~
|
||||
return time.strftime("%H:%M:%S", time.localtime(timestamp))
|
||||
|
||||
|
||||
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 = get_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 = PersonInfoManager.get_person_id(platform, user_id)
|
||||
person_name = None
|
||||
if person_id:
|
||||
# get_value is async, so await it directly
|
||||
person_info_manager = get_person_info_manager()
|
||||
person_name = person_info_manager.get_value_sync(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
|
||||
|
||||
@@ -178,12 +178,24 @@ class ImageManager:
|
||||
"""获取普通图片描述,带查重和保存功能"""
|
||||
try:
|
||||
# 计算图片哈希
|
||||
# 确保base64字符串只包含ASCII字符
|
||||
if isinstance(image_base64, str):
|
||||
image_base64 = image_base64.encode("ascii", errors="ignore").decode("ascii")
|
||||
image_bytes = base64.b64decode(image_base64)
|
||||
image_hash = hashlib.md5(image_bytes).hexdigest()
|
||||
image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
|
||||
|
||||
# 检查图片是否已存在
|
||||
existing_image = Images.get_or_none(Images.emoji_hash == image_hash)
|
||||
if existing_image:
|
||||
# 更新计数
|
||||
if hasattr(existing_image, "count") and existing_image.count is not None:
|
||||
existing_image.count += 1
|
||||
else:
|
||||
existing_image.count = 1
|
||||
existing_image.save()
|
||||
|
||||
# 如果已有描述,直接返回
|
||||
if existing_image.description:
|
||||
return f"[图片:{existing_image.description}]"
|
||||
|
||||
# 查询缓存的描述
|
||||
cached_description = self._get_description_from_db(image_hash, "image")
|
||||
@@ -192,6 +204,7 @@ class ImageManager:
|
||||
return f"[图片:{cached_description}]"
|
||||
|
||||
# 调用AI获取描述
|
||||
image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
|
||||
prompt = "请用中文描述这张图片的内容。如果有文字,请把文字都描述出来,请留意其主题,直观感受,输出为一段平文本,最多50字"
|
||||
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format)
|
||||
|
||||
@@ -199,17 +212,7 @@ class ImageManager:
|
||||
logger.warning("AI未能生成图片描述")
|
||||
return "[图片(描述生成失败)]"
|
||||
|
||||
# 再次检查缓存
|
||||
cached_description = self._get_description_from_db(image_hash, "image")
|
||||
if cached_description:
|
||||
logger.warning(f"虽然生成了描述,但是找到缓存图片描述 {cached_description}")
|
||||
return f"[图片:{cached_description}]"
|
||||
|
||||
logger.debug(f"描述是{description}")
|
||||
|
||||
# 根据配置决定是否保存图片
|
||||
|
||||
# 生成文件名和路径
|
||||
# 保存图片和描述
|
||||
current_timestamp = time.time()
|
||||
filename = f"{int(current_timestamp)}_{image_hash[:8]}.{image_format}"
|
||||
image_dir = os.path.join(self.IMAGE_DIR, "image")
|
||||
@@ -221,26 +224,31 @@ class ImageManager:
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
# 保存到数据库 (Images表)
|
||||
try:
|
||||
img_obj = Images.get((Images.emoji_hash == image_hash) & (Images.type == "image"))
|
||||
img_obj.path = file_path
|
||||
img_obj.description = description
|
||||
img_obj.timestamp = current_timestamp
|
||||
img_obj.save()
|
||||
except Images.DoesNotExist:
|
||||
# 保存到数据库,补充缺失字段
|
||||
if existing_image:
|
||||
existing_image.path = file_path
|
||||
existing_image.description = description
|
||||
existing_image.timestamp = current_timestamp
|
||||
if not hasattr(existing_image, "image_id") or not existing_image.image_id:
|
||||
existing_image.image_id = str(uuid.uuid4())
|
||||
if not hasattr(existing_image, "vlm_processed") or existing_image.vlm_processed is None:
|
||||
existing_image.vlm_processed = True
|
||||
existing_image.save()
|
||||
else:
|
||||
Images.create(
|
||||
image_id=str(uuid.uuid4()),
|
||||
emoji_hash=image_hash,
|
||||
path=file_path,
|
||||
type="image",
|
||||
description=description,
|
||||
timestamp=current_timestamp,
|
||||
vlm_processed=True,
|
||||
count=1,
|
||||
)
|
||||
logger.debug(f"保存图片元数据: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存图片文件或元数据失败: {str(e)}")
|
||||
|
||||
# 保存描述到数据库 (ImageDescriptions表)
|
||||
# 保存描述到ImageDescriptions表
|
||||
self._save_description_to_db(image_hash, description, "image")
|
||||
|
||||
return f"[图片:{description}]"
|
||||
@@ -403,7 +411,16 @@ class ImageManager:
|
||||
or existing_image.vlm_processed is None
|
||||
):
|
||||
logger.debug(f"图片记录缺少必要字段,补全旧记录: {image_hash}")
|
||||
image_id = str(uuid.uuid4())
|
||||
if not existing_image.image_id:
|
||||
existing_image.image_id = str(uuid.uuid4())
|
||||
if existing_image.count is None:
|
||||
existing_image.count = 0
|
||||
if existing_image.vlm_processed is None:
|
||||
existing_image.vlm_processed = False
|
||||
|
||||
existing_image.count += 1
|
||||
existing_image.save()
|
||||
return existing_image.image_id, f"[picid:{existing_image.image_id}]"
|
||||
else:
|
||||
# print(f"图片已存在: {existing_image.image_id}")
|
||||
# print(f"图片描述: {existing_image.description}")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
|
||||
from src.chat.heart_flow.observation.observation import Observation
|
||||
from src.chat.focus_chat.observation.chatting_observation import ChattingObservation
|
||||
from src.chat.focus_chat.observation.observation import Observation
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
import time
|
||||
@@ -7,9 +7,8 @@ import traceback
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from .base_processor import BaseProcessor
|
||||
from typing import List
|
||||
from src.chat.heart_flow.observation.working_observation import WorkingMemoryObservation
|
||||
from src.chat.focus_chat.observation.working_observation import WorkingMemoryObservation
|
||||
from src.chat.focus_chat.working_memory.working_memory import WorkingMemory
|
||||
from src.chat.focus_chat.info.info_base import InfoBase
|
||||
from json_repair import repair_json
|
||||
@@ -44,12 +43,10 @@ def init_prompt():
|
||||
Prompt(memory_proces_prompt, "prompt_memory_proces")
|
||||
|
||||
|
||||
class WorkingMemoryProcessor(BaseProcessor):
|
||||
class WorkingMemoryProcessor:
|
||||
log_prefix = "工作记忆"
|
||||
|
||||
def __init__(self, subheartflow_id: str):
|
||||
super().__init__()
|
||||
|
||||
self.subheartflow_id = subheartflow_id
|
||||
|
||||
self.llm_model = LLMRequest(
|
||||
@@ -71,6 +68,7 @@ class WorkingMemoryProcessor(BaseProcessor):
|
||||
"""
|
||||
working_memory = None
|
||||
chat_info = ""
|
||||
chat_obs = None
|
||||
try:
|
||||
for observation in observations:
|
||||
if isinstance(observation, WorkingMemoryObservation):
|
||||
@@ -79,10 +77,15 @@ class WorkingMemoryProcessor(BaseProcessor):
|
||||
chat_info = observation.get_observe_info()
|
||||
chat_obs = observation
|
||||
# 检查是否有待压缩内容
|
||||
if chat_obs.compressor_prompt:
|
||||
if chat_obs and chat_obs.compressor_prompt:
|
||||
logger.debug(f"{self.log_prefix} 压缩聊天记忆")
|
||||
await self.compress_chat_memory(working_memory, chat_obs)
|
||||
|
||||
# 检查working_memory是否为None
|
||||
if working_memory is None:
|
||||
logger.debug(f"{self.log_prefix} 没有找到工作记忆观察,跳过处理")
|
||||
return []
|
||||
|
||||
all_memory = working_memory.get_all_memories()
|
||||
if not all_memory:
|
||||
logger.debug(f"{self.log_prefix} 目前没有工作记忆,跳过提取")
|
||||
@@ -183,6 +186,11 @@ class WorkingMemoryProcessor(BaseProcessor):
|
||||
working_memory: 工作记忆对象
|
||||
obs: 聊天观察对象
|
||||
"""
|
||||
# 检查working_memory是否为None
|
||||
if working_memory is None:
|
||||
logger.warning(f"{self.log_prefix} 工作记忆对象为None,无法压缩聊天记忆")
|
||||
return
|
||||
|
||||
try:
|
||||
summary_result, _ = await self.llm_model.generate_response_async(obs.compressor_prompt)
|
||||
if not summary_result:
|
||||
@@ -235,6 +243,11 @@ class WorkingMemoryProcessor(BaseProcessor):
|
||||
memory_id1: 第一个记忆ID
|
||||
memory_id2: 第二个记忆ID
|
||||
"""
|
||||
# 检查working_memory是否为None
|
||||
if working_memory is None:
|
||||
logger.warning(f"{self.log_prefix} 工作记忆对象为None,无法合并记忆")
|
||||
return
|
||||
|
||||
try:
|
||||
merged_memory = await working_memory.merge_memory(memory_id1, memory_id2)
|
||||
logger.debug(f"{self.log_prefix} 合并后的记忆梗概: {merged_memory.brief}")
|
||||
@@ -127,6 +127,8 @@ class Messages(BaseModel):
|
||||
|
||||
chat_id = TextField(index=True) # 对应的 ChatStreams stream_id
|
||||
|
||||
reply_to = TextField(null=True)
|
||||
|
||||
# 从 chat_info 扁平化而来的字段
|
||||
chat_info_stream_id = TextField()
|
||||
chat_info_platform = TextField()
|
||||
|
||||
@@ -340,20 +340,18 @@ MODULE_COLORS = {
|
||||
"memory": "\033[34m",
|
||||
"hfc": "\033[96m",
|
||||
"base_action": "\033[96m",
|
||||
"action_manager": "\033[34m",
|
||||
"action_manager": "\033[32m",
|
||||
# 关系系统
|
||||
"relation": "\033[38;5;201m", # 深粉色
|
||||
# 聊天相关模块
|
||||
"normal_chat": "\033[38;5;81m", # 亮蓝绿色
|
||||
"normal_chat_response": "\033[38;5;123m", # 青绿色
|
||||
"normal_chat_expressor": "\033[38;5;117m", # 浅蓝色
|
||||
"normal_chat_action_modifier": "\033[38;5;111m", # 蓝色
|
||||
"normal_chat_planner": "\033[38;5;75m", # 浅蓝色
|
||||
"heartflow": "\033[38;5;213m", # 粉色
|
||||
"heartflow_utils": "\033[38;5;219m", # 浅粉色
|
||||
"sub_heartflow": "\033[38;5;207m", # 粉紫色
|
||||
"subheartflow_manager": "\033[38;5;201m", # 深粉色
|
||||
"observation": "\033[38;5;141m", # 紫色
|
||||
"background_tasks": "\033[38;5;240m", # 灰色
|
||||
"chat_message": "\033[38;5;45m", # 青色
|
||||
"chat_stream": "\033[38;5;51m", # 亮青色
|
||||
@@ -362,7 +360,6 @@ MODULE_COLORS = {
|
||||
# 专注聊天模块
|
||||
"replyer": "\033[38;5;166m", # 橙色
|
||||
"expressor": "\033[38;5;172m", # 黄橙色
|
||||
"planner_factory": "\033[38;5;178m", # 黄色
|
||||
"processor": "\033[38;5;184m", # 黄绿色
|
||||
"base_processor": "\033[38;5;190m", # 绿黄色
|
||||
"working_memory": "\033[38;5;22m", # 深绿色
|
||||
@@ -370,6 +367,7 @@ MODULE_COLORS = {
|
||||
# 插件系统
|
||||
"plugin_manager": "\033[38;5;208m", # 红色
|
||||
"base_plugin": "\033[38;5;202m", # 橙红色
|
||||
"send_api": "\033[38;5;208m", # 橙色
|
||||
"base_command": "\033[38;5;208m", # 橙色
|
||||
"component_registry": "\033[38;5;214m", # 橙黄色
|
||||
"stream_api": "\033[38;5;220m", # 黄色
|
||||
@@ -388,10 +386,8 @@ MODULE_COLORS = {
|
||||
"willing": "\033[38;5;147m", # 浅紫色
|
||||
# 工具模块
|
||||
"tool_use": "\033[38;5;64m", # 深绿色
|
||||
"tool_executor": "\033[38;5;64m", # 深绿色
|
||||
"base_tool": "\033[38;5;70m", # 绿色
|
||||
"compare_numbers_tool": "\033[38;5;76m", # 浅绿色
|
||||
"change_mood_tool": "\033[38;5;82m", # 绿色
|
||||
"relationship_tool": "\033[38;5;88m", # 深红色
|
||||
# 工具和实用模块
|
||||
"prompt": "\033[38;5;99m", # 紫色
|
||||
"prompt_build": "\033[38;5;105m", # 紫色
|
||||
@@ -417,6 +413,8 @@ MODULE_COLORS = {
|
||||
"confirm": "\033[1;93m", # 黄色+粗体
|
||||
# 模型相关
|
||||
"model_utils": "\033[38;5;164m", # 紫红色
|
||||
"relationship_fetcher": "\033[38;5;170m", # 浅紫色
|
||||
"relationship_builder": "\033[38;5;117m", # 浅蓝色
|
||||
}
|
||||
|
||||
RESET_COLOR = "\033[0m"
|
||||
|
||||
@@ -35,6 +35,7 @@ from src.config.official_configs import (
|
||||
LPMMKnowledgeConfig,
|
||||
RelationshipConfig,
|
||||
ToolConfig,
|
||||
DebugConfig,
|
||||
)
|
||||
|
||||
install(extra_lines=3)
|
||||
@@ -50,7 +51,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
|
||||
|
||||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
|
||||
MMC_VERSION = "0.8.1-snapshot.1"
|
||||
MMC_VERSION = "0.8.2-snapshot.1"
|
||||
|
||||
|
||||
def update_config():
|
||||
@@ -165,6 +166,7 @@ class Config(ConfigBase):
|
||||
maim_message: MaimMessageConfig
|
||||
lpmm_knowledge: LPMMKnowledgeConfig
|
||||
tool: ToolConfig
|
||||
debug: DebugConfig
|
||||
|
||||
|
||||
def load_config(config_path: str) -> Config:
|
||||
|
||||
@@ -84,6 +84,9 @@ class ChatConfig(ConfigBase):
|
||||
选择普通模型的概率为 1 - reasoning_normal_model_probability
|
||||
"""
|
||||
|
||||
thinking_timeout: int = 30
|
||||
"""麦麦最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)"""
|
||||
|
||||
talk_frequency: float = 1
|
||||
"""回复频率阈值"""
|
||||
|
||||
@@ -270,24 +273,12 @@ class MessageReceiveConfig(ConfigBase):
|
||||
class NormalChatConfig(ConfigBase):
|
||||
"""普通聊天配置类"""
|
||||
|
||||
message_buffer: bool = False
|
||||
"""消息缓冲器"""
|
||||
|
||||
emoji_chance: float = 0.2
|
||||
"""发送表情包的基础概率"""
|
||||
|
||||
thinking_timeout: int = 120
|
||||
"""最长思考时间"""
|
||||
|
||||
willing_mode: str = "classical"
|
||||
"""意愿模式"""
|
||||
|
||||
response_interested_rate_amplifier: float = 1.0
|
||||
"""回复兴趣度放大系数"""
|
||||
|
||||
emoji_response_penalty: float = 0.0
|
||||
"""表情包回复惩罚系数"""
|
||||
|
||||
mentioned_bot_inevitable_reply: bool = False
|
||||
"""提及 bot 必然回复"""
|
||||
|
||||
@@ -302,26 +293,20 @@ class NormalChatConfig(ConfigBase):
|
||||
class FocusChatConfig(ConfigBase):
|
||||
"""专注聊天配置类"""
|
||||
|
||||
compressed_length: int = 5
|
||||
"""心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5"""
|
||||
|
||||
compress_length_limit: int = 5
|
||||
"""最多压缩份数,超过该数值的压缩上下文会被删除"""
|
||||
|
||||
think_interval: float = 1
|
||||
"""思考间隔(秒)"""
|
||||
|
||||
consecutive_replies: float = 1
|
||||
"""连续回复能力,值越高,麦麦连续回复的概率越高"""
|
||||
|
||||
working_memory_processor: bool = False
|
||||
"""是否启用工作记忆处理器"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpressionConfig(ConfigBase):
|
||||
"""表达配置类"""
|
||||
|
||||
enable_expression: bool = True
|
||||
"""是否启用表达方式"""
|
||||
|
||||
expression_style: str = ""
|
||||
"""表达风格"""
|
||||
|
||||
@@ -353,6 +338,12 @@ class ToolConfig(ConfigBase):
|
||||
class EmojiConfig(ConfigBase):
|
||||
"""表情包配置类"""
|
||||
|
||||
emoji_chance: float = 0.6
|
||||
"""发送表情包的基础概率"""
|
||||
|
||||
emoji_activate_type: str = "random"
|
||||
"""表情包激活类型,可选:random,llm,random下,表情包动作随机启用,llm下,表情包动作根据llm判断是否启用"""
|
||||
|
||||
max_reg_num: int = 200
|
||||
"""表情包最大注册数量"""
|
||||
|
||||
@@ -540,12 +531,20 @@ class TelemetryConfig(ConfigBase):
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExperimentalConfig(ConfigBase):
|
||||
"""实验功能配置类"""
|
||||
class DebugConfig(ConfigBase):
|
||||
"""调试配置类"""
|
||||
|
||||
debug_show_chat_mode: bool = False
|
||||
"""是否在回复后显示当前聊天模式"""
|
||||
|
||||
show_prompt: bool = False
|
||||
"""是否显示prompt"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExperimentalConfig(ConfigBase):
|
||||
"""实验功能配置类"""
|
||||
|
||||
enable_friend_chat: bool = False
|
||||
"""是否启用好友聊天"""
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.message_receive.message import Message
|
||||
from maim_message import UserInfo, Seg
|
||||
from src.chat.message_receive.message import MessageSending, MessageSet
|
||||
from src.chat.message_receive.message_sender import message_manager
|
||||
from src.chat.message_receive.normal_message_sender import message_manager
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.config.config import global_config
|
||||
from rich.traceback import install
|
||||
|
||||
@@ -35,6 +35,11 @@ class KnowledgeFetcher:
|
||||
|
||||
logger.debug(f"[私聊][{self.private_name}]正在从LPMM知识库中获取知识")
|
||||
try:
|
||||
# 检查LPMM知识库是否启用
|
||||
if qa_manager is None:
|
||||
logger.debug(f"[私聊][{self.private_name}]LPMM知识库已禁用,跳过知识获取")
|
||||
return "未找到匹配的知识"
|
||||
|
||||
knowledge_info = qa_manager.get_knowledge(query)
|
||||
logger.debug(f"[私聊][{self.private_name}]LPMM知识库查询结果: {knowledge_info:150}")
|
||||
return knowledge_info
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import random
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from typing import List, Tuple
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
logger = get_logger("expressor")
|
||||
|
||||
|
||||
def init_prompt() -> None:
|
||||
personality_expression_prompt = """
|
||||
你的人物设定:{personality}
|
||||
|
||||
你说话的表达方式:{expression_style}
|
||||
|
||||
请从以上表达方式中总结出这个角色可能的语言风格,你必须严格根据人设引申,不要输出例子
|
||||
思考回复的特殊内容和情感
|
||||
思考有没有特殊的梗,一并总结成语言风格
|
||||
总结成如下格式的规律,总结的内容要详细,但具有概括性:
|
||||
当"xxx"时,可以"xxx", xxx不超过10个字
|
||||
|
||||
例如(不要输出例子):
|
||||
当"表示十分惊叹"时,使用"我嘞个xxxx"
|
||||
当"表示讽刺的赞同,不想讲道理"时,使用"对对对"
|
||||
当"想说明某个观点,但懒得明说",使用"懂的都懂"
|
||||
|
||||
现在请你概括
|
||||
"""
|
||||
Prompt(personality_expression_prompt, "personality_expression_prompt")
|
||||
|
||||
|
||||
class PersonalityExpression:
|
||||
def __init__(self):
|
||||
self.express_learn_model: LLMRequest = LLMRequest(
|
||||
model=global_config.model.replyer_1,
|
||||
max_tokens=512,
|
||||
request_type="expressor.learner",
|
||||
)
|
||||
self.meta_file_path = os.path.join("data", "expression", "personality", "expression_style_meta.json")
|
||||
self.expressions_file_path = os.path.join("data", "expression", "personality", "expressions.json")
|
||||
self.max_calculations = 20
|
||||
|
||||
def _read_meta_data(self):
|
||||
if os.path.exists(self.meta_file_path):
|
||||
try:
|
||||
with open(self.meta_file_path, "r", encoding="utf-8") as meta_file:
|
||||
meta_data = json.load(meta_file)
|
||||
# 检查是否有last_update_time字段
|
||||
if "last_update_time" not in meta_data:
|
||||
logger.warning(f"{self.meta_file_path} 中缺少last_update_time字段,将重新开始。")
|
||||
# 清空并重写元数据文件
|
||||
self._write_meta_data({"last_style_text": None, "count": 0, "last_update_time": None})
|
||||
# 清空并重写表达文件
|
||||
if os.path.exists(self.expressions_file_path):
|
||||
with open(self.expressions_file_path, "w", encoding="utf-8") as expressions_file:
|
||||
json.dump([], expressions_file, ensure_ascii=False, indent=2)
|
||||
logger.debug(f"已清空表达文件: {self.expressions_file_path}")
|
||||
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
||||
return meta_data
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"无法解析 {self.meta_file_path} 中的JSON数据,将重新开始。")
|
||||
# 清空并重写元数据文件
|
||||
self._write_meta_data({"last_style_text": None, "count": 0, "last_update_time": None})
|
||||
# 清空并重写表达文件
|
||||
if os.path.exists(self.expressions_file_path):
|
||||
with open(self.expressions_file_path, "w", encoding="utf-8") as expressions_file:
|
||||
json.dump([], expressions_file, ensure_ascii=False, indent=2)
|
||||
logger.debug(f"已清空表达文件: {self.expressions_file_path}")
|
||||
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
||||
return {"last_style_text": None, "count": 0, "last_update_time": None}
|
||||
|
||||
def _write_meta_data(self, data):
|
||||
os.makedirs(os.path.dirname(self.meta_file_path), exist_ok=True)
|
||||
with open(self.meta_file_path, "w", encoding="utf-8") as meta_file:
|
||||
json.dump(data, meta_file, ensure_ascii=False, indent=2)
|
||||
|
||||
async def extract_and_store_personality_expressions(self):
|
||||
"""
|
||||
检查data/expression/personality目录,不存在则创建。
|
||||
用peronality变量作为chat_str,调用LLM生成表达风格,解析后count=100,存储到expressions.json。
|
||||
如果expression_style、personality或identity发生变化,则删除旧的expressions.json并重置计数。
|
||||
对于相同的expression_style,最多计算self.max_calculations次。
|
||||
"""
|
||||
os.makedirs(os.path.dirname(self.expressions_file_path), exist_ok=True)
|
||||
|
||||
current_style_text = global_config.expression.expression_style
|
||||
current_personality = global_config.personality.personality_core
|
||||
|
||||
meta_data = self._read_meta_data()
|
||||
|
||||
last_style_text = meta_data.get("last_style_text")
|
||||
last_personality = meta_data.get("last_personality")
|
||||
count = meta_data.get("count", 0)
|
||||
|
||||
# 检查是否有任何变化
|
||||
if current_style_text != last_style_text or current_personality != last_personality:
|
||||
logger.info(
|
||||
f"检测到变化:\n风格: '{last_style_text}' -> '{current_style_text}'\n人格: '{last_personality}' -> '{current_personality}'"
|
||||
)
|
||||
count = 0
|
||||
if os.path.exists(self.expressions_file_path):
|
||||
try:
|
||||
os.remove(self.expressions_file_path)
|
||||
logger.info(f"已删除旧的表达文件: {self.expressions_file_path}")
|
||||
except OSError as e:
|
||||
logger.error(f"删除旧的表达文件 {self.expressions_file_path} 失败: {e}")
|
||||
|
||||
if count >= self.max_calculations:
|
||||
logger.debug(f"对于当前配置已达到最大计算次数 ({self.max_calculations})。跳过提取。")
|
||||
# 即使跳过,也更新元数据以反映当前配置已被识别且计数已满
|
||||
self._write_meta_data(
|
||||
{
|
||||
"last_style_text": current_style_text,
|
||||
"last_personality": current_personality,
|
||||
"count": count,
|
||||
"last_update_time": meta_data.get("last_update_time"),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
# 构建prompt
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
"personality_expression_prompt",
|
||||
personality=current_personality,
|
||||
expression_style=current_style_text,
|
||||
)
|
||||
|
||||
try:
|
||||
response, _ = await self.express_learn_model.generate_response_async(prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"个性表达方式提取失败: {e}")
|
||||
# 如果提取失败,保存当前的配置和未增加的计数
|
||||
self._write_meta_data(
|
||||
{
|
||||
"last_style_text": current_style_text,
|
||||
"last_personality": current_personality,
|
||||
"count": count,
|
||||
"last_update_time": meta_data.get("last_update_time"),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"个性表达方式提取response: {response}")
|
||||
|
||||
# 转为dict并count=100
|
||||
if response != "":
|
||||
expressions = self.parse_expression_response(response, "personality")
|
||||
# 读取已有的表达方式
|
||||
existing_expressions = []
|
||||
if os.path.exists(self.expressions_file_path):
|
||||
try:
|
||||
with open(self.expressions_file_path, "r", encoding="utf-8") as f:
|
||||
existing_expressions = json.load(f)
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
logger.warning(f"无法读取或解析 {self.expressions_file_path},将创建新的表达文件。")
|
||||
|
||||
# 创建新的表达方式
|
||||
new_expressions = []
|
||||
for _, situation, style in expressions:
|
||||
new_expressions.append({"situation": situation, "style": style, "count": 1})
|
||||
|
||||
# 合并表达方式,如果situation和style相同则累加count
|
||||
merged_expressions = existing_expressions.copy()
|
||||
for new_expr in new_expressions:
|
||||
found = False
|
||||
for existing_expr in merged_expressions:
|
||||
if (
|
||||
existing_expr["situation"] == new_expr["situation"]
|
||||
and existing_expr["style"] == new_expr["style"]
|
||||
):
|
||||
existing_expr["count"] += new_expr["count"]
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
merged_expressions.append(new_expr)
|
||||
|
||||
# 超过50条时随机删除多余的,只保留50条
|
||||
if len(merged_expressions) > 50:
|
||||
remove_count = len(merged_expressions) - 50
|
||||
remove_indices = set(random.sample(range(len(merged_expressions)), remove_count))
|
||||
merged_expressions = [item for idx, item in enumerate(merged_expressions) if idx not in remove_indices]
|
||||
|
||||
with open(self.expressions_file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(merged_expressions, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"已写入{len(merged_expressions)}条表达到{self.expressions_file_path}")
|
||||
|
||||
# 成功提取后更新元数据
|
||||
count += 1
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self._write_meta_data(
|
||||
{
|
||||
"last_style_text": current_style_text,
|
||||
"last_personality": current_personality,
|
||||
"count": count,
|
||||
"last_update_time": current_time,
|
||||
}
|
||||
)
|
||||
logger.info(f"成功处理。当前配置的计数现在是 {count},最后更新时间:{current_time}。")
|
||||
else:
|
||||
logger.warning(f"个性表达方式提取失败,模型返回空内容: {response}")
|
||||
|
||||
def parse_expression_response(self, response: str, chat_id: str) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
解析LLM返回的表达风格总结,每一行提取"当"和"使用"之间的内容,存储为(situation, style)元组
|
||||
"""
|
||||
expressions: List[Tuple[str, str, str]] = []
|
||||
for line in response.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# 查找"当"和下一个引号
|
||||
idx_when = line.find('当"')
|
||||
if idx_when == -1:
|
||||
continue
|
||||
idx_quote1 = idx_when + 1
|
||||
idx_quote2 = line.find('"', idx_quote1 + 1)
|
||||
if idx_quote2 == -1:
|
||||
continue
|
||||
situation = line[idx_quote1 + 1 : idx_quote2]
|
||||
# 查找"使用"
|
||||
idx_use = line.find('使用"', idx_quote2)
|
||||
if idx_use == -1:
|
||||
continue
|
||||
idx_quote3 = idx_use + 2
|
||||
idx_quote4 = line.find('"', idx_quote3 + 1)
|
||||
if idx_quote4 == -1:
|
||||
continue
|
||||
style = line[idx_quote3 + 1 : idx_quote4]
|
||||
expressions.append((chat_id, situation, style))
|
||||
return expressions
|
||||
|
||||
|
||||
init_prompt()
|
||||
@@ -1,11 +1,9 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import ast
|
||||
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from .personality import Personality
|
||||
from .identity import Identity
|
||||
from .expression_style import PersonalityExpression
|
||||
import random
|
||||
import json
|
||||
import os
|
||||
@@ -27,7 +25,6 @@ class Individuality:
|
||||
# 正常初始化实例属性
|
||||
self.personality: Optional[Personality] = None
|
||||
self.identity: Optional[Identity] = None
|
||||
self.express_style: PersonalityExpression = PersonalityExpression()
|
||||
|
||||
self.name = ""
|
||||
self.bot_person_id = ""
|
||||
@@ -151,8 +148,6 @@ class Individuality:
|
||||
else:
|
||||
logger.error("人设构建失败")
|
||||
|
||||
asyncio.create_task(self.express_style.extract_and_store_personality_expressions())
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将个体特征转换为字典格式"""
|
||||
return {
|
||||
|
||||
@@ -102,7 +102,8 @@ class LLMRequest:
|
||||
"o3",
|
||||
"o3-2025-04-16",
|
||||
"o3-mini",
|
||||
"o3-mini-2025-01-31o4-mini",
|
||||
"o3-mini-2025-01-31",
|
||||
"o4-mini",
|
||||
"o4-mini-2025-04-16",
|
||||
]
|
||||
|
||||
|
||||
15
src/main.py
15
src/main.py
@@ -10,8 +10,7 @@ from src.manager.mood_manager import MoodPrintTask, MoodUpdateTask
|
||||
from src.chat.emoji_system.emoji_manager import get_emoji_manager
|
||||
from src.chat.normal_chat.willing.willing_manager import get_willing_manager
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.heart_flow.heartflow import heartflow
|
||||
from src.chat.message_receive.message_sender import message_manager
|
||||
from src.chat.message_receive.normal_message_sender import message_manager
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.config.config import global_config
|
||||
from src.chat.message_receive.bot import chat_bot
|
||||
@@ -19,7 +18,7 @@ from src.common.logger import get_logger
|
||||
from src.individuality.individuality import get_individuality, Individuality
|
||||
from src.common.server import get_global_server, Server
|
||||
from rich.traceback import install
|
||||
from src.api.main import start_api_server
|
||||
# from src.api.main import start_api_server
|
||||
|
||||
# 导入新的插件管理器
|
||||
from src.plugin_system.core.plugin_manager import plugin_manager
|
||||
@@ -85,8 +84,8 @@ class MainSystem:
|
||||
await async_task_manager.add_task(TelemetryHeartBeatTask())
|
||||
|
||||
# 启动API服务器
|
||||
start_api_server()
|
||||
logger.info("API服务器启动成功")
|
||||
# start_api_server()
|
||||
# logger.info("API服务器启动成功")
|
||||
|
||||
# 加载所有actions,包括默认的和插件的
|
||||
plugin_count, component_count = plugin_manager.load_all_plugins()
|
||||
@@ -142,10 +141,6 @@ class MainSystem:
|
||||
await message_manager.start()
|
||||
logger.info("全局消息管理器启动成功")
|
||||
|
||||
# 启动心流系统主循环
|
||||
asyncio.create_task(heartflow.heartflow_start_working())
|
||||
logger.info("心流系统启动成功")
|
||||
|
||||
init_time = int(1000 * (time.time() - init_start_time))
|
||||
logger.info(f"初始化完成,神经元放电{init_time}次")
|
||||
except Exception as e:
|
||||
@@ -205,7 +200,7 @@ class MainSystem:
|
||||
expression_learner = get_expression_learner()
|
||||
while True:
|
||||
await asyncio.sleep(global_config.expression.learning_interval)
|
||||
if global_config.expression.enable_expression_learning:
|
||||
if global_config.expression.enable_expression_learning and global_config.expression.enable_expression:
|
||||
logger.info("[表达方式学习] 开始学习表达方式...")
|
||||
await expression_learner.learn_and_store_expression()
|
||||
logger.info("[表达方式学习] 表达方式学习完成")
|
||||
|
||||
378
src/mais4u/mais4u_chat/s4u_chat.py
Normal file
378
src/mais4u/mais4u_chat/s4u_chat.py
Normal file
@@ -0,0 +1,378 @@
|
||||
import asyncio
|
||||
import time
|
||||
import random
|
||||
from typing import Optional, Dict, Tuple # 导入类型提示
|
||||
from maim_message import UserInfo, Seg
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
|
||||
from .s4u_stream_generator import S4UStreamGenerator
|
||||
from src.chat.message_receive.message import MessageSending, MessageRecv
|
||||
from src.config.config import global_config
|
||||
from src.common.message.api import get_global_api
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
|
||||
|
||||
logger = get_logger("S4U_chat")
|
||||
|
||||
|
||||
class MessageSenderContainer:
|
||||
"""一个简单的容器,用于按顺序发送消息并模拟打字效果。"""
|
||||
|
||||
def __init__(self, chat_stream: ChatStream, original_message: MessageRecv):
|
||||
self.chat_stream = chat_stream
|
||||
self.original_message = original_message
|
||||
self.queue = asyncio.Queue()
|
||||
self.storage = MessageStorage()
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._paused_event = asyncio.Event()
|
||||
self._paused_event.set() # 默认设置为非暂停状态
|
||||
|
||||
async def add_message(self, chunk: str):
|
||||
"""向队列中添加一个消息块。"""
|
||||
await self.queue.put(chunk)
|
||||
|
||||
async def close(self):
|
||||
"""表示没有更多消息了,关闭队列。"""
|
||||
await self.queue.put(None) # Sentinel
|
||||
|
||||
def pause(self):
|
||||
"""暂停发送。"""
|
||||
self._paused_event.clear()
|
||||
|
||||
def resume(self):
|
||||
"""恢复发送。"""
|
||||
self._paused_event.set()
|
||||
|
||||
def _calculate_typing_delay(self, text: str) -> float:
|
||||
"""根据文本长度计算模拟打字延迟。"""
|
||||
chars_per_second = 15.0
|
||||
min_delay = 0.2
|
||||
max_delay = 2.0
|
||||
|
||||
delay = len(text) / chars_per_second
|
||||
return max(min_delay, min(delay, max_delay))
|
||||
|
||||
async def _send_worker(self):
|
||||
"""从队列中取出消息并发送。"""
|
||||
while True:
|
||||
try:
|
||||
# This structure ensures that task_done() is called for every item retrieved,
|
||||
# even if the worker is cancelled while processing the item.
|
||||
chunk = await self.queue.get()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
try:
|
||||
if chunk is None:
|
||||
break
|
||||
|
||||
# Check for pause signal *after* getting an item.
|
||||
await self._paused_event.wait()
|
||||
|
||||
# delay = self._calculate_typing_delay(chunk)
|
||||
delay = 0.1
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
current_time = time.time()
|
||||
msg_id = f"{current_time}_{random.randint(1000, 9999)}"
|
||||
|
||||
text_to_send = chunk
|
||||
|
||||
message_segment = Seg(type="text", data=text_to_send)
|
||||
bot_message = MessageSending(
|
||||
message_id=msg_id,
|
||||
chat_stream=self.chat_stream,
|
||||
bot_user_info=UserInfo(
|
||||
user_id=global_config.bot.qq_account,
|
||||
user_nickname=global_config.bot.nickname,
|
||||
platform=self.original_message.message_info.platform,
|
||||
),
|
||||
sender_info=self.original_message.message_info.user_info,
|
||||
message_segment=message_segment,
|
||||
reply=self.original_message,
|
||||
is_emoji=False,
|
||||
apply_set_reply_logic=True,
|
||||
reply_to=f"{self.original_message.message_info.user_info.platform}:{self.original_message.message_info.user_info.user_id}",
|
||||
)
|
||||
|
||||
await bot_message.process()
|
||||
|
||||
await get_global_api().send_message(bot_message)
|
||||
logger.info(f"已将消息 '{text_to_send}' 发往平台 '{bot_message.message_info.platform}'")
|
||||
|
||||
await self.storage.store_message(bot_message, self.chat_stream)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.chat_stream.get_stream_name()}] 消息发送或存储时出现错误: {e}", exc_info=True)
|
||||
|
||||
finally:
|
||||
# CRUCIAL: Always call task_done() for any item that was successfully retrieved.
|
||||
self.queue.task_done()
|
||||
|
||||
def start(self):
|
||||
"""启动发送任务。"""
|
||||
if self._task is None:
|
||||
self._task = asyncio.create_task(self._send_worker())
|
||||
|
||||
async def join(self):
|
||||
"""等待所有消息发送完毕。"""
|
||||
if self._task:
|
||||
await self._task
|
||||
|
||||
|
||||
class S4UChatManager:
|
||||
def __init__(self):
|
||||
self.s4u_chats: Dict[str, "S4UChat"] = {}
|
||||
|
||||
def get_or_create_chat(self, chat_stream: ChatStream) -> "S4UChat":
|
||||
if chat_stream.stream_id not in self.s4u_chats:
|
||||
stream_name = get_chat_manager().get_stream_name(chat_stream.stream_id) or chat_stream.stream_id
|
||||
logger.info(f"Creating new S4UChat for stream: {stream_name}")
|
||||
self.s4u_chats[chat_stream.stream_id] = S4UChat(chat_stream)
|
||||
return self.s4u_chats[chat_stream.stream_id]
|
||||
|
||||
|
||||
s4u_chat_manager = S4UChatManager()
|
||||
|
||||
|
||||
def get_s4u_chat_manager() -> S4UChatManager:
|
||||
return s4u_chat_manager
|
||||
|
||||
|
||||
class S4UChat:
|
||||
_MESSAGE_TIMEOUT_SECONDS = 60 # 普通消息存活时间(秒)
|
||||
|
||||
def __init__(self, chat_stream: ChatStream):
|
||||
"""初始化 S4UChat 实例。"""
|
||||
|
||||
self.chat_stream = chat_stream
|
||||
self.stream_id = chat_stream.stream_id
|
||||
self.stream_name = get_chat_manager().get_stream_name(self.stream_id) or self.stream_id
|
||||
|
||||
# 两个消息队列
|
||||
self._vip_queue = asyncio.PriorityQueue()
|
||||
self._normal_queue = asyncio.PriorityQueue()
|
||||
|
||||
self._entry_counter = 0 # 保证FIFO的全局计数器
|
||||
self._new_message_event = asyncio.Event() # 用于唤醒处理器
|
||||
|
||||
self._processing_task = asyncio.create_task(self._message_processor())
|
||||
self._current_generation_task: Optional[asyncio.Task] = None
|
||||
# 当前消息的元数据:(队列类型, 优先级分数, 计数器, 消息对象)
|
||||
self._current_message_being_replied: Optional[Tuple[str, float, int, MessageRecv]] = None
|
||||
|
||||
self._is_replying = False
|
||||
self.gpt = S4UStreamGenerator()
|
||||
self.interest_dict: Dict[str, float] = {} # 用户兴趣分
|
||||
self.at_bot_priority_bonus = 100.0 # @机器人的优先级加成
|
||||
self.normal_queue_max_size = 50 # 普通队列最大容量
|
||||
logger.info(f"[{self.stream_name}] S4UChat with two-queue system initialized.")
|
||||
|
||||
def _is_vip(self, message: MessageRecv) -> bool:
|
||||
"""检查消息是否来自VIP用户。"""
|
||||
# 您需要修改此处或在配置文件中定义VIP用户
|
||||
vip_user_ids = ["1026294844"]
|
||||
vip_user_ids = [""]
|
||||
return message.message_info.user_info.user_id in vip_user_ids
|
||||
|
||||
def _get_interest_score(self, user_id: str) -> float:
|
||||
"""获取用户的兴趣分,默认为1.0"""
|
||||
return self.interest_dict.get(user_id, 1.0)
|
||||
|
||||
def _calculate_base_priority_score(self, message: MessageRecv) -> float:
|
||||
"""
|
||||
为消息计算基础优先级分数。分数越高,优先级越高。
|
||||
"""
|
||||
score = 0.0
|
||||
# 如果消息 @ 了机器人,则增加一个很大的分数
|
||||
if f"@{global_config.bot.nickname}" in message.processed_plain_text or any(
|
||||
f"@{alias}" in message.processed_plain_text for alias in global_config.bot.alias_names
|
||||
):
|
||||
score += self.at_bot_priority_bonus
|
||||
|
||||
# 加上用户的固有兴趣分
|
||||
score += self._get_interest_score(message.message_info.user_info.user_id)
|
||||
return score
|
||||
|
||||
async def add_message(self, message: MessageRecv) -> None:
|
||||
"""根据VIP状态和中断逻辑将消息放入相应队列。"""
|
||||
is_vip = self._is_vip(message)
|
||||
new_priority_score = self._calculate_base_priority_score(message)
|
||||
|
||||
should_interrupt = False
|
||||
if self._current_generation_task and not self._current_generation_task.done():
|
||||
if self._current_message_being_replied:
|
||||
current_queue, current_priority, _, current_msg = self._current_message_being_replied
|
||||
|
||||
# 规则:VIP从不被打断
|
||||
if current_queue == "vip":
|
||||
pass # Do nothing
|
||||
|
||||
# 规则:普通消息可以被打断
|
||||
elif current_queue == "normal":
|
||||
# VIP消息可以打断普通消息
|
||||
if is_vip:
|
||||
should_interrupt = True
|
||||
logger.info(f"[{self.stream_name}] VIP message received, interrupting current normal task.")
|
||||
# 普通消息的内部打断逻辑
|
||||
else:
|
||||
new_sender_id = message.message_info.user_info.user_id
|
||||
current_sender_id = current_msg.message_info.user_info.user_id
|
||||
# 新消息优先级更高
|
||||
if new_priority_score > current_priority:
|
||||
should_interrupt = True
|
||||
logger.info(f"[{self.stream_name}] New normal message has higher priority, interrupting.")
|
||||
# 同用户,新消息的优先级不能更低
|
||||
elif new_sender_id == current_sender_id and new_priority_score >= current_priority:
|
||||
should_interrupt = True
|
||||
logger.info(f"[{self.stream_name}] Same user sent new message, interrupting.")
|
||||
|
||||
if should_interrupt:
|
||||
if self.gpt.partial_response:
|
||||
logger.warning(
|
||||
f"[{self.stream_name}] Interrupting reply. Already generated: '{self.gpt.partial_response}'"
|
||||
)
|
||||
self._current_generation_task.cancel()
|
||||
|
||||
# asyncio.PriorityQueue 是最小堆,所以我们存入分数的相反数
|
||||
# 这样,原始分数越高的消息,在队列中的优先级数字越小,越靠前
|
||||
item = (-new_priority_score, self._entry_counter, time.time(), message)
|
||||
|
||||
if is_vip:
|
||||
await self._vip_queue.put(item)
|
||||
logger.info(f"[{self.stream_name}] VIP message added to queue.")
|
||||
else:
|
||||
# 应用普通队列的最大容量限制
|
||||
if self._normal_queue.qsize() >= self.normal_queue_max_size:
|
||||
# 队列已满,简单忽略新消息
|
||||
# 更复杂的逻辑(如替换掉队列中优先级最低的)对于 asyncio.PriorityQueue 来说实现复杂
|
||||
logger.debug(
|
||||
f"[{self.stream_name}] Normal queue is full, ignoring new message from {message.message_info.user_info.user_id}"
|
||||
)
|
||||
return
|
||||
|
||||
await self._normal_queue.put(item)
|
||||
|
||||
self._entry_counter += 1
|
||||
self._new_message_event.set() # 唤醒处理器
|
||||
|
||||
async def _message_processor(self):
|
||||
"""调度器:优先处理VIP队列,然后处理普通队列。"""
|
||||
while True:
|
||||
try:
|
||||
# 等待有新消息的信号,避免空转
|
||||
await self._new_message_event.wait()
|
||||
self._new_message_event.clear()
|
||||
|
||||
# 优先处理VIP队列
|
||||
if not self._vip_queue.empty():
|
||||
neg_priority, entry_count, _, message = self._vip_queue.get_nowait()
|
||||
priority = -neg_priority
|
||||
queue_name = "vip"
|
||||
# 其次处理普通队列
|
||||
elif not self._normal_queue.empty():
|
||||
neg_priority, entry_count, timestamp, message = self._normal_queue.get_nowait()
|
||||
priority = -neg_priority
|
||||
# 检查普通消息是否超时
|
||||
if time.time() - timestamp > self._MESSAGE_TIMEOUT_SECONDS:
|
||||
logger.info(
|
||||
f"[{self.stream_name}] Discarding stale normal message: {message.processed_plain_text[:20]}..."
|
||||
)
|
||||
self._normal_queue.task_done()
|
||||
continue # 处理下一条
|
||||
queue_name = "normal"
|
||||
else:
|
||||
continue # 没有消息了,回去等事件
|
||||
|
||||
self._current_message_being_replied = (queue_name, priority, entry_count, message)
|
||||
self._current_generation_task = asyncio.create_task(self._generate_and_send(message))
|
||||
|
||||
try:
|
||||
await self._current_generation_task
|
||||
except asyncio.CancelledError:
|
||||
logger.info(
|
||||
f"[{self.stream_name}] Reply generation was interrupted externally for {queue_name} message. The message will be discarded."
|
||||
)
|
||||
# 被中断的消息应该被丢弃,而不是重新排队,以响应最新的用户输入。
|
||||
# 旧的重新入队逻辑会导致所有中断的消息最终都被回复。
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.stream_name}] _generate_and_send task error: {e}", exc_info=True)
|
||||
finally:
|
||||
self._current_generation_task = None
|
||||
self._current_message_being_replied = None
|
||||
# 标记任务完成
|
||||
if queue_name == "vip":
|
||||
self._vip_queue.task_done()
|
||||
else:
|
||||
self._normal_queue.task_done()
|
||||
|
||||
# 检查是否还有任务,有则立即再次触发事件
|
||||
if not self._vip_queue.empty() or not self._normal_queue.empty():
|
||||
self._new_message_event.set()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.stream_name}] Message processor is shutting down.")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.stream_name}] Message processor main loop error: {e}", exc_info=True)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _generate_and_send(self, message: MessageRecv):
|
||||
"""为单个消息生成文本和音频回复。整个过程可以被中断。"""
|
||||
self._is_replying = True
|
||||
sender_container = MessageSenderContainer(self.chat_stream, message)
|
||||
sender_container.start()
|
||||
|
||||
try:
|
||||
logger.info(f"[S4U] 开始为消息生成文本和音频流: '{message.processed_plain_text[:30]}...'")
|
||||
|
||||
# 1. 逐句生成文本、发送并播放音频
|
||||
gen = self.gpt.generate_response(message, "")
|
||||
async for chunk in gen:
|
||||
# 如果任务被取消,await 会在此处引发 CancelledError
|
||||
|
||||
# a. 发送文本块
|
||||
await sender_container.add_message(chunk)
|
||||
|
||||
# b. 为该文本块生成并播放音频
|
||||
# if chunk.strip():
|
||||
# audio_data = await self.audio_generator.generate(chunk)
|
||||
# player = MockAudioPlayer(audio_data)
|
||||
# await player.play()
|
||||
|
||||
# 等待所有文本消息发送完成
|
||||
await sender_container.close()
|
||||
await sender_container.join()
|
||||
logger.info(f"[{self.stream_name}] 所有文本和音频块处理完毕。")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"[{self.stream_name}] 回复流程(文本或音频)被中断。")
|
||||
raise # 将取消异常向上传播
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.stream_name}] 回复生成过程中出现错误: {e}", exc_info=True)
|
||||
finally:
|
||||
self._is_replying = False
|
||||
# 确保发送器被妥善关闭(即使已关闭,再次调用也是安全的)
|
||||
sender_container.resume()
|
||||
if not sender_container._task.done():
|
||||
await sender_container.close()
|
||||
await sender_container.join()
|
||||
logger.info(f"[{self.stream_name}] _generate_and_send 任务结束,资源已清理。")
|
||||
|
||||
async def shutdown(self):
|
||||
"""平滑关闭处理任务。"""
|
||||
logger.info(f"正在关闭 S4UChat: {self.stream_name}")
|
||||
|
||||
# 取消正在运行的任务
|
||||
if self._current_generation_task and not self._current_generation_task.done():
|
||||
self._current_generation_task.cancel()
|
||||
|
||||
if self._processing_task and not self._processing_task.done():
|
||||
self._processing_task.cancel()
|
||||
|
||||
# 等待任务响应取消
|
||||
try:
|
||||
await self._processing_task
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"处理任务已成功取消: {self.stream_name}")
|
||||
57
src/mais4u/mais4u_chat/s4u_msg_processor.py
Normal file
57
src/mais4u/mais4u_chat/s4u_msg_processor.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from src.chat.message_receive.message import MessageRecv
|
||||
from src.chat.message_receive.storage import MessageStorage
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.common.logger import get_logger
|
||||
from .s4u_chat import get_s4u_chat_manager
|
||||
|
||||
|
||||
# from ..message_receive.message_buffer import message_buffer
|
||||
|
||||
logger = get_logger("chat")
|
||||
|
||||
|
||||
class S4UMessageProcessor:
|
||||
"""心流处理器,负责处理接收到的消息并计算兴趣度"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化心流处理器,创建消息存储实例"""
|
||||
self.storage = MessageStorage()
|
||||
|
||||
async def process_message(self, message: MessageRecv) -> None:
|
||||
"""处理接收到的原始消息数据
|
||||
|
||||
主要流程:
|
||||
1. 消息解析与初始化
|
||||
2. 消息缓冲处理
|
||||
3. 过滤检查
|
||||
4. 兴趣度计算
|
||||
5. 关系处理
|
||||
|
||||
Args:
|
||||
message_data: 原始消息字符串
|
||||
"""
|
||||
|
||||
target_user_id_list = ["1026294844", "964959351"]
|
||||
|
||||
# 1. 消息解析与初始化
|
||||
groupinfo = message.message_info.group_info
|
||||
userinfo = message.message_info.user_info
|
||||
messageinfo = message.message_info
|
||||
|
||||
chat = await get_chat_manager().get_or_create_stream(
|
||||
platform=messageinfo.platform,
|
||||
user_info=userinfo,
|
||||
group_info=groupinfo,
|
||||
)
|
||||
|
||||
await self.storage.store_message(message, chat)
|
||||
|
||||
s4u_chat = get_s4u_chat_manager().get_or_create_chat(chat)
|
||||
|
||||
if userinfo.user_id in target_user_id_list:
|
||||
await s4u_chat.add_message(message)
|
||||
else:
|
||||
await s4u_chat.add_message(message)
|
||||
|
||||
# 7. 日志记录
|
||||
logger.info(f"[S4U]{userinfo.user_nickname}:{message.processed_plain_text}")
|
||||
270
src/mais4u/mais4u_chat/s4u_prompt.py
Normal file
270
src/mais4u/mais4u_chat/s4u_prompt.py
Normal file
@@ -0,0 +1,270 @@
|
||||
from src.config.config import global_config
|
||||
from src.common.logger import get_logger
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
import time
|
||||
from src.chat.utils.utils import get_recent_group_speaker
|
||||
from src.chat.memory_system.Hippocampus import hippocampus_manager
|
||||
import random
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
import ast
|
||||
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
from src.person_info.relationship_manager import get_relationship_manager
|
||||
|
||||
logger = get_logger("prompt")
|
||||
|
||||
|
||||
def init_prompt():
|
||||
Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1")
|
||||
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
|
||||
Prompt("在群里聊天", "chat_target_group2")
|
||||
Prompt("和{sender_name}私聊", "chat_target_private2")
|
||||
|
||||
Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
|
||||
Prompt("\n关于你们的关系,你需要知道:\n{relation_info}\n", "relation_prompt")
|
||||
Prompt("你回想起了一些事情:\n{memory_info}\n", "memory_prompt")
|
||||
|
||||
Prompt(
|
||||
"""{identity_block}
|
||||
|
||||
{relation_info_block}
|
||||
{memory_block}
|
||||
|
||||
你现在的主要任务是和 {sender_name} 聊天。同时,也有其他用户会参与你们的聊天,你可以参考他们的回复内容,但是你主要还是关注你和{sender_name}的聊天内容。
|
||||
|
||||
{background_dialogue_prompt}
|
||||
--------------------------------
|
||||
{time_block}
|
||||
这是你和{sender_name}的对话,你们正在交流中:
|
||||
{core_dialogue_prompt}
|
||||
|
||||
对方最新发送的内容:{message_txt}
|
||||
回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。
|
||||
不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容,现在{sender_name}正在等待你的回复。
|
||||
你的回复风格不要浮夸,有逻辑和条理,请你继续回复{sender_name}。
|
||||
你的发言:
|
||||
""",
|
||||
"s4u_prompt", # New template for private CHAT chat
|
||||
)
|
||||
|
||||
|
||||
class PromptBuilder:
|
||||
def __init__(self):
|
||||
self.prompt_built = ""
|
||||
self.activate_messages = ""
|
||||
|
||||
async def build_identity_block(self) -> str:
|
||||
person_info_manager = get_person_info_manager()
|
||||
bot_person_id = person_info_manager.get_person_id("system", "bot_id")
|
||||
bot_name = global_config.bot.nickname
|
||||
if global_config.bot.alias_names:
|
||||
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
|
||||
else:
|
||||
bot_nickname = ""
|
||||
short_impression = await person_info_manager.get_value(bot_person_id, "short_impression")
|
||||
try:
|
||||
if isinstance(short_impression, str) and short_impression.strip():
|
||||
short_impression = ast.literal_eval(short_impression)
|
||||
elif not short_impression:
|
||||
logger.warning("short_impression为空,使用默认值")
|
||||
short_impression = ["友好活泼", "人类"]
|
||||
except (ValueError, SyntaxError) as e:
|
||||
logger.error(f"解析short_impression失败: {e}, 原始值: {short_impression}")
|
||||
short_impression = ["友好活泼", "人类"]
|
||||
|
||||
if not isinstance(short_impression, list) or len(short_impression) < 2:
|
||||
logger.warning(f"short_impression格式不正确: {short_impression}, 使用默认值")
|
||||
short_impression = ["友好活泼", "人类"]
|
||||
personality = short_impression[0]
|
||||
identity = short_impression[1]
|
||||
prompt_personality = personality + "," + identity
|
||||
return f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}:"
|
||||
|
||||
async def build_relation_info(self, chat_stream) -> str:
|
||||
is_group_chat = bool(chat_stream.group_info)
|
||||
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.chat.max_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 = ""
|
||||
if global_config.relationship.enable_relationship and who_chat_in_group:
|
||||
relationship_manager = get_relationship_manager()
|
||||
relation_info_list = await asyncio.gather(
|
||||
*[relationship_manager.build_relationship_info(person) for person in who_chat_in_group]
|
||||
)
|
||||
relation_info = "".join(relation_info_list)
|
||||
if relation_info:
|
||||
relation_prompt = await global_prompt_manager.format_prompt(
|
||||
"relation_prompt", relation_info=relation_info
|
||||
)
|
||||
return relation_prompt
|
||||
|
||||
async def build_memory_block(self, text: str) -> str:
|
||||
related_memory = await hippocampus_manager.get_memory_from_text(
|
||||
text=text, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False
|
||||
)
|
||||
|
||||
related_memory_info = ""
|
||||
if related_memory:
|
||||
for memory in related_memory:
|
||||
related_memory_info += memory[1]
|
||||
return await global_prompt_manager.format_prompt("memory_prompt", memory_info=related_memory_info)
|
||||
return ""
|
||||
|
||||
def build_chat_history_prompts(self, chat_stream, message) -> (str, str):
|
||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_stream.stream_id,
|
||||
timestamp=time.time(),
|
||||
limit=100,
|
||||
)
|
||||
|
||||
talk_type = message.message_info.platform + ":" + message.chat_stream.user_info.user_id
|
||||
|
||||
core_dialogue_list = []
|
||||
background_dialogue_list = []
|
||||
bot_id = str(global_config.bot.qq_account)
|
||||
target_user_id = str(message.chat_stream.user_info.user_id)
|
||||
|
||||
for msg_dict in message_list_before_now:
|
||||
try:
|
||||
msg_user_id = str(msg_dict.get("user_id"))
|
||||
if msg_user_id == bot_id:
|
||||
if msg_dict.get("reply_to") and talk_type == msg_dict.get("reply_to"):
|
||||
core_dialogue_list.append(msg_dict)
|
||||
else:
|
||||
background_dialogue_list.append(msg_dict)
|
||||
elif msg_user_id == target_user_id:
|
||||
core_dialogue_list.append(msg_dict)
|
||||
else:
|
||||
background_dialogue_list.append(msg_dict)
|
||||
except Exception as e:
|
||||
logger.error(f"无法处理历史消息记录: {msg_dict}, 错误: {e}")
|
||||
|
||||
background_dialogue_prompt = ""
|
||||
if background_dialogue_list:
|
||||
latest_25_msgs = background_dialogue_list[-25:]
|
||||
background_dialogue_prompt_str = build_readable_messages(
|
||||
latest_25_msgs,
|
||||
merge_messages=True,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
show_pic=False,
|
||||
)
|
||||
background_dialogue_prompt = f"这是其他用户的发言:\n{background_dialogue_prompt_str}"
|
||||
|
||||
core_msg_str = ""
|
||||
if core_dialogue_list:
|
||||
core_dialogue_list = core_dialogue_list[-50:]
|
||||
|
||||
first_msg = core_dialogue_list[0]
|
||||
start_speaking_user_id = first_msg.get("user_id")
|
||||
if start_speaking_user_id == bot_id:
|
||||
last_speaking_user_id = bot_id
|
||||
msg_seg_str = "你的发言:\n"
|
||||
else:
|
||||
start_speaking_user_id = target_user_id
|
||||
last_speaking_user_id = start_speaking_user_id
|
||||
msg_seg_str = "对方的发言:\n"
|
||||
|
||||
msg_seg_str += f"{time.strftime('%H:%M:%S', time.localtime(first_msg.get('time')))}: {first_msg.get('processed_plain_text')}\n"
|
||||
|
||||
all_msg_seg_list = []
|
||||
for msg in core_dialogue_list[1:]:
|
||||
speaker = msg.get("user_id")
|
||||
if speaker == last_speaking_user_id:
|
||||
msg_seg_str += f"{time.strftime('%H:%M:%S', time.localtime(msg.get('time')))}: {msg.get('processed_plain_text')}\n"
|
||||
else:
|
||||
msg_seg_str = f"{msg_seg_str}\n"
|
||||
all_msg_seg_list.append(msg_seg_str)
|
||||
|
||||
if speaker == bot_id:
|
||||
msg_seg_str = "你的发言:\n"
|
||||
else:
|
||||
msg_seg_str = "对方的发言:\n"
|
||||
|
||||
msg_seg_str += f"{time.strftime('%H:%M:%S', time.localtime(msg.get('time')))}: {msg.get('processed_plain_text')}\n"
|
||||
last_speaking_user_id = speaker
|
||||
|
||||
all_msg_seg_list.append(msg_seg_str)
|
||||
for msg in all_msg_seg_list:
|
||||
core_msg_str += msg
|
||||
|
||||
return core_msg_str, background_dialogue_prompt
|
||||
|
||||
async def build_prompt_normal(
|
||||
self,
|
||||
message,
|
||||
chat_stream,
|
||||
message_txt: str,
|
||||
sender_name: str = "某人",
|
||||
) -> str:
|
||||
identity_block, relation_info_block, memory_block = await asyncio.gather(
|
||||
self.build_identity_block(), self.build_relation_info(chat_stream), self.build_memory_block(message_txt)
|
||||
)
|
||||
|
||||
core_dialogue_prompt, background_dialogue_prompt = self.build_chat_history_prompts(chat_stream, message)
|
||||
|
||||
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
template_name = "s4u_prompt"
|
||||
|
||||
prompt = await global_prompt_manager.format_prompt(
|
||||
template_name,
|
||||
identity_block=identity_block,
|
||||
time_block=time_block,
|
||||
relation_info_block=relation_info_block,
|
||||
memory_block=memory_block,
|
||||
sender_name=sender_name,
|
||||
core_dialogue_prompt=core_dialogue_prompt,
|
||||
background_dialogue_prompt=background_dialogue_prompt,
|
||||
message_txt=message_txt,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def weighted_sample_no_replacement(items, weights, k) -> list:
|
||||
"""
|
||||
加权且不放回地随机抽取k个元素。
|
||||
|
||||
参数:
|
||||
items: 待抽取的元素列表
|
||||
weights: 每个元素对应的权重(与items等长,且为正数)
|
||||
k: 需要抽取的元素个数
|
||||
返回:
|
||||
selected: 按权重加权且不重复抽取的k个元素组成的列表
|
||||
|
||||
如果 items 中的元素不足 k 个,就只会返回所有可用的元素
|
||||
|
||||
实现思路:
|
||||
每次从当前池中按权重加权随机选出一个元素,选中后将其从池中移除,重复k次。
|
||||
这样保证了:
|
||||
1. count越大被选中概率越高
|
||||
2. 不会重复选中同一个元素
|
||||
"""
|
||||
selected = []
|
||||
pool = list(zip(items, weights))
|
||||
for _ in range(min(k, len(pool))):
|
||||
total = sum(w for _, w in pool)
|
||||
r = random.uniform(0, total)
|
||||
upto = 0
|
||||
for idx, (item, weight) in enumerate(pool):
|
||||
upto += weight
|
||||
if upto >= r:
|
||||
selected.append(item)
|
||||
pool.pop(idx)
|
||||
break
|
||||
return selected
|
||||
|
||||
|
||||
init_prompt()
|
||||
prompt_builder = PromptBuilder()
|
||||
157
src/mais4u/mais4u_chat/s4u_stream_generator.py
Normal file
157
src/mais4u/mais4u_chat/s4u_stream_generator.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.mais4u.openai_client import AsyncOpenAIClient
|
||||
from src.config.config import global_config
|
||||
from src.chat.message_receive.message import MessageRecv
|
||||
from src.mais4u.mais4u_chat.s4u_prompt import prompt_builder
|
||||
from src.common.logger import get_logger
|
||||
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
|
||||
logger = get_logger("s4u_stream_generator")
|
||||
|
||||
|
||||
class S4UStreamGenerator:
|
||||
def __init__(self):
|
||||
replyer_1_config = global_config.model.replyer_1
|
||||
provider = replyer_1_config.get("provider")
|
||||
if not provider:
|
||||
logger.error("`replyer_1` 在配置文件中缺少 `provider` 字段")
|
||||
raise ValueError("`replyer_1` 在配置文件中缺少 `provider` 字段")
|
||||
|
||||
api_key = os.environ.get(f"{provider.upper()}_KEY")
|
||||
base_url = os.environ.get(f"{provider.upper()}_BASE_URL")
|
||||
|
||||
if not api_key:
|
||||
logger.error(f"环境变量 {provider.upper()}_KEY 未设置")
|
||||
raise ValueError(f"环境变量 {provider.upper()}_KEY 未设置")
|
||||
|
||||
self.client_1 = AsyncOpenAIClient(api_key=api_key, base_url=base_url)
|
||||
self.model_1_name = replyer_1_config.get("name")
|
||||
if not self.model_1_name:
|
||||
logger.error("`replyer_1` 在配置文件中缺少 `model_name` 字段")
|
||||
raise ValueError("`replyer_1` 在配置文件中缺少 `model_name` 字段")
|
||||
self.replyer_1_config = replyer_1_config
|
||||
|
||||
self.model_sum = LLMRequest(model=global_config.model.memory_summary, temperature=0.7, request_type="relation")
|
||||
self.current_model_name = "unknown model"
|
||||
self.partial_response = ""
|
||||
|
||||
# 正则表达式用于按句子切分,同时处理各种标点和边缘情况
|
||||
# 匹配常见的句子结束符,但会忽略引号内和数字中的标点
|
||||
self.sentence_split_pattern = re.compile(
|
||||
r'([^\s\w"\'([{]*["\'([{].*?["\'}\])][^\s\w"\'([{]*|' # 匹配被引号/括号包裹的内容
|
||||
r'[^.。!??!\n\r]+(?:[.。!??!\n\r](?![\'"])|$))', # 匹配直到句子结束符
|
||||
re.UNICODE | re.DOTALL,
|
||||
)
|
||||
|
||||
async def generate_response(
|
||||
self, message: MessageRecv, previous_reply_context: str = ""
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""根据当前模型类型选择对应的生成函数"""
|
||||
# 从global_config中获取模型概率值并选择模型
|
||||
self.partial_response = ""
|
||||
current_client = self.client_1
|
||||
self.current_model_name = self.model_1_name
|
||||
|
||||
person_id = PersonInfoManager.get_person_id(
|
||||
message.chat_stream.user_info.platform, message.chat_stream.user_info.user_id
|
||||
)
|
||||
person_info_manager = get_person_info_manager()
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
|
||||
if message.chat_stream.user_info.user_nickname:
|
||||
sender_name = f"[{message.chat_stream.user_info.user_nickname}](你叫ta{person_name})"
|
||||
else:
|
||||
sender_name = f"用户({message.chat_stream.user_info.user_id})"
|
||||
|
||||
# 构建prompt
|
||||
if previous_reply_context:
|
||||
message_txt = f"""
|
||||
你正在回复用户的消息,但中途被打断了。这是已有的对话上下文:
|
||||
[你已经对上一条消息说的话]: {previous_reply_context}
|
||||
---
|
||||
[这是用户发来的新消息, 你需要结合上下文,对此进行回复]:
|
||||
{message.processed_plain_text}
|
||||
"""
|
||||
else:
|
||||
message_txt = message.processed_plain_text
|
||||
|
||||
prompt = await prompt_builder.build_prompt_normal(
|
||||
message=message,
|
||||
message_txt=message_txt,
|
||||
sender_name=sender_name,
|
||||
chat_stream=message.chat_stream,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"{self.current_model_name}思考:{message_txt[:30] + '...' if len(message_txt) > 30 else message_txt}"
|
||||
) # noqa: E501
|
||||
|
||||
extra_kwargs = {}
|
||||
if self.replyer_1_config.get("enable_thinking") is not None:
|
||||
extra_kwargs["enable_thinking"] = self.replyer_1_config.get("enable_thinking")
|
||||
if self.replyer_1_config.get("thinking_budget") is not None:
|
||||
extra_kwargs["thinking_budget"] = self.replyer_1_config.get("thinking_budget")
|
||||
|
||||
async for chunk in self._generate_response_with_model(
|
||||
prompt, current_client, self.current_model_name, **extra_kwargs
|
||||
):
|
||||
yield chunk
|
||||
|
||||
async def _generate_response_with_model(
|
||||
self,
|
||||
prompt: str,
|
||||
client: AsyncOpenAIClient,
|
||||
model_name: str,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
print(prompt)
|
||||
|
||||
buffer = ""
|
||||
delimiters = ",。!?,.!?\n\r" # For final trimming
|
||||
punctuation_buffer = ""
|
||||
|
||||
async for content in client.get_stream_content(
|
||||
messages=[{"role": "user", "content": prompt}], model=model_name, **kwargs
|
||||
):
|
||||
buffer += content
|
||||
|
||||
# 使用正则表达式匹配句子
|
||||
last_match_end = 0
|
||||
for match in self.sentence_split_pattern.finditer(buffer):
|
||||
sentence = match.group(0).strip()
|
||||
if sentence:
|
||||
# 如果句子看起来完整(即不只是等待更多内容),则发送
|
||||
if match.end(0) < len(buffer) or sentence.endswith(tuple(delimiters)):
|
||||
# 检查是否只是一个标点符号
|
||||
if sentence in [",", ",", ".", "。", "!", "!", "?", "?"]:
|
||||
punctuation_buffer += sentence
|
||||
else:
|
||||
# 发送之前累积的标点和当前句子
|
||||
to_yield = punctuation_buffer + sentence
|
||||
if to_yield.endswith((",", ",")):
|
||||
to_yield = to_yield.rstrip(",,")
|
||||
|
||||
self.partial_response += to_yield
|
||||
yield to_yield
|
||||
punctuation_buffer = "" # 清空标点符号缓冲区
|
||||
await asyncio.sleep(0) # 允许其他任务运行
|
||||
|
||||
last_match_end = match.end(0)
|
||||
|
||||
# 从缓冲区移除已发送的部分
|
||||
if last_match_end > 0:
|
||||
buffer = buffer[last_match_end:]
|
||||
|
||||
# 发送缓冲区中剩余的任何内容
|
||||
to_yield = (punctuation_buffer + buffer).strip()
|
||||
if to_yield:
|
||||
if to_yield.endswith((",", ",")):
|
||||
to_yield = to_yield.rstrip(",,")
|
||||
if to_yield:
|
||||
self.partial_response += to_yield
|
||||
yield to_yield
|
||||
286
src/mais4u/openai_client.py
Normal file
286
src/mais4u/openai_client.py
Normal file
@@ -0,0 +1,286 @@
|
||||
from typing import AsyncGenerator, Dict, List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from openai import AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatMessage:
|
||||
"""聊天消息数据类"""
|
||||
|
||||
role: str
|
||||
content: str
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {"role": self.role, "content": self.content}
|
||||
|
||||
|
||||
class AsyncOpenAIClient:
|
||||
"""异步OpenAI客户端,支持流式传输"""
|
||||
|
||||
def __init__(self, api_key: str, base_url: Optional[str] = None):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
Args:
|
||||
api_key: OpenAI API密钥
|
||||
base_url: 可选的API基础URL,用于自定义端点
|
||||
"""
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
timeout=10.0, # 设置60秒的全局超时
|
||||
)
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
messages: List[Union[ChatMessage, Dict[str, str]]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> ChatCompletion:
|
||||
"""
|
||||
非流式聊天完成
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
完整的聊天回复
|
||||
"""
|
||||
# 转换消息格式
|
||||
formatted_messages = []
|
||||
for msg in messages:
|
||||
if isinstance(msg, ChatMessage):
|
||||
formatted_messages.append(msg.to_dict())
|
||||
else:
|
||||
formatted_messages.append(msg)
|
||||
|
||||
extra_body = {}
|
||||
if kwargs.get("enable_thinking") is not None:
|
||||
extra_body["enable_thinking"] = kwargs.pop("enable_thinking")
|
||||
if kwargs.get("thinking_budget") is not None:
|
||||
extra_body["thinking_budget"] = kwargs.pop("thinking_budget")
|
||||
|
||||
response = await self.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=formatted_messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=False,
|
||||
extra_body=extra_body if extra_body else None,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def chat_completion_stream(
|
||||
self,
|
||||
messages: List[Union[ChatMessage, Dict[str, str]]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
||||
"""
|
||||
流式聊天完成
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
**kwargs: 其他参数
|
||||
|
||||
Yields:
|
||||
ChatCompletionChunk: 流式响应块
|
||||
"""
|
||||
# 转换消息格式
|
||||
formatted_messages = []
|
||||
for msg in messages:
|
||||
if isinstance(msg, ChatMessage):
|
||||
formatted_messages.append(msg.to_dict())
|
||||
else:
|
||||
formatted_messages.append(msg)
|
||||
|
||||
extra_body = {}
|
||||
if kwargs.get("enable_thinking") is not None:
|
||||
extra_body["enable_thinking"] = kwargs.pop("enable_thinking")
|
||||
if kwargs.get("thinking_budget") is not None:
|
||||
extra_body["thinking_budget"] = kwargs.pop("thinking_budget")
|
||||
|
||||
stream = await self.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=formatted_messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
stream=True,
|
||||
extra_body=extra_body if extra_body else None,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async for chunk in stream:
|
||||
yield chunk
|
||||
|
||||
async def get_stream_content(
|
||||
self,
|
||||
messages: List[Union[ChatMessage, Dict[str, str]]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
获取流式内容(只返回文本内容)
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
**kwargs: 其他参数
|
||||
|
||||
Yields:
|
||||
str: 文本内容片段
|
||||
"""
|
||||
async for chunk in self.chat_completion_stream(
|
||||
messages=messages, model=model, temperature=temperature, max_tokens=max_tokens, **kwargs
|
||||
):
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
async def collect_stream_response(
|
||||
self,
|
||||
messages: List[Union[ChatMessage, Dict[str, str]]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
收集完整的流式响应
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
str: 完整的响应文本
|
||||
"""
|
||||
full_response = ""
|
||||
async for content in self.get_stream_content(
|
||||
messages=messages, model=model, temperature=temperature, max_tokens=max_tokens, **kwargs
|
||||
):
|
||||
full_response += content
|
||||
|
||||
return full_response
|
||||
|
||||
async def close(self):
|
||||
"""关闭客户端"""
|
||||
await self.client.close()
|
||||
|
||||
async def __aenter__(self):
|
||||
"""异步上下文管理器入口"""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""异步上下文管理器退出"""
|
||||
await self.close()
|
||||
|
||||
|
||||
class ConversationManager:
|
||||
"""对话管理器,用于管理对话历史"""
|
||||
|
||||
def __init__(self, client: AsyncOpenAIClient, system_prompt: Optional[str] = None):
|
||||
"""
|
||||
初始化对话管理器
|
||||
|
||||
Args:
|
||||
client: OpenAI客户端实例
|
||||
system_prompt: 系统提示词
|
||||
"""
|
||||
self.client = client
|
||||
self.messages: List[ChatMessage] = []
|
||||
|
||||
if system_prompt:
|
||||
self.messages.append(ChatMessage(role="system", content=system_prompt))
|
||||
|
||||
def add_user_message(self, content: str):
|
||||
"""添加用户消息"""
|
||||
self.messages.append(ChatMessage(role="user", content=content))
|
||||
|
||||
def add_assistant_message(self, content: str):
|
||||
"""添加助手消息"""
|
||||
self.messages.append(ChatMessage(role="assistant", content=content))
|
||||
|
||||
async def send_message_stream(
|
||||
self, content: str, model: str = "gpt-3.5-turbo", **kwargs
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
发送消息并获取流式响应
|
||||
|
||||
Args:
|
||||
content: 用户消息内容
|
||||
model: 模型名称
|
||||
**kwargs: 其他参数
|
||||
|
||||
Yields:
|
||||
str: 响应内容片段
|
||||
"""
|
||||
self.add_user_message(content)
|
||||
|
||||
response_content = ""
|
||||
async for chunk in self.client.get_stream_content(messages=self.messages, model=model, **kwargs):
|
||||
response_content += chunk
|
||||
yield chunk
|
||||
|
||||
self.add_assistant_message(response_content)
|
||||
|
||||
async def send_message(self, content: str, model: str = "gpt-3.5-turbo", **kwargs) -> str:
|
||||
"""
|
||||
发送消息并获取完整响应
|
||||
|
||||
Args:
|
||||
content: 用户消息内容
|
||||
model: 模型名称
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
str: 完整响应
|
||||
"""
|
||||
self.add_user_message(content)
|
||||
|
||||
response = await self.client.chat_completion(messages=self.messages, model=model, **kwargs)
|
||||
|
||||
response_content = response.choices[0].message.content
|
||||
self.add_assistant_message(response_content)
|
||||
|
||||
return response_content
|
||||
|
||||
def clear_history(self, keep_system: bool = True):
|
||||
"""
|
||||
清除对话历史
|
||||
|
||||
Args:
|
||||
keep_system: 是否保留系统消息
|
||||
"""
|
||||
if keep_system and self.messages and self.messages[0].role == "system":
|
||||
self.messages = [self.messages[0]]
|
||||
else:
|
||||
self.messages = []
|
||||
|
||||
def get_message_count(self) -> int:
|
||||
"""获取消息数量"""
|
||||
return len(self.messages)
|
||||
|
||||
def get_conversation_history(self) -> List[Dict[str, str]]:
|
||||
"""获取对话历史"""
|
||||
return [msg.to_dict() for msg in self.messages]
|
||||
@@ -25,7 +25,7 @@ class RelationshipBuilderManager:
|
||||
"""
|
||||
if chat_id not in self.builders:
|
||||
self.builders[chat_id] = RelationshipBuilder(chat_id)
|
||||
logger.info(f"创建聊天 {chat_id} 的关系构建器")
|
||||
logger.debug(f"创建聊天 {chat_id} 的关系构建器")
|
||||
|
||||
return self.builders[chat_id]
|
||||
|
||||
@@ -51,7 +51,7 @@ class RelationshipBuilderManager:
|
||||
"""
|
||||
if chat_id in self.builders:
|
||||
del self.builders[chat_id]
|
||||
logger.info(f"移除聊天 {chat_id} 的关系构建器")
|
||||
logger.debug(f"移除聊天 {chat_id} 的关系构建器")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import List, Dict
|
||||
from json_repair import repair_json
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
import json
|
||||
|
||||
import random
|
||||
|
||||
logger = get_logger("relationship_fetcher")
|
||||
|
||||
@@ -23,7 +23,7 @@ def init_real_time_info_prompts():
|
||||
|
||||
{name_block}
|
||||
现在,你想要回复{person_name}的消息,消息内容是:{target_message}。请根据聊天记录和你要回复的消息,从你对{person_name}的了解中提取有关的信息:
|
||||
1.你需要提供你想要提取的信息具体是哪方面的信息,例如:年龄,性别,对ta的印象,最近发生的事等等。
|
||||
1.你需要提供你想要提取的信息具体是哪方面的信息,例如:年龄,性别,你们之间的交流方式,最近发生的事等等。
|
||||
2.请注意,请不要重复调取相同的信息,已经调取的信息如下:
|
||||
{info_cache_block}
|
||||
3.如果当前聊天记录中没有需要查询的信息,或者现有信息已经足够回复,请返回{{"none": "不需要查询"}}
|
||||
@@ -70,14 +70,14 @@ class RelationshipFetcher:
|
||||
|
||||
# LLM模型配置
|
||||
self.llm_model = LLMRequest(
|
||||
model=global_config.model.relation,
|
||||
request_type="focus.real_time_info",
|
||||
model=global_config.model.utils_small,
|
||||
request_type="relation.fetcher",
|
||||
)
|
||||
|
||||
# 小模型用于即时信息提取
|
||||
self.instant_llm_model = LLMRequest(
|
||||
model=global_config.model.utils_small,
|
||||
request_type="focus.real_time_info.instant",
|
||||
request_type="relation.fetch",
|
||||
)
|
||||
|
||||
name = get_chat_manager().get_stream_name(self.chat_id)
|
||||
@@ -101,12 +101,72 @@ class RelationshipFetcher:
|
||||
person_name = await person_info_manager.get_value(person_id, "person_name")
|
||||
short_impression = await person_info_manager.get_value(person_id, "short_impression")
|
||||
|
||||
nickname_str = await person_info_manager.get_value(person_id, "nickname")
|
||||
platform = await person_info_manager.get_value(person_id, "platform")
|
||||
|
||||
if person_name == nickname_str and not short_impression:
|
||||
return ""
|
||||
|
||||
current_points = await person_info_manager.get_value(person_id, "points") or []
|
||||
|
||||
if isinstance(current_points, str):
|
||||
try:
|
||||
current_points = json.loads(current_points)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"解析points JSON失败: {current_points}")
|
||||
current_points = []
|
||||
elif not isinstance(current_points, list):
|
||||
current_points = []
|
||||
|
||||
# 按时间排序forgotten_points
|
||||
current_points.sort(key=lambda x: x[2])
|
||||
# 按权重加权随机抽取3个points,point[1]的值在1-10之间,权重越高被抽到概率越大
|
||||
if len(current_points) > 3:
|
||||
# point[1] 取值范围1-10,直接作为权重
|
||||
weights = [max(1, min(10, int(point[1]))) for point in current_points]
|
||||
points = random.choices(current_points, weights=weights, k=3)
|
||||
else:
|
||||
points = current_points
|
||||
|
||||
# 构建points文本
|
||||
points_text = "\n".join([f"{point[2]}:{point[0]}" for point in points])
|
||||
|
||||
info_type = await self._build_fetch_query(person_id, target_message, chat_history)
|
||||
if info_type:
|
||||
await self._extract_single_info(person_id, info_type, person_name)
|
||||
|
||||
relation_info = self._organize_known_info()
|
||||
relation_info = f"你对{person_name}的印象是:{short_impression}\n{relation_info}"
|
||||
|
||||
nickname_str = ""
|
||||
if person_name != nickname_str:
|
||||
nickname_str = f"(ta在{platform}上的昵称是{nickname_str})"
|
||||
|
||||
if short_impression and relation_info:
|
||||
if points_text:
|
||||
relation_info = f"你对{person_name}的印象是{nickname_str}:{short_impression}。具体来说:{relation_info}。你还记得ta最近做的事:{points_text}"
|
||||
else:
|
||||
relation_info = (
|
||||
f"你对{person_name}的印象是{nickname_str}:{short_impression}。具体来说:{relation_info}"
|
||||
)
|
||||
elif short_impression:
|
||||
if points_text:
|
||||
relation_info = (
|
||||
f"你对{person_name}的印象是{nickname_str}:{short_impression}。你还记得ta最近做的事:{points_text}"
|
||||
)
|
||||
else:
|
||||
relation_info = f"你对{person_name}的印象是{nickname_str}:{short_impression}"
|
||||
elif relation_info:
|
||||
if points_text:
|
||||
relation_info = (
|
||||
f"你对{person_name}的了解{nickname_str}:{relation_info}。你还记得ta最近做的事:{points_text}"
|
||||
)
|
||||
else:
|
||||
relation_info = f"你对{person_name}的了解{nickname_str}:{relation_info}"
|
||||
elif points_text:
|
||||
relation_info = f"你记得{person_name}{nickname_str}最近做的事:{points_text}"
|
||||
else:
|
||||
relation_info = ""
|
||||
|
||||
return relation_info
|
||||
|
||||
async def _build_fetch_query(self, person_id, target_message, chat_history):
|
||||
@@ -134,7 +194,7 @@ class RelationshipFetcher:
|
||||
|
||||
# 检查是否返回了不需要查询的标志
|
||||
if "none" in content_json:
|
||||
logger.info(f"{self.log_prefix} LLM判断当前不需要查询任何信息:{content_json.get('none', '')}")
|
||||
logger.debug(f"{self.log_prefix} LLM判断当前不需要查询任何信息:{content_json.get('none', '')}")
|
||||
return None
|
||||
|
||||
info_type = content_json.get("info_type")
|
||||
|
||||
@@ -125,6 +125,30 @@ class RelationshipManager:
|
||||
return ""
|
||||
short_impression = await person_info_manager.get_value(person_id, "short_impression")
|
||||
|
||||
current_points = await person_info_manager.get_value(person_id, "points") or []
|
||||
print(f"current_points: {current_points}")
|
||||
if isinstance(current_points, str):
|
||||
try:
|
||||
current_points = json.loads(current_points)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"解析points JSON失败: {current_points}")
|
||||
current_points = []
|
||||
elif not isinstance(current_points, list):
|
||||
current_points = []
|
||||
|
||||
# 按时间排序forgotten_points
|
||||
current_points.sort(key=lambda x: x[2])
|
||||
# 按权重加权随机抽取3个points,point[1]的值在1-10之间,权重越高被抽到概率越大
|
||||
if len(current_points) > 3:
|
||||
# point[1] 取值范围1-10,直接作为权重
|
||||
weights = [max(1, min(10, int(point[1]))) for point in current_points]
|
||||
points = random.choices(current_points, weights=weights, k=3)
|
||||
else:
|
||||
points = current_points
|
||||
|
||||
# 构建points文本
|
||||
points_text = "\n".join([f"{point[2]}:{point[0]}\n" for point in points])
|
||||
|
||||
nickname_str = await person_info_manager.get_value(person_id, "nickname")
|
||||
platform = await person_info_manager.get_value(person_id, "platform")
|
||||
|
||||
@@ -137,7 +161,10 @@ class RelationshipManager:
|
||||
relation_prompt = f"'{person_name}' ,ta在{platform}上的昵称是{nickname_str}。"
|
||||
|
||||
if short_impression:
|
||||
relation_prompt += f"你对ta的印象是:{short_impression}。"
|
||||
relation_prompt += f"你对ta的印象是:{short_impression}。\n"
|
||||
|
||||
if points_text:
|
||||
relation_prompt += f"你记得ta最近做的事:{points_text}"
|
||||
|
||||
return relation_prompt
|
||||
|
||||
@@ -241,16 +268,16 @@ class RelationshipManager:
|
||||
"weight": 10
|
||||
}},
|
||||
{{
|
||||
"point": "我让{person_name}帮我写作业,他拒绝了",
|
||||
"weight": 4
|
||||
"point": "我让{person_name}帮我写化学作业,他拒绝了,我感觉他对我有意见,或者ta不喜欢我",
|
||||
"weight": 3
|
||||
}},
|
||||
{{
|
||||
"point": "{person_name}居然搞错了我的名字,生气了",
|
||||
"point": "{person_name}居然搞错了我的名字,我感到生气了,之后不理ta了",
|
||||
"weight": 8
|
||||
}},
|
||||
{{
|
||||
"point": "{person_name}喜欢吃辣,我和她关系不错",
|
||||
"weight": 8
|
||||
"point": "{person_name}喜欢吃辣,具体来说,没有辣的食物ta都不喜欢吃,可能是因为ta是湖南人。",
|
||||
"weight": 7
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -456,7 +483,7 @@ class RelationshipManager:
|
||||
你对{person_name}的了解是:
|
||||
{compressed_summary}
|
||||
|
||||
请你用一句话概括你对{person_name}的了解。突出:
|
||||
请你概括你对{person_name}的了解。突出:
|
||||
1.对{person_name}的直观印象
|
||||
2.{global_config.bot.nickname}与{person_name}的关系
|
||||
3.{person_name}的关键信息
|
||||
@@ -487,8 +514,8 @@ class RelationshipManager:
|
||||
2. **好感度 (liking_value)**: 0-100的整数,表示这些信息让你对ta的喜。
|
||||
- 0: 非常厌恶
|
||||
- 25: 有点反感
|
||||
- 50: 中立/无感
|
||||
- 75: 有点喜欢
|
||||
- 50: 中立/无感(或者文本中无法明显看出)
|
||||
- 75: 喜欢这个人
|
||||
- 100: 非常喜欢/开心对这个人
|
||||
|
||||
请严格按照json格式输出,不要有其他多余内容:
|
||||
|
||||
@@ -17,7 +17,6 @@ from src.common.logger import get_logger
|
||||
|
||||
# 导入依赖
|
||||
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
|
||||
from src.chat.focus_chat.info.obs_info import ObsInfo
|
||||
|
||||
logger = get_logger("chat_api")
|
||||
|
||||
@@ -193,39 +192,6 @@ class ChatManager:
|
||||
logger.error(f"[ChatAPI] 获取聊天流信息失败: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_recent_messages_from_obs(observations: List[Any], count: int = 5) -> List[Dict[str, Any]]:
|
||||
"""从观察对象获取最近的消息
|
||||
|
||||
Args:
|
||||
observations: 观察对象列表
|
||||
count: 要获取的消息数量
|
||||
|
||||
Returns:
|
||||
List[Dict]: 消息列表,每个消息包含发送者、内容等信息
|
||||
"""
|
||||
messages = []
|
||||
|
||||
try:
|
||||
if observations and len(observations) > 0:
|
||||
obs = observations[0]
|
||||
if hasattr(obs, "get_talking_message"):
|
||||
obs: ObsInfo
|
||||
raw_messages = obs.get_talking_message()
|
||||
# 转换为简化格式
|
||||
for msg in raw_messages[-count:]:
|
||||
simple_msg = {
|
||||
"sender": msg.get("sender", "未知"),
|
||||
"content": msg.get("content", ""),
|
||||
"timestamp": msg.get("timestamp", 0),
|
||||
}
|
||||
messages.append(simple_msg)
|
||||
logger.debug(f"[ChatAPI] 获取到 {len(messages)} 条最近消息")
|
||||
except Exception as e:
|
||||
logger.error(f"[ChatAPI] 获取最近消息失败: {e}")
|
||||
|
||||
return messages
|
||||
|
||||
@staticmethod
|
||||
def get_streams_summary() -> Dict[str, int]:
|
||||
"""获取聊天流统计摘要
|
||||
|
||||
@@ -374,7 +374,7 @@ async def store_action_info(
|
||||
)
|
||||
|
||||
if saved_record:
|
||||
logger.info(f"[DatabaseAPI] 成功存储动作信息: {action_name} (ID: {record_data['action_id']})")
|
||||
logger.debug(f"[DatabaseAPI] 成功存储动作信息: {action_name} (ID: {record_data['action_id']})")
|
||||
else:
|
||||
logger.error(f"[DatabaseAPI] 存储动作信息失败: {action_name}")
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]
|
||||
Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[EmojiAPI] 根据描述获取表情包: {description}")
|
||||
logger.debug(f"[EmojiAPI] 根据描述获取表情包: {description}")
|
||||
|
||||
emoji_manager = get_emoji_manager()
|
||||
emoji_result = await emoji_manager.get_emoji_for_text(description)
|
||||
@@ -47,7 +47,7 @@ async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]
|
||||
logger.error(f"[EmojiAPI] 无法将表情包文件转换为base64: {emoji_path}")
|
||||
return None
|
||||
|
||||
logger.info(f"[EmojiAPI] 成功获取表情包: {emoji_description}, 匹配情感: {matched_emotion}")
|
||||
logger.debug(f"[EmojiAPI] 成功获取表情包: {emoji_description}, 匹配情感: {matched_emotion}")
|
||||
return emoji_base64, emoji_description, matched_emotion
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -27,7 +27,6 @@ logger = get_logger("generator_api")
|
||||
def get_replyer(
|
||||
chat_stream: Optional[ChatStream] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
enable_tool: bool = False,
|
||||
model_configs: Optional[List[Dict[str, Any]]] = None,
|
||||
request_type: str = "replyer",
|
||||
) -> Optional[DefaultReplyer]:
|
||||
@@ -52,7 +51,6 @@ def get_replyer(
|
||||
chat_id=chat_id,
|
||||
model_configs=model_configs,
|
||||
request_type=request_type,
|
||||
enable_tool=enable_tool,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[GeneratorAPI] 获取回复器时发生意外错误: {e}", exc_info=True)
|
||||
@@ -70,7 +68,6 @@ async def generate_reply(
|
||||
chat_id: str = None,
|
||||
action_data: Dict[str, Any] = None,
|
||||
reply_to: str = "",
|
||||
relation_info: str = "",
|
||||
extra_info: str = "",
|
||||
available_actions: List[str] = None,
|
||||
enable_tool: bool = False,
|
||||
@@ -79,6 +76,7 @@ async def generate_reply(
|
||||
return_prompt: bool = False,
|
||||
model_configs: Optional[List[Dict[str, Any]]] = None,
|
||||
request_type: str = "",
|
||||
enable_timeout: bool = False,
|
||||
) -> Tuple[bool, List[Tuple[str, Any]]]:
|
||||
"""生成回复
|
||||
|
||||
@@ -94,28 +92,27 @@ async def generate_reply(
|
||||
"""
|
||||
try:
|
||||
# 获取回复器
|
||||
replyer = get_replyer(
|
||||
chat_stream, chat_id, model_configs=model_configs, request_type=request_type, enable_tool=enable_tool
|
||||
)
|
||||
replyer = get_replyer(chat_stream, chat_id, model_configs=model_configs, request_type=request_type)
|
||||
if not replyer:
|
||||
logger.error("[GeneratorAPI] 无法获取回复器")
|
||||
return False, []
|
||||
|
||||
logger.info("[GeneratorAPI] 开始生成回复")
|
||||
logger.debug("[GeneratorAPI] 开始生成回复")
|
||||
|
||||
# 调用回复器生成回复
|
||||
success, content, prompt = await replyer.generate_reply_with_context(
|
||||
reply_data=action_data or {},
|
||||
reply_to=reply_to,
|
||||
relation_info=relation_info,
|
||||
extra_info=extra_info,
|
||||
available_actions=available_actions,
|
||||
enable_timeout=enable_timeout,
|
||||
enable_tool=enable_tool,
|
||||
)
|
||||
|
||||
reply_set = await process_human_text(content, enable_splitter, enable_chinese_typo)
|
||||
|
||||
if success:
|
||||
logger.info(f"[GeneratorAPI] 回复生成成功,生成了 {len(reply_set)} 个回复项")
|
||||
logger.debug(f"[GeneratorAPI] 回复生成成功,生成了 {len(reply_set)} 个回复项")
|
||||
else:
|
||||
logger.warning("[GeneratorAPI] 回复生成失败")
|
||||
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
import traceback
|
||||
import time
|
||||
import difflib
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
from src.common.logger import get_logger
|
||||
|
||||
# 导入依赖
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.focus_chat.heartFC_sender import HeartFCSender
|
||||
from src.chat.message_receive.uni_message_sender import HeartFCSender
|
||||
from src.chat.message_receive.message import MessageSending, MessageRecv
|
||||
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
@@ -65,7 +66,7 @@ async def _send_to_target(
|
||||
bool: 是否发送成功
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[SendAPI] 发送{message_type}消息到 {stream_id}")
|
||||
logger.debug(f"[SendAPI] 发送{message_type}消息到 {stream_id}")
|
||||
|
||||
# 查找目标聊天流
|
||||
target_stream = get_chat_manager().get_stream(stream_id)
|
||||
@@ -115,7 +116,7 @@ async def _send_to_target(
|
||||
)
|
||||
|
||||
if sent_msg:
|
||||
logger.info(f"[SendAPI] 成功发送消息到 {stream_id}")
|
||||
logger.debug(f"[SendAPI] 成功发送消息到 {stream_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error("[SendAPI] 发送消息失败")
|
||||
@@ -171,7 +172,41 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR
|
||||
person_id = get_person_info_manager().get_person_id(platform, user_id)
|
||||
person_name = await get_person_info_manager().get_value(person_id, "person_name")
|
||||
if person_name == sender:
|
||||
similarity = difflib.SequenceMatcher(None, text, message["processed_plain_text"]).ratio()
|
||||
translate_text = message["processed_plain_text"]
|
||||
|
||||
# 检查是否有 回复<aaa:bbb> 字段
|
||||
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
|
||||
match = re.search(reply_pattern, translate_text)
|
||||
if match:
|
||||
aaa = match.group(1)
|
||||
bbb = match.group(2)
|
||||
reply_person_id = get_person_info_manager().get_person_id(platform, bbb)
|
||||
reply_person_name = await get_person_info_manager().get_value(reply_person_id, "person_name")
|
||||
if not reply_person_name:
|
||||
reply_person_name = aaa
|
||||
# 在内容前加上回复信息
|
||||
translate_text = re.sub(reply_pattern, f"回复 {reply_person_name}", translate_text, count=1)
|
||||
|
||||
# 检查是否有 @<aaa:bbb> 字段
|
||||
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
|
||||
at_matches = list(re.finditer(at_pattern, translate_text))
|
||||
if at_matches:
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for m in at_matches:
|
||||
new_content += translate_text[last_end : m.start()]
|
||||
aaa = m.group(1)
|
||||
bbb = m.group(2)
|
||||
at_person_id = get_person_info_manager().get_person_id(platform, bbb)
|
||||
at_person_name = await get_person_info_manager().get_value(at_person_id, "person_name")
|
||||
if not at_person_name:
|
||||
at_person_name = aaa
|
||||
new_content += f"@{at_person_name}"
|
||||
last_end = m.end()
|
||||
new_content += translate_text[last_end:]
|
||||
translate_text = new_content
|
||||
|
||||
similarity = difflib.SequenceMatcher(None, text, translate_text).ratio()
|
||||
if similarity >= 0.9:
|
||||
find_msg = message
|
||||
break
|
||||
|
||||
@@ -44,7 +44,6 @@ class BaseAction(ABC):
|
||||
reasoning: 执行该动作的理由
|
||||
cycle_timers: 计时器字典
|
||||
thinking_id: 思考ID
|
||||
observations: 观察列表
|
||||
expressor: 表达器对象
|
||||
replyer: 回复器对象
|
||||
chat_stream: 聊天流对象
|
||||
|
||||
84
src/plugins/built_in/core_actions/emoji.py
Normal file
84
src/plugins/built_in/core_actions/emoji.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from typing import Tuple
|
||||
|
||||
# 导入新插件系统
|
||||
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||
|
||||
# 导入依赖的系统组件
|
||||
from src.common.logger import get_logger
|
||||
|
||||
# 导入API模块 - 标准Python包方式
|
||||
from src.plugin_system.apis import emoji_api
|
||||
from src.plugins.built_in.core_actions.no_reply import NoReplyAction
|
||||
|
||||
|
||||
logger = get_logger("core_actions")
|
||||
|
||||
|
||||
class EmojiAction(BaseAction):
|
||||
"""表情动作 - 发送表情包"""
|
||||
|
||||
# 激活设置
|
||||
focus_activation_type = ActionActivationType.RANDOM
|
||||
normal_activation_type = ActionActivationType.RANDOM
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = True
|
||||
random_activation_probability = 0.2 # 默认值,可通过配置覆盖
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "emoji"
|
||||
action_description = "发送表情包辅助表达情绪"
|
||||
|
||||
# LLM判断提示词
|
||||
llm_judge_prompt = """
|
||||
判定是否需要使用表情动作的条件:
|
||||
1. 用户明确要求使用表情包
|
||||
2. 这是一个适合表达强烈情绪的场合
|
||||
3. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否"
|
||||
|
||||
请回答"是"或"否"。
|
||||
"""
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters = {"description": "文字描述你想要发送的表情包内容"}
|
||||
|
||||
# 动作使用场景
|
||||
action_require = [
|
||||
"发送表情包辅助表达情绪",
|
||||
"表达情绪时可以选择使用",
|
||||
"不要连续发送,如果你已经发过[表情包],就不要选择此动作",
|
||||
]
|
||||
|
||||
# 关联类型
|
||||
associated_types = ["emoji"]
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""执行表情动作"""
|
||||
logger.info(f"{self.log_prefix} 决定发送表情")
|
||||
|
||||
try:
|
||||
# 1. 根据描述选择表情包
|
||||
description = self.action_data.get("description", "")
|
||||
emoji_result = await emoji_api.get_by_description(description)
|
||||
|
||||
if not emoji_result:
|
||||
logger.warning(f"{self.log_prefix} 未找到匹配描述 '{description}' 的表情包")
|
||||
return False, f"未找到匹配 '{description}' 的表情包"
|
||||
|
||||
emoji_base64, emoji_description, matched_emotion = emoji_result
|
||||
logger.info(f"{self.log_prefix} 找到表情包: {emoji_description}, 匹配情感: {matched_emotion}")
|
||||
|
||||
# 使用BaseAction的便捷方法发送表情包
|
||||
success = await self.send_emoji(emoji_base64)
|
||||
|
||||
if not success:
|
||||
logger.error(f"{self.log_prefix} 表情包发送失败")
|
||||
return False, "表情包发送失败"
|
||||
|
||||
# 重置NoReplyAction的连续计数器
|
||||
NoReplyAction.reset_consecutive_count()
|
||||
|
||||
return True, f"发送表情包: {emoji_description}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}")
|
||||
return False, f"表情发送失败: {str(e)}"
|
||||
@@ -77,7 +77,7 @@ class NoReplyAction(BaseAction):
|
||||
|
||||
reason = self.action_data.get("reason", "")
|
||||
start_time = time.time()
|
||||
last_judge_time = 0 # 上次进行LLM判断的时间
|
||||
last_judge_time = start_time # 上次进行LLM判断的时间
|
||||
min_judge_interval = self._min_judge_interval # 最小判断间隔,从配置获取
|
||||
check_interval = 0.2 # 检查新消息的间隔,设为0.2秒提高响应性
|
||||
|
||||
@@ -107,43 +107,13 @@ class NoReplyAction(BaseAction):
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - start_time
|
||||
|
||||
if global_config.chat.chat_mode == "auto":
|
||||
if global_config.chat.chat_mode == "auto" and self.is_group:
|
||||
# 检查是否超时
|
||||
if elapsed_time >= self._max_timeout:
|
||||
logger.info(f"{self.log_prefix} 达到最大等待时间{self._max_timeout}秒,退出专注模式")
|
||||
if elapsed_time >= self._max_timeout or self._check_no_activity_and_exit_focus(current_time):
|
||||
logger.info(f"{self.log_prefix} 等待时间过久({self._max_timeout}秒)或过去10分钟完全没有发言,退出专注模式")
|
||||
# 标记退出专注模式
|
||||
self.action_data["_system_command"] = "stop_focus_chat"
|
||||
exit_reason = f"{global_config.bot.nickname}(你)等待了{self._max_timeout}秒,感觉群里没有新内容,决定退出专注模式,稍作休息"
|
||||
await self.store_action_info(
|
||||
action_build_into_prompt=True,
|
||||
action_prompt_display=exit_reason,
|
||||
action_done=True,
|
||||
)
|
||||
return True, exit_reason
|
||||
|
||||
# **新增**:检查回复频率,决定是否退出专注模式
|
||||
should_exit_focus = await self._check_frequency_and_exit_focus(current_time)
|
||||
if should_exit_focus:
|
||||
logger.info(f"{self.log_prefix} 检测到回复频率过高,退出专注模式")
|
||||
# 标记退出专注模式
|
||||
self.action_data["_system_command"] = "stop_focus_chat"
|
||||
exit_reason = (
|
||||
f"{global_config.bot.nickname}(你)发现自己回复太频繁了,决定退出专注模式,稍作休息"
|
||||
)
|
||||
await self.store_action_info(
|
||||
action_build_into_prompt=True,
|
||||
action_prompt_display=exit_reason,
|
||||
action_done=True,
|
||||
)
|
||||
return True, exit_reason
|
||||
|
||||
# **新增**:检查过去10分钟是否完全没有发言,如果是则退出专注模式
|
||||
should_exit_no_activity = await self._check_no_activity_and_exit_focus(current_time)
|
||||
if should_exit_no_activity:
|
||||
logger.info(f"{self.log_prefix} 检测到过去10分钟完全没有发言,退出专注模式")
|
||||
# 标记退出专注模式
|
||||
self.action_data["_system_command"] = "stop_focus_chat"
|
||||
exit_reason = f"{global_config.bot.nickname}(你)发现自己过去10分钟完全没有说话,感觉可能不太活跃,决定退出专注模式"
|
||||
exit_reason = f"{global_config.bot.nickname}(你)等待了{self._max_timeout}秒,或完全没有说话,感觉群里没有新内容,决定退出专注模式,稍作休息"
|
||||
await self.store_action_info(
|
||||
action_build_into_prompt=True,
|
||||
action_prompt_display=exit_reason,
|
||||
@@ -289,15 +259,17 @@ class NoReplyAction(BaseAction):
|
||||
frequency_block = ""
|
||||
|
||||
# 如果决定跳过LLM判断,直接更新时间并继续等待
|
||||
|
||||
if should_skip_llm_judge:
|
||||
last_judge_time = time.time() # 更新判断时间,避免立即重新判断
|
||||
continue # 跳过本次LLM判断,继续循环等待
|
||||
|
||||
# 构建判断上下文
|
||||
chat_context = "QQ群" if self.is_group else "私聊"
|
||||
judge_prompt = f"""
|
||||
{identity_block}
|
||||
|
||||
你现在正在QQ群参与聊天,以下是聊天内容:
|
||||
你现在正在{chat_context}参与聊天,以下是聊天内容:
|
||||
{context_str}
|
||||
在以上的聊天中,你选择了暂时不回复,现在,你看到了新的聊天消息如下:
|
||||
{messages_text}
|
||||
@@ -357,7 +329,7 @@ class NoReplyAction(BaseAction):
|
||||
judge_history.append((current_time, judge_result, reason))
|
||||
|
||||
if judge_result == "需要回复":
|
||||
logger.info(f"{self.log_prefix} 模型判断需要回复,结束等待")
|
||||
# logger.info(f"{self.log_prefix} 模型判断需要回复,结束等待")
|
||||
|
||||
full_prompt = f"{global_config.bot.nickname}(你)的想法是:{reason}"
|
||||
await self.store_action_info(
|
||||
@@ -380,14 +352,17 @@ class NoReplyAction(BaseAction):
|
||||
logger.error(f"{self.log_prefix} 模型判断异常: {e},继续等待")
|
||||
last_judge_time = time.time() # 异常时也更新时间,避免频繁重试
|
||||
|
||||
|
||||
|
||||
# 每10秒输出一次等待状态
|
||||
logger.info(f"{self.log_prefix} 开始等待新消息...")
|
||||
if elapsed_time < 60:
|
||||
if int(elapsed_time) % 10 == 0 and int(elapsed_time) > 0:
|
||||
logger.info(f"{self.log_prefix} 已等待{elapsed_time:.0f}秒,等待新消息...")
|
||||
logger.debug(f"{self.log_prefix} 已等待{elapsed_time:.0f}秒,等待新消息...")
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
if int(elapsed_time) % 60 == 0 and int(elapsed_time) > 0:
|
||||
logger.info(f"{self.log_prefix} 已等待{elapsed_time / 60:.0f}分钟,等待新消息...")
|
||||
logger.debug(f"{self.log_prefix} 已等待{elapsed_time / 60:.0f}分钟,等待新消息...")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 短暂等待后继续检查
|
||||
@@ -405,65 +380,7 @@ class NoReplyAction(BaseAction):
|
||||
)
|
||||
return False, f"不回复动作执行失败: {e}"
|
||||
|
||||
async def _check_frequency_and_exit_focus(self, current_time: float) -> bool:
|
||||
"""检查回复频率,决定是否退出专注模式
|
||||
|
||||
Args:
|
||||
current_time: 当前时间戳
|
||||
|
||||
Returns:
|
||||
bool: 是否应该退出专注模式
|
||||
"""
|
||||
try:
|
||||
# 只在auto模式下进行频率检查
|
||||
if global_config.chat.chat_mode != "auto":
|
||||
return False
|
||||
|
||||
# 获取检查窗口内的所有消息
|
||||
window_start_time = current_time - self._frequency_check_window
|
||||
all_messages = message_api.get_messages_by_time_in_chat(
|
||||
chat_id=self.chat_id,
|
||||
start_time=window_start_time,
|
||||
end_time=current_time,
|
||||
)
|
||||
|
||||
if not all_messages:
|
||||
return False
|
||||
|
||||
# 统计bot自己的回复数量
|
||||
bot_message_count = 0
|
||||
user_id = global_config.bot.qq_account
|
||||
|
||||
for message in all_messages:
|
||||
sender_id = message.get("user_id", "")
|
||||
if sender_id == user_id:
|
||||
bot_message_count += 1
|
||||
|
||||
# 计算当前回复频率(每分钟回复数)
|
||||
window_minutes = self._frequency_check_window / 60
|
||||
current_frequency = bot_message_count / window_minutes
|
||||
|
||||
# 计算阈值频率:使用 exit_focus_threshold * 1.5
|
||||
threshold_multiplier = global_config.chat.exit_focus_threshold * 1.5
|
||||
threshold_frequency = global_config.chat.get_current_talk_frequency(self.chat_id) * threshold_multiplier
|
||||
|
||||
# 判断是否超过阈值
|
||||
if current_frequency > threshold_frequency:
|
||||
logger.info(
|
||||
f"{self.log_prefix} 回复频率检查:当前频率 {current_frequency:.2f}/分钟,超过阈值 {threshold_frequency:.2f}/分钟 (exit_threshold={global_config.chat.exit_focus_threshold} * 1.5),准备退出专注模式"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.debug(
|
||||
f"{self.log_prefix} 回复频率检查:当前频率 {current_frequency:.2f}/分钟,未超过阈值 {threshold_frequency:.2f}/分钟 (exit_threshold={global_config.chat.exit_focus_threshold} * 1.5)"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 检查回复频率时出错: {e}")
|
||||
return False
|
||||
|
||||
async def _check_no_activity_and_exit_focus(self, current_time: float) -> bool:
|
||||
def _check_no_activity_and_exit_focus(self, current_time: float) -> bool:
|
||||
"""检查过去10分钟是否完全没有发言,决定是否退出专注模式
|
||||
|
||||
Args:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import random
|
||||
import time
|
||||
from typing import List, Tuple, Type
|
||||
import asyncio
|
||||
|
||||
# 导入新插件系统
|
||||
from src.plugin_system import BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode
|
||||
@@ -18,8 +19,9 @@ from src.config.config import global_config
|
||||
from src.common.logger import get_logger
|
||||
|
||||
# 导入API模块 - 标准Python包方式
|
||||
from src.plugin_system.apis import emoji_api, generator_api, message_api
|
||||
from src.plugin_system.apis import generator_api, message_api
|
||||
from src.plugins.built_in.core_actions.no_reply import NoReplyAction
|
||||
from src.plugins.built_in.core_actions.emoji import EmojiAction
|
||||
|
||||
logger = get_logger("core_actions")
|
||||
|
||||
@@ -54,17 +56,24 @@ class ReplyAction(BaseAction):
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""执行回复动作"""
|
||||
logger.info(f"{self.log_prefix} 决定回复: {self.reasoning}")
|
||||
logger.info(f"{self.log_prefix} 决定进行回复")
|
||||
|
||||
start_time = self.action_data.get("loop_start_time", time.time())
|
||||
|
||||
try:
|
||||
success, reply_set = await generator_api.generate_reply(
|
||||
try:
|
||||
success, reply_set = await asyncio.wait_for(
|
||||
generator_api.generate_reply(
|
||||
action_data=self.action_data,
|
||||
chat_id=self.chat_id,
|
||||
request_type="focus.replyer",
|
||||
enable_tool=global_config.tool.enable_in_focus_chat,
|
||||
),
|
||||
timeout=global_config.chat.thinking_timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"{self.log_prefix} 回复生成超时 ({global_config.chat.thinking_timeout}s)")
|
||||
return False, "timeout"
|
||||
|
||||
# 检查从start_time以来的新消息数量
|
||||
# 获取动作触发时间或使用默认值
|
||||
@@ -76,7 +85,7 @@ class ReplyAction(BaseAction):
|
||||
# 根据新消息数量决定是否使用reply_to
|
||||
need_reply = new_message_count >= random.randint(2, 5)
|
||||
logger.info(
|
||||
f"{self.log_prefix} 从{start_time}到{current_time}共有{new_message_count}条新消息,{'使用' if need_reply else '不使用'}reply_to"
|
||||
f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,{'使用' if need_reply else '不使用'}引用回复"
|
||||
)
|
||||
|
||||
# 构建回复文本
|
||||
@@ -112,72 +121,6 @@ class ReplyAction(BaseAction):
|
||||
return False, f"回复失败: {str(e)}"
|
||||
|
||||
|
||||
class EmojiAction(BaseAction):
|
||||
"""表情动作 - 发送表情包"""
|
||||
|
||||
# 激活设置
|
||||
focus_activation_type = ActionActivationType.LLM_JUDGE
|
||||
normal_activation_type = ActionActivationType.RANDOM
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = True
|
||||
random_activation_probability = 0.2 # 默认值,可通过配置覆盖
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "emoji"
|
||||
action_description = "发送表情包辅助表达情绪"
|
||||
|
||||
# LLM判断提示词
|
||||
llm_judge_prompt = """
|
||||
判定是否需要使用表情动作的条件:
|
||||
1. 用户明确要求使用表情包
|
||||
2. 这是一个适合表达强烈情绪的场合
|
||||
3. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否"
|
||||
|
||||
请回答"是"或"否"。
|
||||
"""
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters = {"description": "文字描述你想要发送的表情包内容"}
|
||||
|
||||
# 动作使用场景
|
||||
action_require = ["表达情绪时可以选择使用", "重点:不要连续发,如果你已经发过[表情包],就不要选择此动作"]
|
||||
|
||||
# 关联类型
|
||||
associated_types = ["emoji"]
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""执行表情动作"""
|
||||
logger.info(f"{self.log_prefix} 决定发送表情")
|
||||
|
||||
try:
|
||||
# 1. 根据描述选择表情包
|
||||
description = self.action_data.get("description", "")
|
||||
emoji_result = await emoji_api.get_by_description(description)
|
||||
|
||||
if not emoji_result:
|
||||
logger.warning(f"{self.log_prefix} 未找到匹配描述 '{description}' 的表情包")
|
||||
return False, f"未找到匹配 '{description}' 的表情包"
|
||||
|
||||
emoji_base64, emoji_description, matched_emotion = emoji_result
|
||||
logger.info(f"{self.log_prefix} 找到表情包: {emoji_description}, 匹配情感: {matched_emotion}")
|
||||
|
||||
# 使用BaseAction的便捷方法发送表情包
|
||||
success = await self.send_emoji(emoji_base64)
|
||||
|
||||
if not success:
|
||||
logger.error(f"{self.log_prefix} 表情包发送失败")
|
||||
return False, "表情包发送失败"
|
||||
|
||||
# 重置NoReplyAction的连续计数器
|
||||
NoReplyAction.reset_consecutive_count()
|
||||
|
||||
return True, f"发送表情包: {emoji_description}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}")
|
||||
return False, f"表情发送失败: {str(e)}"
|
||||
|
||||
|
||||
@register_plugin
|
||||
class CoreActionsPlugin(BasePlugin):
|
||||
"""核心动作插件
|
||||
@@ -206,14 +149,12 @@ class CoreActionsPlugin(BasePlugin):
|
||||
config_schema = {
|
||||
"plugin": {
|
||||
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
|
||||
"config_version": ConfigField(type=str, default="0.1.0", description="配置文件版本"),
|
||||
"config_version": ConfigField(type=str, default="0.3.1", description="配置文件版本"),
|
||||
},
|
||||
"components": {
|
||||
"enable_reply": ConfigField(type=bool, default=True, description="是否启用'回复'动作"),
|
||||
"enable_no_reply": ConfigField(type=bool, default=True, description="是否启用'不回复'动作"),
|
||||
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用'表情'动作"),
|
||||
"enable_change_to_focus": ConfigField(type=bool, default=True, description="是否启用'切换到专注模式'动作"),
|
||||
"enable_exit_focus": ConfigField(type=bool, default=True, description="是否启用'退出专注模式'动作"),
|
||||
},
|
||||
"no_reply": {
|
||||
"max_timeout": ConfigField(type=int, default=1200, description="最大等待超时时间(秒)"),
|
||||
@@ -239,8 +180,15 @@ class CoreActionsPlugin(BasePlugin):
|
||||
"""返回插件包含的组件列表"""
|
||||
|
||||
# --- 从配置动态设置Action/Command ---
|
||||
emoji_chance = global_config.normal_chat.emoji_chance
|
||||
emoji_chance = global_config.emoji.emoji_chance
|
||||
if global_config.emoji.emoji_activate_type == "random":
|
||||
EmojiAction.random_activation_probability = emoji_chance
|
||||
EmojiAction.focus_activation_type = ActionActivationType.RANDOM
|
||||
EmojiAction.normal_activation_type = ActionActivationType.RANDOM
|
||||
elif global_config.emoji.emoji_activate_type == "llm":
|
||||
EmojiAction.random_activation_probability = 0.0
|
||||
EmojiAction.focus_activation_type = ActionActivationType.LLM_JUDGE
|
||||
EmojiAction.normal_activation_type = ActionActivationType.LLM_JUDGE
|
||||
|
||||
no_reply_probability = self.get_config("no_reply.random_probability", 0.8)
|
||||
NoReplyAction.random_activation_probability = no_reply_probability
|
||||
@@ -273,127 +221,3 @@ class CoreActionsPlugin(BasePlugin):
|
||||
# components.append((DeepReplyAction.get_action_info(), DeepReplyAction))
|
||||
|
||||
return components
|
||||
|
||||
|
||||
# class DeepReplyAction(BaseAction):
|
||||
# """回复动作 - 参与聊天回复"""
|
||||
|
||||
# # 激活设置
|
||||
# focus_activation_type = ActionActivationType.ALWAYS
|
||||
# normal_activation_type = ActionActivationType.NEVER
|
||||
# mode_enable = ChatMode.FOCUS
|
||||
# parallel_action = False
|
||||
|
||||
# # 动作基本信息
|
||||
# action_name = "deep_reply"
|
||||
# action_description = "参与聊天回复,关注某个话题,对聊天内容进行深度思考,给出回复"
|
||||
|
||||
# # 动作参数定义
|
||||
# action_parameters = {
|
||||
# "topic": "想要思考的话题"
|
||||
# }
|
||||
|
||||
# # 动作使用场景
|
||||
# action_require = ["有些问题需要深度思考", "某个问题可能涉及多个方面", "某个问题涉及专业领域或者需要专业知识","这个问题讨论的很激烈,需要深度思考"]
|
||||
|
||||
# # 关联类型
|
||||
# associated_types = ["text"]
|
||||
|
||||
# async def execute(self) -> Tuple[bool, str]:
|
||||
# """执行回复动作"""
|
||||
# logger.info(f"{self.log_prefix} 决定深度思考")
|
||||
|
||||
# try:
|
||||
# # 获取聊天观察
|
||||
# chatting_observation = self._get_chatting_observation()
|
||||
# if not chatting_observation:
|
||||
# return False, "未找到聊天观察"
|
||||
|
||||
# talking_message_str = chatting_observation.talking_message_str
|
||||
|
||||
# # 处理回复目标
|
||||
# chat_stream = self.api.get_service("chat_stream")
|
||||
# anchor_message = await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
|
||||
|
||||
|
||||
# llm_model = self.api.get_available_models().replyer_1
|
||||
|
||||
# prompt = f"""
|
||||
# {talking_message_str}
|
||||
|
||||
# 在上面的聊天中,你对{self.action_data.get("topic", "")}感兴趣,形成深刻观点,请你思考,总结成一份学术论文,APA标准格式
|
||||
# """
|
||||
|
||||
# success, response, reasoning, model_name = await self.api.generate_with_model(prompt, llm_model)
|
||||
|
||||
# print(prompt)
|
||||
# print(f"DeepReplyAction: {response}")
|
||||
|
||||
# # prompt = f"""
|
||||
# # {talking_message_str}
|
||||
|
||||
# # 在上面的聊天中,你对{self.action_data.get("topic", "")}感兴趣,请你思考
|
||||
# # """
|
||||
|
||||
# extra_info_block = self.action_data.get("extra_info_block", "")
|
||||
# extra_info_block += response
|
||||
# # extra_info_block += f"\n--------------------------------\n注意,这是最重要的内容!!!!!你现在可以用比较长的篇幅来表达你的观点,不要只回复一个字或者几个字\n由于你进入了深度思考模式,所以下方聊天记录的回复要求不再适用,请你自由的表达,不论字数长短限制\n\n--------------------------------\n注意,你现在可以用比较长的篇幅来表达你的观点,不要只回复一个字或者几个字\n由于你进入了深度思考模式,所以下方聊天记录的回复要求不再适用,请你自由的表达,不论字数长短限制\n"
|
||||
# # extra_info_block += f"\n--------------------------------\n注意,优先关注这句!!!!你现在可以用比较长的篇幅来表达你的观点,不要只回复一个字或者几个字\n由于你进入了深度思考模式,所以下方聊天记录的回复要求不再适用,请你自由的表达,不论字数长短限制\n\n--------------------------------\n注意,你现在可以用比较长的篇幅来表达你的观点,不要只回复一个字或者几个字\n由于你进入了深度思考模式,所以其他的回复要求不再适用,请你自由的表达,不论字数长短限制\n"
|
||||
# self.action_data["extra_info_block"] = extra_info_block
|
||||
|
||||
|
||||
# # 获取回复器服务
|
||||
# # replyer = self.api.get_service("replyer")
|
||||
# # if not replyer:
|
||||
# # logger.error(f"{self.log_prefix} 未找到回复器服务")
|
||||
# # return False, "回复器服务不可用"
|
||||
|
||||
# # await self.send_message_by_expressor(extra_info_block)
|
||||
# await self.send_text(extra_info_block)
|
||||
# # 执行回复
|
||||
# # success, reply_set = await replyer.deal_reply(
|
||||
# # cycle_timers=self.cycle_timers,
|
||||
# # action_data=self.action_data,
|
||||
# # anchor_message=anchor_message,
|
||||
# # reasoning=self.reasoning,
|
||||
# # thinking_id=self.thinking_id,
|
||||
# # )
|
||||
|
||||
# # 构建回复文本
|
||||
# reply_text = "self._build_reply_text(reply_set)"
|
||||
|
||||
# # 存储动作记录
|
||||
# await self.api.store_action_info(
|
||||
# action_build_into_prompt=False,
|
||||
# action_prompt_display=reply_text,
|
||||
# action_done=True,
|
||||
# thinking_id=self.thinking_id,
|
||||
# action_data=self.action_data,
|
||||
# )
|
||||
|
||||
# # 重置NoReplyAction的连续计数器
|
||||
# NoReplyAction.reset_consecutive_count()
|
||||
|
||||
# return success, reply_text
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"{self.log_prefix} 回复动作执行失败: {e}")
|
||||
# return False, f"回复失败: {str(e)}"
|
||||
|
||||
# def _get_chatting_observation(self) -> Optional[ChattingObservation]:
|
||||
# """获取聊天观察对象"""
|
||||
# observations = self.api.get_service("observations") or []
|
||||
# for obs in observations:
|
||||
# if isinstance(obs, ChattingObservation):
|
||||
# return obs
|
||||
# return None
|
||||
|
||||
|
||||
# def _build_reply_text(self, reply_set) -> str:
|
||||
# """构建回复文本"""
|
||||
# reply_text = ""
|
||||
# if reply_set:
|
||||
# for reply in reply_set:
|
||||
# data = reply[1]
|
||||
# reply_text += data
|
||||
# return reply_text
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "豆包图片生成插件 (Doubao Image Generator)",
|
||||
"version": "2.0.0",
|
||||
"description": "基于火山引擎豆包模型的AI图片生成插件,支持智能LLM判定、高质量图片生成、结果缓存和多尺寸支持。",
|
||||
"author": {
|
||||
"name": "MaiBot团队",
|
||||
"url": "https://github.com/MaiM-with-u"
|
||||
},
|
||||
"license": "GPL-v3.0-or-later",
|
||||
|
||||
"host_application": {
|
||||
"min_version": "0.8.0",
|
||||
"max_version": "0.8.10"
|
||||
},
|
||||
"homepage_url": "https://github.com/MaiM-with-u/maibot",
|
||||
"repository_url": "https://github.com/MaiM-with-u/maibot",
|
||||
"keywords": ["ai", "image", "generation", "doubao", "volcengine", "art"],
|
||||
"categories": ["AI Tools", "Image Processing", "Content Generation"],
|
||||
|
||||
"default_locale": "zh-CN",
|
||||
"locales_path": "_locales",
|
||||
|
||||
"plugin_info": {
|
||||
"is_built_in": true,
|
||||
"plugin_type": "content_generator",
|
||||
"api_dependencies": ["volcengine"],
|
||||
"components": [
|
||||
{
|
||||
"type": "action",
|
||||
"name": "doubao_image_generation",
|
||||
"description": "根据描述使用火山引擎豆包API生成高质量图片",
|
||||
"activation_modes": ["llm_judge", "keyword"],
|
||||
"keywords": ["画", "图片", "生成", "画画", "绘制"]
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"智能LLM判定生成时机",
|
||||
"高质量AI图片生成",
|
||||
"结果缓存机制",
|
||||
"多种图片尺寸支持",
|
||||
"完整的错误处理"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
"""
|
||||
豆包图片生成插件
|
||||
|
||||
基于火山引擎豆包模型的AI图片生成插件。
|
||||
|
||||
功能特性:
|
||||
- 智能LLM判定:根据聊天内容智能判断是否需要生成图片
|
||||
- 高质量图片生成:使用豆包Seed Dream模型生成图片
|
||||
- 结果缓存:避免重复生成相同内容的图片
|
||||
- 配置验证:自动验证和修复配置文件
|
||||
- 参数验证:完整的输入参数验证和错误处理
|
||||
- 多尺寸支持:支持多种图片尺寸生成
|
||||
|
||||
包含组件:
|
||||
- 图片生成Action - 根据描述使用火山引擎API生成图片
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import base64
|
||||
import traceback
|
||||
from typing import List, Tuple, Type, Optional
|
||||
|
||||
# 导入新插件系统
|
||||
from src.plugin_system.base.base_plugin import BasePlugin
|
||||
from src.plugin_system.base.base_plugin import register_plugin
|
||||
from src.plugin_system.base.base_action import BaseAction
|
||||
from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode
|
||||
from src.plugin_system.base.config_types import ConfigField
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("doubao_pic_plugin")
|
||||
|
||||
|
||||
# ===== Action组件 =====
|
||||
|
||||
|
||||
class DoubaoImageGenerationAction(BaseAction):
|
||||
"""豆包图片生成Action - 根据描述使用火山引擎API生成图片"""
|
||||
|
||||
# 激活设置
|
||||
focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定,精确理解需求
|
||||
normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活,快速响应
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = True
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "doubao_image_generation"
|
||||
action_description = (
|
||||
"可以根据特定的描述,生成并发送一张图片,如果没提供描述,就根据聊天内容生成,你可以立刻画好,不用等待"
|
||||
)
|
||||
|
||||
# 关键词设置(用于Normal模式)
|
||||
activation_keywords = ["画", "绘制", "生成图片", "画图", "draw", "paint", "图片生成"]
|
||||
keyword_case_sensitive = False
|
||||
|
||||
# LLM判定提示词(用于Focus模式)
|
||||
llm_judge_prompt = """
|
||||
判定是否需要使用图片生成动作的条件:
|
||||
1. 用户明确要求画图、生成图片或创作图像
|
||||
2. 用户描述了想要看到的画面或场景
|
||||
3. 对话中提到需要视觉化展示某些概念
|
||||
4. 用户想要创意图片或艺术作品
|
||||
|
||||
适合使用的情况:
|
||||
- "画一张..."、"画个..."、"生成图片"
|
||||
- "我想看看...的样子"
|
||||
- "能画出...吗"
|
||||
- "创作一幅..."
|
||||
|
||||
绝对不要使用的情况:
|
||||
1. 纯文字聊天和问答
|
||||
2. 只是提到"图片"、"画"等词但不是要求生成
|
||||
3. 谈论已存在的图片或照片
|
||||
4. 技术讨论中提到绘图概念但无生成需求
|
||||
5. 用户明确表示不需要图片时
|
||||
"""
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters = {
|
||||
"description": "图片描述,输入你想要生成并发送的图片的描述,必填",
|
||||
"size": "图片尺寸,例如 '1024x1024' (可选, 默认从配置或 '1024x1024')",
|
||||
}
|
||||
|
||||
# 动作使用场景
|
||||
action_require = [
|
||||
"当有人让你画东西时使用,你可以立刻画好,不用等待",
|
||||
"当有人要求你生成并发送一张图片时使用",
|
||||
"当有人让你画一张图时使用",
|
||||
]
|
||||
|
||||
# 关联类型
|
||||
associated_types = ["image", "text"]
|
||||
|
||||
# 简单的请求缓存,避免短时间内重复请求
|
||||
_request_cache = {}
|
||||
_cache_max_size = 10
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
"""执行图片生成动作"""
|
||||
logger.info(f"{self.log_prefix} 执行豆包图片生成动作")
|
||||
|
||||
# 配置验证
|
||||
http_base_url = self.api.get_config("api.base_url")
|
||||
http_api_key = self.api.get_config("api.volcano_generate_api_key")
|
||||
|
||||
if not (http_base_url and http_api_key):
|
||||
error_msg = "抱歉,图片生成功能所需的HTTP配置(如API地址或密钥)不完整,无法提供服务。"
|
||||
await self.send_text(error_msg)
|
||||
logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.")
|
||||
return False, "HTTP配置不完整"
|
||||
|
||||
# API密钥验证
|
||||
if http_api_key == "YOUR_DOUBAO_API_KEY_HERE":
|
||||
error_msg = "图片生成功能尚未配置,请设置正确的API密钥。"
|
||||
await self.send_text(error_msg)
|
||||
logger.error(f"{self.log_prefix} API密钥未配置")
|
||||
return False, "API密钥未配置"
|
||||
|
||||
# 参数验证
|
||||
description = self.action_data.get("description")
|
||||
if not description or not description.strip():
|
||||
logger.warning(f"{self.log_prefix} 图片描述为空,无法生成图片。")
|
||||
await self.send_text("你需要告诉我想要画什么样的图片哦~ 比如说'画一只可爱的小猫'")
|
||||
return False, "图片描述为空"
|
||||
|
||||
# 清理和验证描述
|
||||
description = description.strip()
|
||||
if len(description) > 1000: # 限制描述长度
|
||||
description = description[:1000]
|
||||
logger.info(f"{self.log_prefix} 图片描述过长,已截断")
|
||||
|
||||
# 获取配置
|
||||
default_model = self.api.get_config("generation.default_model", "doubao-seedream-3-0-t2i-250415")
|
||||
image_size = self.action_data.get("size", self.api.get_config("generation.default_size", "1024x1024"))
|
||||
|
||||
# 验证图片尺寸格式
|
||||
if not self._validate_image_size(image_size):
|
||||
logger.warning(f"{self.log_prefix} 无效的图片尺寸: {image_size},使用默认值")
|
||||
image_size = "1024x1024"
|
||||
|
||||
# 检查缓存
|
||||
cache_key = self._get_cache_key(description, default_model, image_size)
|
||||
if cache_key in self._request_cache:
|
||||
cached_result = self._request_cache[cache_key]
|
||||
logger.info(f"{self.log_prefix} 使用缓存的图片结果")
|
||||
await self.send_text("我之前画过类似的图片,用之前的结果~")
|
||||
|
||||
# 直接发送缓存的结果
|
||||
send_success = await self._send_image(cached_result)
|
||||
if send_success:
|
||||
await self.send_text("图片已发送!")
|
||||
return True, "图片已发送(缓存)"
|
||||
else:
|
||||
# 缓存失败,清除这个缓存项并继续正常流程
|
||||
del self._request_cache[cache_key]
|
||||
|
||||
# 获取其他配置参数
|
||||
guidance_scale_val = self._get_guidance_scale()
|
||||
seed_val = self._get_seed()
|
||||
watermark_val = self._get_watermark()
|
||||
|
||||
await self.send_text(
|
||||
f"收到!正在为您生成关于 '{description}' 的图片,请稍候...(模型: {default_model}, 尺寸: {image_size})"
|
||||
)
|
||||
|
||||
try:
|
||||
success, result = await asyncio.to_thread(
|
||||
self._make_http_image_request,
|
||||
prompt=description,
|
||||
model=default_model,
|
||||
size=image_size,
|
||||
seed=seed_val,
|
||||
guidance_scale=guidance_scale_val,
|
||||
watermark=watermark_val,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} (HTTP) 异步请求执行失败: {e!r}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
success = False
|
||||
result = f"图片生成服务遇到意外问题: {str(e)[:100]}"
|
||||
|
||||
if success:
|
||||
image_url = result
|
||||
# print(f"image_url: {image_url}")
|
||||
# print(f"result: {result}")
|
||||
logger.info(f"{self.log_prefix} 图片URL获取成功: {image_url[:70]}... 下载并编码.")
|
||||
|
||||
try:
|
||||
encode_success, encode_result = await asyncio.to_thread(self._download_and_encode_base64, image_url)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} (B64) 异步下载/编码失败: {e!r}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
encode_success = False
|
||||
encode_result = f"图片下载或编码时发生内部错误: {str(e)[:100]}"
|
||||
|
||||
if encode_success:
|
||||
base64_image_string = encode_result
|
||||
send_success = await self._send_image(base64_image_string)
|
||||
if send_success:
|
||||
# 缓存成功的结果
|
||||
self._request_cache[cache_key] = base64_image_string
|
||||
self._cleanup_cache()
|
||||
|
||||
await self.send_message_by_expressor("图片已发送!")
|
||||
return True, "图片已成功生成并发送"
|
||||
else:
|
||||
print(f"send_success: {send_success}")
|
||||
await self.send_message_by_expressor("图片已处理为Base64,但发送失败了。")
|
||||
return False, "图片发送失败 (Base64)"
|
||||
else:
|
||||
await self.send_message_by_expressor(f"获取到图片URL,但在处理图片时失败了:{encode_result}")
|
||||
return False, f"图片处理失败(Base64): {encode_result}"
|
||||
else:
|
||||
error_message = result
|
||||
await self.send_message_by_expressor(f"哎呀,生成图片时遇到问题:{error_message}")
|
||||
return False, f"图片生成失败: {error_message}"
|
||||
|
||||
def _get_guidance_scale(self) -> float:
|
||||
"""获取guidance_scale配置值"""
|
||||
guidance_scale_input = self.api.get_config("generation.default_guidance_scale", 2.5)
|
||||
try:
|
||||
return float(guidance_scale_input)
|
||||
except (ValueError, TypeError):
|
||||
logger.warning(f"{self.log_prefix} default_guidance_scale 值无效,使用默认值 2.5")
|
||||
return 2.5
|
||||
|
||||
def _get_seed(self) -> int:
|
||||
"""获取seed配置值"""
|
||||
seed_config_value = self.api.get_config("generation.default_seed")
|
||||
if seed_config_value is not None:
|
||||
try:
|
||||
return int(seed_config_value)
|
||||
except (ValueError, TypeError):
|
||||
logger.warning(f"{self.log_prefix} default_seed 值无效,使用默认值 42")
|
||||
return 42
|
||||
|
||||
def _get_watermark(self) -> bool:
|
||||
"""获取watermark配置值"""
|
||||
watermark_source = self.api.get_config("generation.default_watermark", True)
|
||||
if isinstance(watermark_source, bool):
|
||||
return watermark_source
|
||||
elif isinstance(watermark_source, str):
|
||||
return watermark_source.lower() == "true"
|
||||
else:
|
||||
logger.warning(f"{self.log_prefix} default_watermark 值无效,使用默认值 True")
|
||||
return True
|
||||
|
||||
async def _send_image(self, base64_image: str) -> bool:
|
||||
"""发送图片"""
|
||||
try:
|
||||
# 使用聊天流信息确定发送目标
|
||||
chat_stream = self.api.get_service("chat_stream")
|
||||
if not chat_stream:
|
||||
logger.error(f"{self.log_prefix} 没有可用的聊天流发送图片")
|
||||
return False
|
||||
|
||||
if chat_stream.group_info:
|
||||
# 群聊
|
||||
return await self.api.send_message_to_target(
|
||||
message_type="image",
|
||||
content=base64_image,
|
||||
platform=chat_stream.platform,
|
||||
target_id=str(chat_stream.group_info.group_id),
|
||||
is_group=True,
|
||||
display_message="发送生成的图片",
|
||||
)
|
||||
else:
|
||||
# 私聊
|
||||
return await self.api.send_message_to_target(
|
||||
message_type="image",
|
||||
content=base64_image,
|
||||
platform=chat_stream.platform,
|
||||
target_id=str(chat_stream.user_info.user_id),
|
||||
is_group=False,
|
||||
display_message="发送生成的图片",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 发送图片时出错: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _get_cache_key(cls, description: str, model: str, size: str) -> str:
|
||||
"""生成缓存键"""
|
||||
return f"{description[:100]}|{model}|{size}"
|
||||
|
||||
@classmethod
|
||||
def _cleanup_cache(cls):
|
||||
"""清理缓存,保持大小在限制内"""
|
||||
if len(cls._request_cache) > cls._cache_max_size:
|
||||
keys_to_remove = list(cls._request_cache.keys())[: -cls._cache_max_size // 2]
|
||||
for key in keys_to_remove:
|
||||
del cls._request_cache[key]
|
||||
|
||||
def _validate_image_size(self, image_size: str) -> bool:
|
||||
"""验证图片尺寸格式"""
|
||||
try:
|
||||
width, height = map(int, image_size.split("x"))
|
||||
return 100 <= width <= 10000 and 100 <= height <= 10000
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]:
|
||||
"""下载图片并将其编码为Base64字符串"""
|
||||
logger.info(f"{self.log_prefix} (B64) 下载并编码图片: {image_url[:70]}...")
|
||||
try:
|
||||
with urllib.request.urlopen(image_url, timeout=30) as response:
|
||||
if response.status == 200:
|
||||
image_bytes = response.read()
|
||||
base64_encoded_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
logger.info(f"{self.log_prefix} (B64) 图片下载编码完成. Base64长度: {len(base64_encoded_image)}")
|
||||
return True, base64_encoded_image
|
||||
else:
|
||||
error_msg = f"下载图片失败 (状态: {response.status})"
|
||||
logger.error(f"{self.log_prefix} (B64) {error_msg} URL: {image_url}")
|
||||
return False, error_msg
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} (B64) 下载或编码时错误: {e!r}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
return False, f"下载或编码图片时发生错误: {str(e)[:100]}"
|
||||
|
||||
def _make_http_image_request(
|
||||
self, prompt: str, model: str, size: str, seed: int, guidance_scale: float, watermark: bool
|
||||
) -> Tuple[bool, str]:
|
||||
"""发送HTTP请求生成图片"""
|
||||
base_url = self.api.get_config("api.base_url")
|
||||
generate_api_key = self.api.get_config("api.volcano_generate_api_key")
|
||||
|
||||
endpoint = f"{base_url.rstrip('/')}/images/generations"
|
||||
|
||||
payload_dict = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"response_format": "url",
|
||||
"size": size,
|
||||
"guidance_scale": guidance_scale,
|
||||
"watermark": watermark,
|
||||
"seed": seed,
|
||||
"api-key": generate_api_key,
|
||||
}
|
||||
|
||||
data = json.dumps(payload_dict).encode("utf-8")
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {generate_api_key}",
|
||||
}
|
||||
|
||||
logger.info(f"{self.log_prefix} (HTTP) 发起图片请求: {model}, Prompt: {prompt[:30]}... To: {endpoint}")
|
||||
|
||||
req = urllib.request.Request(endpoint, data=data, headers=headers, method="POST")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as response:
|
||||
response_status = response.status
|
||||
response_body_bytes = response.read()
|
||||
response_body_str = response_body_bytes.decode("utf-8")
|
||||
|
||||
logger.info(f"{self.log_prefix} (HTTP) 响应: {response_status}. Preview: {response_body_str[:150]}...")
|
||||
|
||||
if 200 <= response_status < 300:
|
||||
response_data = json.loads(response_body_str)
|
||||
image_url = None
|
||||
if (
|
||||
isinstance(response_data.get("data"), list)
|
||||
and response_data["data"]
|
||||
and isinstance(response_data["data"][0], dict)
|
||||
):
|
||||
image_url = response_data["data"][0].get("url")
|
||||
elif response_data.get("url"):
|
||||
image_url = response_data.get("url")
|
||||
|
||||
if image_url:
|
||||
logger.info(f"{self.log_prefix} (HTTP) 图片生成成功,URL: {image_url[:70]}...")
|
||||
return True, image_url
|
||||
else:
|
||||
logger.error(f"{self.log_prefix} (HTTP) API成功但无图片URL")
|
||||
return False, "图片生成API响应成功但未找到图片URL"
|
||||
else:
|
||||
logger.error(f"{self.log_prefix} (HTTP) API请求失败. 状态: {response.status}")
|
||||
return False, f"图片API请求失败(状态码 {response.status})"
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} (HTTP) 图片生成时意外错误: {e!r}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
return False, f"图片生成HTTP请求时发生意外错误: {str(e)[:100]}"
|
||||
|
||||
|
||||
# ===== 插件主类 =====
|
||||
|
||||
|
||||
@register_plugin
|
||||
class DoubaoImagePlugin(BasePlugin):
|
||||
"""豆包图片生成插件
|
||||
|
||||
基于火山引擎豆包模型的AI图片生成插件:
|
||||
- 图片生成Action:根据描述使用火山引擎API生成图片
|
||||
"""
|
||||
|
||||
# 插件基本信息
|
||||
plugin_name = "doubao_pic_plugin" # 内部标识符
|
||||
enable_plugin = True
|
||||
config_file_name = "config.toml"
|
||||
|
||||
# 配置节描述
|
||||
config_section_descriptions = {
|
||||
"plugin": "插件基本信息配置",
|
||||
"api": "API相关配置,包含火山引擎API的访问信息",
|
||||
"generation": "图片生成参数配置,控制生成图片的各种参数",
|
||||
"cache": "结果缓存配置",
|
||||
"components": "组件启用配置",
|
||||
}
|
||||
|
||||
# 配置Schema定义
|
||||
config_schema = {
|
||||
"plugin": {
|
||||
"name": ConfigField(type=str, default="doubao_pic_plugin", description="插件名称", required=True),
|
||||
"version": ConfigField(type=str, default="2.0.0", description="插件版本号"),
|
||||
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
|
||||
"description": ConfigField(
|
||||
type=str, default="基于火山引擎豆包模型的AI图片生成插件", description="插件描述", required=True
|
||||
),
|
||||
},
|
||||
"api": {
|
||||
"base_url": ConfigField(
|
||||
type=str,
|
||||
default="https://ark.cn-beijing.volces.com/api/v3",
|
||||
description="API基础URL",
|
||||
example="https://api.example.com/v1",
|
||||
),
|
||||
"volcano_generate_api_key": ConfigField(
|
||||
type=str, default="YOUR_DOUBAO_API_KEY_HERE", description="火山引擎豆包API密钥", required=True
|
||||
),
|
||||
},
|
||||
"generation": {
|
||||
"default_model": ConfigField(
|
||||
type=str,
|
||||
default="doubao-seedream-3-0-t2i-250415",
|
||||
description="默认使用的文生图模型",
|
||||
choices=["doubao-seedream-3-0-t2i-250415", "doubao-seedream-2-0-t2i"],
|
||||
),
|
||||
"default_size": ConfigField(
|
||||
type=str,
|
||||
default="1024x1024",
|
||||
description="默认图片尺寸",
|
||||
example="1024x1024",
|
||||
choices=["1024x1024", "1024x1280", "1280x1024", "1024x1536", "1536x1024"],
|
||||
),
|
||||
"default_watermark": ConfigField(type=bool, default=True, description="是否默认添加水印"),
|
||||
"default_guidance_scale": ConfigField(
|
||||
type=float, default=2.5, description="模型指导强度,影响图片与提示的关联性", example="2.0"
|
||||
),
|
||||
"default_seed": ConfigField(type=int, default=42, description="随机种子,用于复现图片"),
|
||||
},
|
||||
"cache": {
|
||||
"enabled": ConfigField(type=bool, default=True, description="是否启用请求缓存"),
|
||||
"max_size": ConfigField(type=int, default=10, description="最大缓存数量"),
|
||||
},
|
||||
"components": {
|
||||
"enable_image_generation": ConfigField(type=bool, default=True, description="是否启用图片生成Action")
|
||||
},
|
||||
}
|
||||
|
||||
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
||||
"""返回插件包含的组件列表"""
|
||||
|
||||
# 从配置获取组件启用状态
|
||||
enable_image_generation = self.get_config("components.enable_image_generation", True)
|
||||
|
||||
components = []
|
||||
|
||||
# 添加图片生成Action
|
||||
if enable_image_generation:
|
||||
components.append((DoubaoImageGenerationAction.get_action_info(), DoubaoImageGenerationAction))
|
||||
|
||||
return components
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "群聊禁言管理插件 (Mute Plugin)",
|
||||
"version": "3.0.0",
|
||||
"description": "群聊禁言管理插件,提供智能禁言功能",
|
||||
"author": {
|
||||
"name": "MaiBot开发团队",
|
||||
"url": "https://github.com/MaiM-with-u"
|
||||
},
|
||||
"license": "GPL-v3.0-or-later",
|
||||
"host_application": {
|
||||
"min_version": "0.8.0",
|
||||
"max_version": "0.8.10"
|
||||
},
|
||||
"keywords": ["mute", "ban", "moderation", "admin", "management", "group"],
|
||||
"categories": ["Moderation", "Group Management", "Admin Tools"],
|
||||
"default_locale": "zh-CN",
|
||||
"locales_path": "_locales"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user