Files
Mofox-Core/src/person_info/group_relationship_manager.py
2025-08-11 20:25:13 +08:00

184 lines
7.3 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 time
import json
import re
import asyncio
from typing import Any, Optional
from src.common.logger import get_logger
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
from src.chat.utils.chat_message_builder import (
get_raw_msg_by_timestamp_with_chat_inclusive,
build_readable_messages,
)
from src.person_info.group_info import get_group_info_manager
from src.plugin_system.apis import message_api
from json_repair import repair_json
logger = get_logger("group_relationship_manager")
class GroupRelationshipManager:
def __init__(self):
self.group_llm = LLMRequest(
model_set=model_config.model_task_config.utils, request_type="group.relationship"
)
self.last_group_impression_time = 0.0
self.last_group_impression_message_count = 0
async def build_relation(self, chat_id: str, platform: str) -> None:
"""构建群关系,类似 relationship_builder.build_relation() 的调用方式"""
current_time = time.time()
talk_frequency = global_config.chat.get_current_talk_frequency(chat_id)
# 计算间隔时间基于活跃度动态调整最小10分钟最大30分钟
interval_seconds = max(600, int(1800 / max(0.5, talk_frequency)))
# 统计新消息数量
# 先获取所有新消息,然后过滤掉麦麦的消息和命令消息
all_new_messages = message_api.get_messages_by_time_in_chat(
chat_id=chat_id,
start_time=self.last_group_impression_time,
end_time=current_time,
filter_mai=True,
filter_command=True,
)
new_messages_since_last_impression = len(all_new_messages)
# 触发条件:时间间隔 OR 消息数量阈值
if (current_time - self.last_group_impression_time >= interval_seconds) or \
(new_messages_since_last_impression >= 100):
logger.info(f"[{chat_id}] 触发群印象构建 (时间间隔: {current_time - self.last_group_impression_time:.0f}s, 消息数: {new_messages_since_last_impression})")
# 异步执行群印象构建
asyncio.create_task(
self.build_group_impression(
chat_id=chat_id,
platform=platform,
lookback_hours=12,
max_messages=300
)
)
self.last_group_impression_time = current_time
self.last_group_impression_message_count = 0
else:
# 更新消息计数
self.last_group_impression_message_count = new_messages_since_last_impression
logger.debug(f"[{chat_id}] 群印象构建等待中 (时间: {current_time - self.last_group_impression_time:.0f}s/{interval_seconds}s, 消息: {new_messages_since_last_impression}/100)")
async def build_group_impression(
self,
chat_id: str,
platform: str,
lookback_hours: int = 24,
max_messages: int = 300,
) -> Optional[str]:
"""基于最近聊天记录构建群印象并存储
返回生成的topic
"""
now = time.time()
start_ts = now - lookback_hours * 3600
# 拉取最近消息(包含边界)
messages = get_raw_msg_by_timestamp_with_chat_inclusive(chat_id, start_ts, now)
if not messages:
logger.info(f"[{chat_id}] 无近期消息,跳过群印象构建")
return None
# 限制数量,优先最新
messages = sorted(messages, key=lambda m: m.get("time", 0))[-max_messages:]
# 构建可读文本
readable = build_readable_messages(
messages=messages, replace_bot_name=True, timestamp_mode="normal_no_YMD", truncate=True
)
if not readable:
logger.info(f"[{chat_id}] 构建可读消息文本为空,跳过")
return None
# 确保群存在
group_info_manager = get_group_info_manager()
group_id = await group_info_manager.get_or_create_group(platform, chat_id)
group_name = await group_info_manager.get_value(group_id, "group_name") or chat_id
alias_str = ", ".join(global_config.bot.alias_names)
prompt = f"""
你的名字是{global_config.bot.nickname}{global_config.bot.nickname}的别名是{alias_str}
你现在在群「{group_name}」(平台:{platform})中。
请你根据以下群内最近的聊天记录,总结这个群给你的印象。
要求:
- 关注群的氛围(友好/活跃/娱乐/学习/严肃等)、常见话题、互动风格、活跃时段或频率、是否有显著文化/梗。
- 用白话表达,避免夸张或浮夸的词汇;语气自然、接地气。
- 不要暴露任何个人隐私信息。
- 请严格按照json格式输出不要有其他多余内容
{{
"impression": "不超过200字的群印象长描述白话、自然",
"topic": "一句话概括群主要聊什么,白话"
}}
群内聊天(节选):
{readable}
"""
# 生成印象
content, _ = await self.group_llm.generate_response_async(prompt=prompt)
raw_text = (content or "").strip()
def _strip_code_fences(text: str) -> str:
if text.startswith("```") and text.endswith("```"):
# 去除首尾围栏
return re.sub(r"^```[a-zA-Z0-9_\-]*\n|\n```$", "", text, flags=re.S)
# 提取围栏中的主体
match = re.search(r"```[a-zA-Z0-9_\-]*\n([\s\S]*?)\n```", text)
return match.group(1) if match else text
parsed_text = _strip_code_fences(raw_text)
long_impression: str = ""
topic_val: Any = ""
# 参考关系模块先repair_json再loads兼容返回列表/字典/字符串
try:
fixed = repair_json(parsed_text)
data = json.loads(fixed) if isinstance(fixed, str) else fixed
if isinstance(data, list) and data and isinstance(data[0], dict):
data = data[0]
if isinstance(data, dict):
long_impression = str(data.get("impression") or "").strip()
topic_val = data.get("topic", "")
else:
# 不是字典,直接作为文本
text_fallback = str(data)
long_impression = text_fallback[:400].strip()
topic_val = ""
except Exception:
long_impression = parsed_text[:400].strip()
topic_val = ""
# 兜底
if not long_impression and not topic_val:
logger.info(f"[{chat_id}] LLM未产生有效群印象跳过")
return None
# 写入数据库
await group_info_manager.update_one_field(group_id, "group_impression", long_impression)
if topic_val:
await group_info_manager.update_one_field(group_id, "topic", topic_val)
await group_info_manager.update_one_field(group_id, "last_active", now)
logger.info(f"[{chat_id}] 群印象更新完成: topic={topic_val}")
return str(topic_val) if topic_val else ""
group_relationship_manager: Optional[GroupRelationshipManager] = None
def get_group_relationship_manager() -> GroupRelationshipManager:
global group_relationship_manager
if group_relationship_manager is None:
group_relationship_manager = GroupRelationshipManager()
return group_relationship_manager