Files
mai-bot/src/mais4u/mais4u_chat/s4u_mood_manager.py
2025-07-13 13:00:03 +08:00

621 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import json
import time
from src.chat.message_receive.message import MessageRecv
from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive
from src.config.config import global_config
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.manager.async_task_manager import AsyncTask, async_task_manager
from src.plugin_system.apis import send_api
"""
面部表情系统使用说明:
1. 预定义的面部表情:
- happy: 高兴表情(眼睛微笑 + 眉毛微笑 + 嘴巴大笑)
- very_happy: 非常高兴(高兴表情 + 脸红)
- sad: 悲伤表情(眼睛哭泣 + 眉毛忧伤 + 嘴巴悲伤)
- angry: 生气表情(眉毛生气 + 嘴巴生气)
- fear: 恐惧表情(眼睛闭上)
- shy: 害羞表情(嘴巴嘟起 + 脸红)
- neutral: 中性表情(无表情)
2. 使用方法:
# 获取面部表情管理器
facial_expression = mood_manager.get_facial_expression_by_chat_id(chat_id)
# 发送指定表情
await facial_expression.send_expression("happy")
# 根据情绪值自动选择表情
await facial_expression.send_expression_by_mood(mood_values)
# 重置为中性表情
await facial_expression.reset_expression()
3. 自动表情系统:
- 当情绪值更新时系统会自动根据mood_values选择合适的面部表情
- 只有当新表情与当前表情不同时才会发送,避免重复发送
- 支持joy >= 8时显示very_happyjoy >= 6时显示happy等梯度表情
4. amadus表情更新系统
- 每1秒检查一次表情是否有变化如有变化则发送到amadus
- 每次mood更新后立即发送表情更新
- 发送消息类型为"amadus_expression_update",格式为{"action": "表情名", "data": 1.0}
5. 表情选择逻辑:
- 系统会找出最强的情绪joy, anger, sorrow, fear
- 根据情绪强度选择相应的表情组合
- 默认情况下返回neutral表情
"""
logger = get_logger("mood")
def init_prompt():
Prompt(
"""
{chat_talking_prompt}
以上是直播间里正在进行的对话
{indentify_block}
你刚刚的情绪状态是:{mood_state}
现在,发送了消息,引起了你的注意,你对其进行了阅读和思考,请你输出一句话描述你新的情绪状态,不要输出任何其他内容
请只输出情绪状态,不要输出其他内容:
""",
"change_mood_prompt_vtb",
)
Prompt(
"""
{chat_talking_prompt}
以上是直播间里最近的对话
{indentify_block}
你之前的情绪状态是:{mood_state}
距离你上次关注直播间消息已经过去了一段时间,你冷静了下来,请你输出一句话描述你现在的情绪状态
请只输出情绪状态,不要输出其他内容:
""",
"regress_mood_prompt_vtb",
)
Prompt(
"""
{chat_talking_prompt}
以上是直播间里正在进行的对话
{indentify_block}
你刚刚的情绪状态是:{mood_state}
具体来说从1-10分你的情绪状态是
喜(Joy): {joy}
怒(Anger): {anger}
哀(Sorrow): {sorrow}
惧(Fear): {fear}
现在,发送了消息,引起了你的注意,你对其进行了阅读和思考。请基于对话内容,评估你新的情绪状态。
请以JSON格式输出你新的情绪状态包含"喜怒哀惧"四个维度每个维度的取值范围为1-10。
键值请使用英文: "joy", "anger", "sorrow", "fear".
例如: {{"joy": 5, "anger": 1, "sorrow": 1, "fear": 1}}
不要输出任何其他内容只输出JSON。
""",
"change_mood_numerical_prompt",
)
Prompt(
"""
{chat_talking_prompt}
以上是直播间里最近的对话
{indentify_block}
你之前的情绪状态是:{mood_state}
具体来说从1-10分你的情绪状态是
喜(Joy): {joy}
怒(Anger): {anger}
哀(Sorrow): {sorrow}
惧(Fear): {fear}
距离你上次关注直播间消息已经过去了一段时间,你冷静了下来。请基于此,评估你现在的情绪状态。
请以JSON格式输出你新的情绪状态包含"喜怒哀惧"四个维度每个维度的取值范围为1-10。
键值请使用英文: "joy", "anger", "sorrow", "fear".
例如: {{"joy": 5, "anger": 1, "sorrow": 1, "fear": 1}}
不要输出任何其他内容只输出JSON。
""",
"regress_mood_numerical_prompt",
)
class FacialExpression:
def __init__(self, chat_id: str):
self.chat_id: str = chat_id
# 预定义面部表情动作
self.expressions = {
# 眼睛表情
"eye_smile": {"action": "eye_smile", "data": 1.0},
"eye_cry": {"action": "eye_cry", "data": 1.0},
"eye_close": {"action": "eye_close", "data": 1.0},
"eye_normal": {"action": "eye_normal", "data": 1.0},
# 眉毛表情
"eyebrow_smile": {"action": "eyebrow_smile", "data": 1.0},
"eyebrow_angry": {"action": "eyebrow_angry", "data": 1.0},
"eyebrow_sad": {"action": "eyebrow_sad", "data": 1.0},
"eyebrow_normal": {"action": "eyebrow_normal", "data": 1.0},
# 嘴巴表情
"mouth_sad": {"action": "mouth_sad", "data": 1.0},
"mouth_angry": {"action": "mouth_angry", "data": 1.0},
"mouth_laugh": {"action": "mouth_laugh", "data": 1.0},
"mouth_pout": {"action": "mouth_pout", "data": 1.0},
"mouth_normal": {"action": "mouth_normal", "data": 1.0},
# 脸部表情
"face_blush": {"action": "face_blush", "data": 1.0},
"face_normal": {"action": "face_normal", "data": 1.0},
}
# 表情组合模板
self.expression_combinations = {
"happy": {
"eye": "eye_smile",
"eyebrow": "eyebrow_smile",
"mouth": "mouth_laugh",
"face": "face_normal"
},
"very_happy": {
"eye": "eye_smile",
"eyebrow": "eyebrow_smile",
"mouth": "mouth_laugh",
"face": "face_blush"
},
"sad": {
"eye": "eye_cry",
"eyebrow": "eyebrow_sad",
"mouth": "mouth_sad",
"face": "face_normal"
},
"angry": {
"eye": "eye_normal",
"eyebrow": "eyebrow_angry",
"mouth": "mouth_angry",
"face": "face_normal"
},
"fear": {
"eye": "eye_close",
"eyebrow": "eyebrow_normal",
"mouth": "mouth_normal",
"face": "face_normal"
},
"shy": {
"eye": "eye_normal",
"eyebrow": "eyebrow_normal",
"mouth": "mouth_pout",
"face": "face_blush"
},
"neutral": {
"eye": "eye_normal",
"eyebrow": "eyebrow_normal",
"mouth": "mouth_normal",
"face": "face_normal"
}
}
def select_expression_by_mood(self, mood_values: dict[str, int]) -> str:
"""根据情绪值选择合适的表情组合"""
joy = mood_values.get("joy", 5)
anger = mood_values.get("anger", 1)
sorrow = mood_values.get("sorrow", 1)
fear = mood_values.get("fear", 1)
# 找出最强的情绪
emotions = {
"joy": joy,
"anger": anger,
"sorrow": sorrow,
"fear": fear
}
# 获取最强情绪
dominant_emotion = max(emotions, key=emotions.get)
dominant_value = emotions[dominant_emotion]
# 根据情绪强度和类型选择表情
if dominant_emotion == "joy":
if joy >= 8:
return "very_happy"
elif joy >= 6:
return "happy"
elif joy >= 4:
return "shy"
else:
return "neutral"
elif dominant_emotion == "anger" and anger >= 6:
return "angry"
elif dominant_emotion == "sorrow" and sorrow >= 6:
return "sad"
elif dominant_emotion == "fear" and fear >= 6:
return "fear"
else:
return "neutral"
async def send_expression(self, expression_name: str):
"""发送表情组合"""
if expression_name not in self.expression_combinations:
logger.warning(f"[{self.chat_id}] 未知表情: {expression_name}")
return
combination = self.expression_combinations[expression_name]
# 依次发送各部位表情
for part, expression_key in combination.items():
if expression_key in self.expressions:
expression_data = self.expressions[expression_key]
await send_api.custom_to_stream(
message_type="facial_expression",
content=expression_data,
stream_id=self.chat_id
)
logger.info(f"[{self.chat_id}] 发送面部表情 {part}: {expression_data}")
await asyncio.sleep(0.1) # 短暂延迟避免同时发送过多消息
# 通知ChatMood需要更新amadus
# 这里需要从mood_manager获取ChatMood实例并标记
chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id)
if chat_mood.last_expression != expression_name:
chat_mood.last_expression = expression_name
chat_mood.expression_needs_update = True
async def send_expression_by_mood(self, mood_values: dict[str, int]):
"""根据情绪值发送相应的面部表情"""
expression_name = self.select_expression_by_mood(mood_values)
logger.info(f"[{self.chat_id}] 根据情绪值选择表情: {expression_name}, 情绪值: {mood_values}")
await self.send_expression(expression_name)
async def reset_expression(self):
"""重置为中性表情"""
await self.send_expression("neutral")
class ChatMood:
def __init__(self, chat_id: str):
self.chat_id: str = chat_id
self.mood_state: str = "感觉很平静"
self.mood_values: dict[str, int] = {"joy": 5, "anger": 1, "sorrow": 1, "fear": 1}
self.regression_count: int = 0
self.mood_model = LLMRequest(
model=global_config.model.emotion,
temperature=0.7,
request_type="mood_text",
)
self.mood_model_numerical = LLMRequest(
model=global_config.model.emotion,
temperature=0.4,
request_type="mood_numerical",
)
self.last_change_time = 0
# 添加面部表情系统
self.facial_expression = FacialExpression(chat_id)
self.last_expression = "neutral" # 记录上一次的表情
self.expression_needs_update = False # 标记表情是否需要更新
# 设置初始中性表情
asyncio.create_task(self.facial_expression.reset_expression())
self.expression_needs_update = True # 初始化时也标记需要更新
def _parse_numerical_mood(self, response: str) -> dict[str, int] | None:
try:
# The LLM might output markdown with json inside
if "```json" in response:
response = response.split("```json")[1].split("```")[0]
elif "```" in response:
response = response.split("```")[1].split("```")[0]
data = json.loads(response)
# Validate
required_keys = {"joy", "anger", "sorrow", "fear"}
if not required_keys.issubset(data.keys()):
logger.warning(f"Numerical mood response missing keys: {response}")
return None
for key in required_keys:
value = data[key]
if not isinstance(value, int) or not (1 <= value <= 10):
logger.warning(f"Numerical mood response invalid value for {key}: {value} in {response}")
return None
return {key: data[key] for key in required_keys}
except json.JSONDecodeError:
logger.warning(f"Failed to parse numerical mood JSON: {response}")
return None
except Exception as e:
logger.error(f"Error parsing numerical mood: {e}, response: {response}")
return None
async def update_mood_by_message(self, message: MessageRecv):
self.regression_count = 0
message_time = message.message_info.time
message_list_before_now = get_raw_msg_by_timestamp_with_chat_inclusive(
chat_id=self.chat_id,
timestamp_start=self.last_change_time,
timestamp_end=message_time,
limit=15,
limit_mode="last",
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="normal_no_YMD",
read_mark=0.0,
truncate=True,
show_actions=True,
)
bot_name = global_config.bot.nickname
if global_config.bot.alias_names:
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
else:
bot_nickname = ""
prompt_personality = global_config.personality.personality_core
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
async def _update_text_mood():
prompt = await global_prompt_manager.format_prompt(
"change_mood_prompt_vtb",
chat_talking_prompt=chat_talking_prompt,
indentify_block=indentify_block,
mood_state=self.mood_state,
)
logger.debug(f"text mood prompt: {prompt}")
response, (reasoning_content, model_name) = await self.mood_model.generate_response_async(prompt=prompt)
logger.info(f"text mood response: {response}")
logger.debug(f"text mood reasoning_content: {reasoning_content}")
return response
async def _update_numerical_mood():
prompt = await global_prompt_manager.format_prompt(
"change_mood_numerical_prompt",
chat_talking_prompt=chat_talking_prompt,
indentify_block=indentify_block,
mood_state=self.mood_state,
joy=self.mood_values["joy"],
anger=self.mood_values["anger"],
sorrow=self.mood_values["sorrow"],
fear=self.mood_values["fear"],
)
logger.info(f"numerical mood prompt: {prompt}")
response, (reasoning_content, model_name) = await self.mood_model_numerical.generate_response_async(
prompt=prompt
)
logger.info(f"numerical mood response: {response}")
logger.debug(f"numerical mood reasoning_content: {reasoning_content}")
return self._parse_numerical_mood(response)
results = await asyncio.gather(_update_text_mood(), _update_numerical_mood())
text_mood_response, numerical_mood_response = results
if text_mood_response:
self.mood_state = text_mood_response
if numerical_mood_response:
old_mood_values = self.mood_values.copy()
self.mood_values = numerical_mood_response
# 发送面部表情
new_expression = self.facial_expression.select_expression_by_mood(self.mood_values)
if new_expression != self.last_expression:
# 立即发送表情
asyncio.create_task(self.facial_expression.send_expression(new_expression))
self.last_expression = new_expression
self.expression_needs_update = True # 标记表情已更新
self.last_change_time = message_time
async def regress_mood(self):
message_time = time.time()
message_list_before_now = get_raw_msg_by_timestamp_with_chat_inclusive(
chat_id=self.chat_id,
timestamp_start=self.last_change_time,
timestamp_end=message_time,
limit=15,
limit_mode="last",
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="normal_no_YMD",
read_mark=0.0,
truncate=True,
show_actions=True,
)
bot_name = global_config.bot.nickname
if global_config.bot.alias_names:
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
else:
bot_nickname = ""
prompt_personality = global_config.personality.personality_core
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}"
async def _regress_text_mood():
prompt = await global_prompt_manager.format_prompt(
"regress_mood_prompt_vtb",
chat_talking_prompt=chat_talking_prompt,
indentify_block=indentify_block,
mood_state=self.mood_state,
)
logger.debug(f"text regress prompt: {prompt}")
response, (reasoning_content, model_name) = await self.mood_model.generate_response_async(prompt=prompt)
logger.info(f"text regress response: {response}")
logger.debug(f"text regress reasoning_content: {reasoning_content}")
return response
async def _regress_numerical_mood():
prompt = await global_prompt_manager.format_prompt(
"regress_mood_numerical_prompt",
chat_talking_prompt=chat_talking_prompt,
indentify_block=indentify_block,
mood_state=self.mood_state,
joy=self.mood_values["joy"],
anger=self.mood_values["anger"],
sorrow=self.mood_values["sorrow"],
fear=self.mood_values["fear"],
)
logger.debug(f"numerical regress prompt: {prompt}")
response, (reasoning_content, model_name) = await self.mood_model_numerical.generate_response_async(
prompt=prompt
)
logger.info(f"numerical regress response: {response}")
logger.debug(f"numerical regress reasoning_content: {reasoning_content}")
return self._parse_numerical_mood(response)
results = await asyncio.gather(_regress_text_mood(), _regress_numerical_mood())
text_mood_response, numerical_mood_response = results
if text_mood_response:
self.mood_state = text_mood_response
if numerical_mood_response:
old_mood_values = self.mood_values.copy()
self.mood_values = numerical_mood_response
# 发送面部表情
new_expression = self.facial_expression.select_expression_by_mood(self.mood_values)
if new_expression != self.last_expression:
# 立即发送表情
asyncio.create_task(self.facial_expression.send_expression(new_expression))
self.last_expression = new_expression
self.expression_needs_update = True # 标记表情已更新
self.regression_count += 1
async def send_expression_update_if_needed(self):
"""如果表情有变化发送更新到amadus"""
if self.expression_needs_update:
# 发送当前表情状态到amadus使用简洁的action/data格式
expression_data = {
"action": self.last_expression,
"data": 1.0
}
await send_api.custom_to_stream(
message_type="amadus_expression_update",
content=expression_data,
stream_id=self.chat_id
)
logger.info(f"[{self.chat_id}] 发送表情更新到amadus: {expression_data}")
self.expression_needs_update = False # 重置标记
class MoodRegressionTask(AsyncTask):
def __init__(self, mood_manager: "MoodManager"):
super().__init__(task_name="MoodRegressionTask", run_interval=30)
self.mood_manager = mood_manager
async def run(self):
logger.debug("Running mood regression task...")
now = time.time()
for mood in self.mood_manager.mood_list:
if mood.last_change_time == 0:
continue
if now - mood.last_change_time > 180:
if mood.regression_count >= 3:
continue
logger.info(f"chat {mood.chat_id} 开始情绪回归, 这是第 {mood.regression_count + 1}")
await mood.regress_mood()
class ExpressionUpdateTask(AsyncTask):
def __init__(self, mood_manager: "MoodManager"):
super().__init__(task_name="ExpressionUpdateTask", run_interval=1)
self.mood_manager = mood_manager
async def run(self):
logger.debug("Running expression update task...")
for mood in self.mood_manager.mood_list:
await mood.send_expression_update_if_needed()
class MoodManager:
def __init__(self):
self.mood_list: list[ChatMood] = []
"""当前情绪状态"""
self.task_started: bool = False
async def start(self):
"""启动情绪回归后台任务"""
if self.task_started:
return
logger.info("启动情绪管理任务...")
# 启动情绪回归任务
regression_task = MoodRegressionTask(self)
await async_task_manager.add_task(regression_task)
# 启动表情更新任务
expression_task = ExpressionUpdateTask(self)
await async_task_manager.add_task(expression_task)
self.task_started = True
logger.info("情绪管理任务已启动(包含情绪回归和表情更新)")
def get_mood_by_chat_id(self, chat_id: str) -> ChatMood:
for mood in self.mood_list:
if mood.chat_id == chat_id:
return mood
new_mood = ChatMood(chat_id)
self.mood_list.append(new_mood)
return new_mood
def reset_mood_by_chat_id(self, chat_id: str):
for mood in self.mood_list:
if mood.chat_id == chat_id:
mood.mood_state = "感觉很平静"
mood.mood_values = {"joy": 5, "anger": 1, "sorrow": 1, "fear": 1}
mood.regression_count = 0
# 重置面部表情为中性
asyncio.create_task(mood.facial_expression.reset_expression())
mood.last_expression = "neutral"
mood.expression_needs_update = True # 标记表情需要更新
return
# 如果没有找到现有的mood创建新的
new_mood = ChatMood(chat_id)
self.mood_list.append(new_mood)
asyncio.create_task(new_mood.facial_expression.reset_expression())
new_mood.expression_needs_update = True # 标记表情需要更新
def get_facial_expression_by_chat_id(self, chat_id: str) -> FacialExpression:
"""获取聊天对应的面部表情管理器"""
for mood in self.mood_list:
if mood.chat_id == chat_id:
return mood.facial_expression
# 如果没有找到,创建新的
new_mood = ChatMood(chat_id)
self.mood_list.append(new_mood)
return new_mood.facial_expression
init_prompt()
mood_manager = MoodManager()
"""全局情绪管理器"""