feat(profile):对用户关系和分析系统进行重构,采用结构化数据和异步更新
此提交完全重写了用户关系和分析系统,创建了一个更强大、详细和响应式的框架。旧系统已被弃用,取而代之的是一个集中式的`UserRelationships`模型。
主要变更:
1. ‌**增强数据库模型(`UserRelationships`):**‌
- 添加`impression_text`用于长期、叙述式印象。
- 引入`key_facts`(JSON)存储结构化数据如生日、工作和位置。
- 添加`relationship_stage`跟踪关系进展(如陌生人、朋友、挚友)。
- 添加`first_met_time`和`last_impression_update`的时间戳。
2. ‌**重设计`UserProfileTool`:**‌
- 工具的用途被限定为仅捕捉重要新信息,防止用于小聊。
- 更新现在在后台异步处理,确保机器人回复不被延迟。
- 引入`key_info_type`和`key_info_value`参数供LLM提交结构化事实。
3. ‌**复杂的印象和情感逻辑:**‌
- 关系追踪LLM现在分析最近聊天历史生成更丰富、更上下文的印象。
- 用渐进的`affection_change`(最大±0.03)取代直接情感分数设置,使关系发展更真实。
4. ‌**数据源整合:**‌
- `RelationshipFetcher`重构为仅依赖`UserRelationships`表作为唯一数据源。
- 简化`get_user_relationship` API并移除其缓存,确保分析的实时数据访问。
破坏性变更:`UserProfileTool`已重设计,新增参数(`key_info_type`、`key_info_value`)并改变用途。移除`affection_score`参数。此外,`get_user_relationship`数据库API签名简化为仅接受`user_id`。
This commit is contained in:
@@ -5,6 +5,7 @@ AffinityFlow Chatter 工具模块
|
||||
"""
|
||||
|
||||
from .chat_stream_impression_tool import ChatStreamImpressionTool
|
||||
from .user_fact_tool import UserFactTool
|
||||
from .user_profile_tool import UserProfileTool
|
||||
|
||||
__all__ = ["ChatStreamImpressionTool", "UserProfileTool"]
|
||||
__all__ = ["ChatStreamImpressionTool", "UserProfileTool", "UserFactTool"]
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
用户关键信息记录工具
|
||||
|
||||
用于记录用户的长期重要信息,如生日、职业、理想、宠物等。
|
||||
这些信息会存储在 user_relationships 表的 key_facts 字段中。
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.common.database.compatibility import get_db_session
|
||||
from src.common.database.core.models import UserRelationships
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system import BaseTool, ToolParamType
|
||||
|
||||
logger = get_logger("user_fact_tool")
|
||||
|
||||
|
||||
class UserFactTool(BaseTool):
|
||||
"""用户关键信息记录工具
|
||||
|
||||
用于记录生日、职业、理想、宠物等长期重要信息。
|
||||
注意:一般情况下使用 update_user_profile 工具即可同时记录印象和关键信息。
|
||||
此工具仅在需要单独补充记录信息时使用。
|
||||
"""
|
||||
|
||||
name = "remember_user_info"
|
||||
description = """【备用工具】单独记录用户的重要个人信息。
|
||||
注意:大多数情况请直接使用 update_user_profile 工具(它可以同时更新印象和记录关键信息)。
|
||||
仅当你只想补充记录一条信息、不需要更新印象时才使用此工具。"""
|
||||
|
||||
parameters = [
|
||||
("target_user_id", ToolParamType.STRING, "目标用户的ID(必须)", True, None),
|
||||
("target_user_name", ToolParamType.STRING, "目标用户的名字/昵称(必须)", True, None),
|
||||
("info_type", ToolParamType.STRING, "信息类型:birthday(生日)/job(职业)/location(所在地)/dream(理想)/family(家庭)/pet(宠物)/other(其他)", True, None),
|
||||
("info_value", ToolParamType.STRING, "具体内容,如'11月23日'、'程序员'、'想开咖啡店'", True, None),
|
||||
]
|
||||
available_for_llm = True
|
||||
history_ttl = 5
|
||||
|
||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行关键信息记录
|
||||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
target_user_id = function_args.get("target_user_id")
|
||||
target_user_name = function_args.get("target_user_name", target_user_id)
|
||||
info_type = function_args.get("info_type", "other")
|
||||
info_value = function_args.get("info_value", "")
|
||||
|
||||
if not target_user_id:
|
||||
return {
|
||||
"type": "error",
|
||||
"id": "remember_user_info",
|
||||
"content": "错误:必须提供目标用户ID"
|
||||
}
|
||||
|
||||
if not info_value:
|
||||
return {
|
||||
"type": "error",
|
||||
"id": "remember_user_info",
|
||||
"content": "错误:必须提供要记录的信息内容"
|
||||
}
|
||||
|
||||
# 验证 info_type
|
||||
valid_types = ["birthday", "job", "location", "dream", "family", "pet", "other"]
|
||||
if info_type not in valid_types:
|
||||
info_type = "other"
|
||||
|
||||
# 更新数据库
|
||||
await self._add_key_fact(target_user_id, info_type, info_value)
|
||||
|
||||
# 生成友好的类型名称
|
||||
type_names = {
|
||||
"birthday": "生日",
|
||||
"job": "职业",
|
||||
"location": "所在地",
|
||||
"dream": "理想",
|
||||
"family": "家庭",
|
||||
"pet": "宠物",
|
||||
"other": "其他信息"
|
||||
}
|
||||
type_name = type_names.get(info_type, "信息")
|
||||
|
||||
result_text = f"已记住 {target_user_name} 的{type_name}:{info_value}"
|
||||
logger.info(f"记录用户关键信息: {target_user_id}, {info_type}={info_value}")
|
||||
|
||||
return {
|
||||
"type": "user_fact_recorded",
|
||||
"id": target_user_id,
|
||||
"content": result_text
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"记录用户关键信息失败: {e}")
|
||||
return {
|
||||
"type": "error",
|
||||
"id": function_args.get("target_user_id", "unknown"),
|
||||
"content": f"记录失败: {e!s}"
|
||||
}
|
||||
|
||||
async def _add_key_fact(self, user_id: str, info_type: str, info_value: str):
|
||||
"""添加或更新关键信息
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
info_type: 信息类型
|
||||
info_value: 信息内容
|
||||
"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
async with get_db_session() as session:
|
||||
stmt = select(UserRelationships).where(UserRelationships.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
# 解析现有的 key_facts
|
||||
try:
|
||||
facts = orjson.loads(existing.key_facts) if existing.key_facts else []
|
||||
except Exception:
|
||||
facts = []
|
||||
|
||||
if not isinstance(facts, list):
|
||||
facts = []
|
||||
|
||||
# 查找是否已有相同类型的信息
|
||||
found = False
|
||||
for i, fact in enumerate(facts):
|
||||
if isinstance(fact, dict) and fact.get("type") == info_type:
|
||||
# 更新现有记录
|
||||
facts[i] = {"type": info_type, "value": info_value}
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
# 添加新记录
|
||||
facts.append({"type": info_type, "value": info_value})
|
||||
|
||||
# 更新数据库
|
||||
existing.key_facts = orjson.dumps(facts).decode("utf-8")
|
||||
existing.last_updated = current_time
|
||||
else:
|
||||
# 创建新用户记录
|
||||
facts = [{"type": info_type, "value": info_value}]
|
||||
new_profile = UserRelationships(
|
||||
user_id=user_id,
|
||||
user_name=user_id,
|
||||
key_facts=orjson.dumps(facts).decode("utf-8"),
|
||||
first_met_time=current_time,
|
||||
last_updated=current_time
|
||||
)
|
||||
session.add(new_profile)
|
||||
|
||||
await session.commit()
|
||||
logger.info(f"关键信息已保存: {user_id}, {info_type}={info_value}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存关键信息失败: {e}")
|
||||
raise
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
采用两阶段设计:
|
||||
1. 工具调用模型(tool_use)负责判断是否需要更新,传入基本信息
|
||||
2. 关系追踪模型(relationship_tracker)负责生成高质量的、有人设特色的印象内容
|
||||
2. 关系追踪模型(relationship_tracker)负责:
|
||||
- 读取最近聊天记录
|
||||
- 生成高质量的、有人设特色的印象内容
|
||||
- 决定好感度变化(联动更新)
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -11,40 +14,61 @@ from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages
|
||||
from src.common.database.compatibility import get_db_session
|
||||
from src.common.database.core.models import UserRelationships
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config, model_config
|
||||
from src.config.config import global_config, model_config # type: ignore[attr-defined]
|
||||
from src.plugin_system import BaseTool, ToolParamType
|
||||
|
||||
# 默认好感度分数,用于配置未初始化时的回退
|
||||
DEFAULT_RELATIONSHIP_SCORE = 0.3
|
||||
|
||||
logger = get_logger("user_profile_tool")
|
||||
|
||||
|
||||
def _get_base_relationship_score() -> float:
|
||||
"""安全获取基础好感度分数"""
|
||||
if global_config and global_config.affinity_flow:
|
||||
return global_config.affinity_flow.base_relationship_score
|
||||
return DEFAULT_RELATIONSHIP_SCORE
|
||||
|
||||
|
||||
class UserProfileTool(BaseTool):
|
||||
"""用户画像更新工具
|
||||
|
||||
两阶段设计:
|
||||
- 第一阶段:tool_use模型判断是否更新,传入简要信息
|
||||
- 第二阶段:relationship_tracker模型生成有人设特色的印象描述
|
||||
- 第二阶段:relationship_tracker模型读取聊天记录,生成印象并决定好感度变化
|
||||
"""
|
||||
|
||||
name = "update_user_profile"
|
||||
description = """当你通过聊天对某个人产生了新的认识或印象时使用此工具。
|
||||
调用时机:当你发现TA透露了新信息、展现了性格特点、表达了兴趣爱好,或你们的互动让你对TA有了新感受时。
|
||||
注意:impression_hint只需要简单描述你观察到的要点,系统会自动用你的人设风格来润色生成最终印象。"""
|
||||
description = """记录你对某个人的重要认识。【不要频繁调用】
|
||||
只在以下情况使用:
|
||||
1. TA首次告诉你【具体的个人信息】:生日日期、职业、所在城市、真实姓名等 → 必填 key_info_type 和 key_info_value
|
||||
2. 你对TA产生了【显著的、值得长期记住的】新印象(不是每次聊天都要记)
|
||||
3. 你们的关系有了【实质性变化】
|
||||
|
||||
【不要调用的情况】:
|
||||
- 普通的日常对话、闲聊
|
||||
- 只是聊得开心但没有实质性新认识
|
||||
- TA只是表达了一下情绪或感受
|
||||
- 你已经记录过类似的印象
|
||||
此工具会在后台异步执行,不会阻塞你的回复。"""
|
||||
parameters = [
|
||||
("target_user_id", ToolParamType.STRING, "目标用户的ID(必须)", True, None),
|
||||
("target_user_name", ToolParamType.STRING, "目标用户的名字/昵称(必须,用于生成印象时称呼)", True, None),
|
||||
("user_aliases", ToolParamType.STRING, "TA的其他昵称或别名,多个用逗号分隔(可选)", False, None),
|
||||
("impression_hint", ToolParamType.STRING, "【简要描述】你观察到的关于TA的要点,如'很健谈,喜欢聊游戏,有点害羞'。系统会用你的人设风格润色(可选)", False, None),
|
||||
("impression_hint", ToolParamType.STRING, "【简要描述】你观察到的关于TA的要点,如'很健谈,喜欢聊游戏,有点害羞'(可选,只填显著的新认识)", False, None),
|
||||
("preference_keywords", ToolParamType.STRING, "TA的兴趣爱好关键词,如'编程,游戏,音乐',用逗号分隔(可选)", False, None),
|
||||
("affection_score", ToolParamType.FLOAT, "你对TA的好感度(0.0-1.0)。0.3=普通认识,0.5=还不错的朋友,0.7=很喜欢,0.9=非常亲密。打分要保守(可选)", False, None),
|
||||
("key_info_type", ToolParamType.STRING, "【重要信息类型】birthday(生日日期)/job(职业)/location(城市)/dream(人生理想)/family(家庭成员)/pet(宠物名字)。只有TA告诉你这些【具体事实】时才填,不要用other!", False, None),
|
||||
("key_info_value", ToolParamType.STRING, "【重要信息内容】如'11月23日'、'程序员'、'北京'。必须是具体的事实信息!", False, None),
|
||||
]
|
||||
available_for_llm = True
|
||||
history_ttl = 5
|
||||
|
||||
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""执行用户画像更新
|
||||
"""执行用户画像更新(异步后台执行,不阻塞回复)
|
||||
|
||||
Args:
|
||||
function_args: 工具参数
|
||||
@@ -52,6 +76,8 @@ class UserProfileTool(BaseTool):
|
||||
Returns:
|
||||
dict: 执行结果
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
# 提取参数
|
||||
target_user_id = function_args.get("target_user_id")
|
||||
@@ -67,61 +93,33 @@ class UserProfileTool(BaseTool):
|
||||
new_aliases = function_args.get("user_aliases", "")
|
||||
impression_hint = function_args.get("impression_hint", "")
|
||||
new_keywords = function_args.get("preference_keywords", "")
|
||||
new_score = function_args.get("affection_score")
|
||||
|
||||
# 从数据库获取现有用户画像
|
||||
existing_profile = await self._get_user_profile(target_user_id)
|
||||
key_info_type = function_args.get("key_info_type", "")
|
||||
key_info_value = function_args.get("key_info_value", "")
|
||||
|
||||
# 如果LLM没有传入任何有效参数,返回提示
|
||||
if not any([new_aliases, impression_hint, new_keywords, new_score is not None]):
|
||||
if not any([new_aliases, impression_hint, new_keywords, key_info_value]):
|
||||
return {
|
||||
"type": "info",
|
||||
"id": target_user_id,
|
||||
"content": "提示:需要提供至少一项更新内容(别名、印象描述、偏好关键词或好感分数)"
|
||||
"content": "提示:需要提供至少一项更新内容(别名、印象描述、偏好关键词或重要信息)"
|
||||
}
|
||||
|
||||
# 🎯 核心:使用relationship_tracker模型生成高质量印象
|
||||
final_impression = existing_profile.get("relationship_text", "")
|
||||
if impression_hint:
|
||||
final_impression = await self._generate_impression_with_personality(
|
||||
target_user_name=str(target_user_name) if target_user_name else str(target_user_id),
|
||||
impression_hint=str(impression_hint),
|
||||
existing_impression=str(existing_profile.get("relationship_text", "")),
|
||||
preference_keywords=str(new_keywords or existing_profile.get("preference_keywords", "")),
|
||||
)
|
||||
|
||||
# 构建最终画像
|
||||
final_profile = {
|
||||
"user_aliases": new_aliases if new_aliases else existing_profile.get("user_aliases", ""),
|
||||
"relationship_text": final_impression,
|
||||
"preference_keywords": new_keywords if new_keywords else existing_profile.get("preference_keywords", ""),
|
||||
"relationship_score": new_score if new_score is not None else existing_profile.get("relationship_score", global_config.affinity_flow.base_relationship_score),
|
||||
}
|
||||
|
||||
# 确保分数在有效范围内
|
||||
final_profile["relationship_score"] = max(0.0, min(1.0, float(final_profile["relationship_score"])))
|
||||
|
||||
# 更新数据库
|
||||
await self._update_user_profile_in_db(target_user_id, final_profile)
|
||||
|
||||
# 构建返回信息
|
||||
updates = []
|
||||
if final_profile.get("user_aliases"):
|
||||
updates.append(f"别名: {final_profile['user_aliases']}")
|
||||
if final_profile.get("relationship_text"):
|
||||
updates.append(f"印象: {final_profile['relationship_text'][:80]}...")
|
||||
if final_profile.get("preference_keywords"):
|
||||
updates.append(f"偏好: {final_profile['preference_keywords']}")
|
||||
if final_profile.get("relationship_score") is not None:
|
||||
updates.append(f"好感分: {final_profile['relationship_score']:.2f}")
|
||||
|
||||
result_text = f"已更新用户 {target_user_name} 的画像:\n" + "\n".join(updates)
|
||||
logger.info(f"用户画像更新成功: {target_user_id}")
|
||||
# 🎯 异步后台执行,不阻塞回复
|
||||
asyncio.create_task(self._background_update(
|
||||
target_user_id=target_user_id,
|
||||
target_user_name=str(target_user_name) if target_user_name else str(target_user_id),
|
||||
new_aliases=new_aliases,
|
||||
impression_hint=impression_hint,
|
||||
new_keywords=new_keywords,
|
||||
key_info_type=key_info_type,
|
||||
key_info_value=key_info_value,
|
||||
))
|
||||
|
||||
# 立即返回,让回复继续
|
||||
return {
|
||||
"type": "user_profile_update",
|
||||
"id": target_user_id,
|
||||
"content": result_text
|
||||
"content": f"正在后台更新对 {target_user_name} 的印象..."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -132,89 +130,374 @@ class UserProfileTool(BaseTool):
|
||||
"content": f"用户画像更新失败: {e!s}"
|
||||
}
|
||||
|
||||
async def _generate_impression_with_personality(
|
||||
async def _background_update(
|
||||
self,
|
||||
target_user_id: str,
|
||||
target_user_name: str,
|
||||
new_aliases: str,
|
||||
impression_hint: str,
|
||||
new_keywords: str,
|
||||
key_info_type: str = "",
|
||||
key_info_value: str = "",
|
||||
):
|
||||
"""后台执行用户画像更新"""
|
||||
try:
|
||||
# 从数据库获取现有用户画像
|
||||
existing_profile = await self._get_user_profile(target_user_id)
|
||||
|
||||
# 🎯 如果有关键信息,先保存(生日、职业等重要信息)
|
||||
if key_info_value:
|
||||
await self._add_key_fact(target_user_id, key_info_type or "other", key_info_value)
|
||||
logger.info(f"[后台] 已记录关键信息: {target_user_id}, {key_info_type}={key_info_value}")
|
||||
|
||||
# 获取最近的聊天记录
|
||||
chat_history_text = await self._get_recent_chat_history(target_user_id)
|
||||
|
||||
# 🎯 核心:使用relationship_tracker模型生成印象并决定好感度变化
|
||||
final_impression = existing_profile.get("relationship_text", "")
|
||||
affection_change = 0.0 # 好感度变化量
|
||||
|
||||
if impression_hint or chat_history_text:
|
||||
impression_result = await self._generate_impression_with_affection(
|
||||
target_user_name=target_user_name,
|
||||
impression_hint=impression_hint,
|
||||
existing_impression=str(existing_profile.get("relationship_text", "")),
|
||||
preference_keywords=str(new_keywords or existing_profile.get("preference_keywords", "")),
|
||||
chat_history=chat_history_text,
|
||||
current_score=float(existing_profile.get("relationship_score", _get_base_relationship_score())),
|
||||
)
|
||||
final_impression = impression_result.get("impression", final_impression)
|
||||
affection_change = impression_result.get("affection_change", 0.0)
|
||||
|
||||
# 计算新的好感度
|
||||
old_score = float(existing_profile.get("relationship_score", _get_base_relationship_score()))
|
||||
new_score = old_score + affection_change
|
||||
new_score = max(0.0, min(1.0, new_score)) # 确保在0-1范围内
|
||||
|
||||
# 构建最终画像
|
||||
final_profile = {
|
||||
"user_aliases": new_aliases if new_aliases else existing_profile.get("user_aliases", ""),
|
||||
"relationship_text": final_impression,
|
||||
"preference_keywords": new_keywords if new_keywords else existing_profile.get("preference_keywords", ""),
|
||||
"relationship_score": new_score,
|
||||
}
|
||||
|
||||
# 更新数据库
|
||||
await self._update_user_profile_in_db(target_user_id, final_profile)
|
||||
|
||||
logger.info(f"[后台] 用户画像更新成功: {target_user_id}, 好感度变化: {affection_change:+.2f}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[后台] 用户画像更新失败: {e}")
|
||||
|
||||
async def _add_key_fact(self, user_id: str, info_type: str, info_value: str):
|
||||
"""添加或更新关键信息(生日、职业等)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
info_type: 信息类型(birthday/job/location/dream/family/pet/other)
|
||||
info_value: 信息内容
|
||||
"""
|
||||
import orjson
|
||||
|
||||
try:
|
||||
# 验证 info_type
|
||||
valid_types = ["birthday", "job", "location", "dream", "family", "pet", "other"]
|
||||
if info_type not in valid_types:
|
||||
info_type = "other"
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
async with get_db_session() as session:
|
||||
stmt = select(UserRelationships).where(UserRelationships.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
# 解析现有的 key_facts
|
||||
try:
|
||||
facts = orjson.loads(existing.key_facts) if existing.key_facts else []
|
||||
except Exception:
|
||||
facts = []
|
||||
|
||||
if not isinstance(facts, list):
|
||||
facts = []
|
||||
|
||||
# 查找是否已有相同类型的信息
|
||||
found = False
|
||||
for i, fact in enumerate(facts):
|
||||
if isinstance(fact, dict) and fact.get("type") == info_type:
|
||||
# 更新现有记录
|
||||
facts[i] = {"type": info_type, "value": info_value}
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
# 添加新记录
|
||||
facts.append({"type": info_type, "value": info_value})
|
||||
|
||||
# 更新数据库
|
||||
existing.key_facts = orjson.dumps(facts).decode("utf-8")
|
||||
existing.last_updated = current_time
|
||||
else:
|
||||
# 创建新用户记录
|
||||
facts = [{"type": info_type, "value": info_value}]
|
||||
new_profile = UserRelationships(
|
||||
user_id=user_id,
|
||||
user_name=user_id,
|
||||
key_facts=orjson.dumps(facts).decode("utf-8"),
|
||||
first_met_time=current_time,
|
||||
last_updated=current_time
|
||||
)
|
||||
session.add(new_profile)
|
||||
|
||||
await session.commit()
|
||||
|
||||
# 清除缓存,确保下次查询获取最新数据
|
||||
try:
|
||||
from src.common.database.optimization.cache_manager import get_cache
|
||||
cache = await get_cache()
|
||||
cache_key = f"user_relationships:filter:[('user_id', '{user_id}')]"
|
||||
await cache.delete(cache_key)
|
||||
logger.debug(f"已清除用户关系缓存: {user_id}")
|
||||
except Exception as cache_err:
|
||||
logger.warning(f"清除缓存失败(不影响数据保存): {cache_err}")
|
||||
|
||||
logger.info(f"关键信息已保存: {user_id}, {info_type}={info_value}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存关键信息失败: {e}")
|
||||
# 不抛出异常,因为这是后台任务
|
||||
|
||||
async def _get_recent_chat_history(self, target_user_id: str, max_messages: int = 40) -> str:
|
||||
"""获取最近的聊天记录
|
||||
|
||||
Args:
|
||||
target_user_id: 目标用户ID
|
||||
max_messages: 最大消息数量
|
||||
|
||||
Returns:
|
||||
str: 格式化的聊天记录文本
|
||||
"""
|
||||
try:
|
||||
# 从 chat_stream 获取上下文
|
||||
if not self.chat_stream:
|
||||
logger.warning("chat_stream 未初始化,无法获取聊天记录")
|
||||
return ""
|
||||
|
||||
context = getattr(self.chat_stream, "context", None)
|
||||
if not context:
|
||||
logger.warning("chat_stream.context 不存在,无法获取聊天记录")
|
||||
return ""
|
||||
|
||||
# 获取最近的消息 - 使用正确的方法名 get_messages
|
||||
messages = context.get_messages(limit=max_messages, include_unread=True)
|
||||
if not messages:
|
||||
return ""
|
||||
|
||||
# 将 DatabaseMessages 对象转换为字典列表
|
||||
messages_dict = []
|
||||
for msg in messages:
|
||||
try:
|
||||
if hasattr(msg, 'to_dict'):
|
||||
messages_dict.append(msg.to_dict())
|
||||
elif hasattr(msg, '__dict__'):
|
||||
# 手动构建字典
|
||||
msg_dict = {
|
||||
"time": getattr(msg, "time", 0),
|
||||
"processed_plain_text": getattr(msg, "processed_plain_text", ""),
|
||||
"display_message": getattr(msg, "display_message", ""),
|
||||
}
|
||||
# 处理 user_info
|
||||
user_info = getattr(msg, "user_info", None)
|
||||
if user_info:
|
||||
msg_dict["user_info"] = {
|
||||
"user_id": getattr(user_info, "user_id", ""),
|
||||
"user_nickname": getattr(user_info, "user_nickname", ""),
|
||||
}
|
||||
# 处理 chat_info
|
||||
chat_info = getattr(msg, "chat_info", None)
|
||||
if chat_info:
|
||||
msg_dict["chat_info"] = {
|
||||
"platform": getattr(chat_info, "platform", ""),
|
||||
}
|
||||
messages_dict.append(msg_dict)
|
||||
except Exception as e:
|
||||
logger.warning(f"转换消息失败: {e}")
|
||||
continue
|
||||
|
||||
if not messages_dict:
|
||||
return ""
|
||||
|
||||
# 构建可读的消息文本
|
||||
readable_messages = await build_readable_messages(
|
||||
messages=messages_dict,
|
||||
replace_bot_name=True,
|
||||
timestamp_mode="normal_no_YMD",
|
||||
truncate=True
|
||||
)
|
||||
|
||||
return readable_messages or ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取聊天记录失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _generate_impression_with_affection(
|
||||
self,
|
||||
target_user_name: str,
|
||||
impression_hint: str,
|
||||
existing_impression: str,
|
||||
preference_keywords: str,
|
||||
) -> str:
|
||||
"""使用relationship_tracker模型生成有人设特色的印象描述
|
||||
chat_history: str,
|
||||
current_score: float,
|
||||
) -> dict[str, Any]:
|
||||
"""使用relationship_tracker模型生成印象并决定好感度变化
|
||||
|
||||
Args:
|
||||
target_user_name: 目标用户的名字
|
||||
impression_hint: 工具调用模型传入的简要观察
|
||||
existing_impression: 现有的印象描述
|
||||
preference_keywords: 用户的兴趣偏好
|
||||
chat_history: 最近的聊天记录
|
||||
current_score: 当前好感度分数
|
||||
|
||||
Returns:
|
||||
str: 生成的印象描述
|
||||
dict: {"impression": str, "affection_change": float}
|
||||
"""
|
||||
try:
|
||||
import orjson
|
||||
from json_repair import repair_json
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
|
||||
# 获取人设信息
|
||||
bot_name = global_config.bot.nickname
|
||||
personality_core = global_config.personality.personality_core
|
||||
personality_side = global_config.personality.personality_side
|
||||
# 获取人设信息(添加空值保护)
|
||||
bot_name = global_config.bot.nickname if global_config and global_config.bot else "Bot"
|
||||
personality_core = global_config.personality.personality_core if global_config and global_config.personality else ""
|
||||
personality_side = global_config.personality.personality_side if global_config and global_config.personality else ""
|
||||
reply_style = global_config.personality.reply_style if global_config and global_config.personality else ""
|
||||
|
||||
# 构建提示词
|
||||
prompt = f"""你是{bot_name},现在要记录你对一个人的印象。
|
||||
# 根据是否有旧印象决定任务类型
|
||||
is_first_impression = not existing_impression or len(existing_impression) < 20
|
||||
|
||||
prompt = f"""你是{bot_name},现在要记录你对"{target_user_name}"的印象。
|
||||
|
||||
## 你的人设
|
||||
## 你的核心人格
|
||||
{personality_core}
|
||||
|
||||
## 你的性格特点
|
||||
## 你的性格侧面
|
||||
{personality_side}
|
||||
|
||||
## 任务
|
||||
根据下面的观察要点,用你自己的语气和视角,写一段对"{target_user_name}"的印象描述。
|
||||
## 你的说话风格
|
||||
{reply_style}
|
||||
|
||||
## 观察到的要点
|
||||
{impression_hint}
|
||||
## 你之前对{target_user_name}的印象
|
||||
{existing_impression if existing_impression else "(这是你第一次记录对TA的印象)"}
|
||||
|
||||
## TA的兴趣爱好
|
||||
## 最近的聊天记录
|
||||
{chat_history if chat_history else "(无聊天记录)"}
|
||||
|
||||
## 这次观察到的新要点
|
||||
{impression_hint if impression_hint else "(无特别观察)"}
|
||||
|
||||
## {target_user_name}的兴趣爱好
|
||||
{preference_keywords if preference_keywords else "暂未了解"}
|
||||
|
||||
## 之前对TA的印象(如果有)
|
||||
{existing_impression if existing_impression else "这是第一次记录对TA的印象"}
|
||||
## 当前好感度
|
||||
{current_score:.2f} (范围0-1,0.3=普通认识,0.5=朋友,0.7=好友,0.9=挚友)
|
||||
|
||||
## 写作要求
|
||||
1. 用第一人称"我"来写,就像在写日记或者跟朋友聊天时描述一个人
|
||||
2. 用"{target_user_name}"或"TA"来称呼对方,不要用"该用户"、"此人"
|
||||
3. 写出你真实的、主观的感受,可以带情绪和直觉判断
|
||||
4. 如果有之前的印象,可以结合新观察进行补充或修正
|
||||
5. 长度控制在50-150字,自然流畅
|
||||
## 任务
|
||||
1. 根据聊天记录判断{target_user_name}的性别(男用"他",女用"她",无法判断用名字)
|
||||
2. {"写下你对这个人的第一印象" if is_first_impression else "在原有印象基础上,融入新的观察"}
|
||||
3. 决定好感度是否需要变化(大多数情况不需要)
|
||||
|
||||
请直接输出印象描述,不要加任何前缀或解释:"""
|
||||
## 印象写作要求
|
||||
- 用第一人称"我"来写
|
||||
- 根据判断的性别使用"他/她",或者直接用"{target_user_name}"
|
||||
- {"第一印象可以短一些,50-150字,写下初步感受" if is_first_impression else "在原有印象基础上补充新认识,150-300字"}
|
||||
- {"不要假装很熟,你们才刚认识" if is_first_impression else "体现出你们相处时间的积累"}
|
||||
- 写出这个人的特点、性格、给你的感觉
|
||||
|
||||
# 使用relationship_tracker模型
|
||||
## 好感度变化规则(极度严格!)
|
||||
- 范围:-0.03 到 +0.03,**但默认是0,90%以上的对话好感度应该不变**
|
||||
- 好感度是长期积累的结果,不是短短几句话就能改变的
|
||||
- **绝对不变(=0)的情况**:
|
||||
- 普通聊天、日常问候、闲聊
|
||||
- 正常的交流,即使聊得很开心
|
||||
- 分享日常、讨论话题
|
||||
- 简单的互相关心
|
||||
- **可能微涨(+0.01)的情况**(很少见):
|
||||
- 对方真正信任你,分享了很私密的心事或秘密
|
||||
- 在你困难时主动帮助
|
||||
- **可能涨(+0.02~0.03)的情况**(非常罕见):
|
||||
- 真正触动内心的深度交流
|
||||
- 长期相处后的重要情感突破
|
||||
- 记住:聊得好≠好感度增加,好感是需要长时间培养的
|
||||
|
||||
请严格按照以下JSON格式输出:
|
||||
{{
|
||||
"gender": "male/female/unknown",
|
||||
"impression": "你对{target_user_name}的印象...",
|
||||
"affection_change": 0,
|
||||
"change_reason": "无变化/变化原因"
|
||||
}}"""
|
||||
|
||||
# 使用relationship_tracker模型(添加空值保护)
|
||||
if not model_config or not model_config.model_task_config:
|
||||
raise ValueError("model_config 未初始化")
|
||||
|
||||
llm = LLMRequest(
|
||||
model_set=model_config.model_task_config.relationship_tracker,
|
||||
request_type="user_profile.impression_generator"
|
||||
request_type="user_profile.impression_and_affection"
|
||||
)
|
||||
|
||||
response, _ = await llm.generate_response_async(
|
||||
prompt=prompt,
|
||||
temperature=0.7,
|
||||
max_tokens=300,
|
||||
max_tokens=600,
|
||||
)
|
||||
|
||||
# 清理响应
|
||||
impression = response.strip()
|
||||
|
||||
# 如果响应为空或太短,回退到原始hint
|
||||
if not impression or len(impression) < 10:
|
||||
logger.warning(f"印象生成结果过短,使用原始hint: {impression_hint}")
|
||||
return impression_hint
|
||||
# 解析响应
|
||||
response = response.strip()
|
||||
try:
|
||||
result = orjson.loads(repair_json(response))
|
||||
impression = result.get("impression", "")
|
||||
affection_change = float(result.get("affection_change", 0))
|
||||
change_reason = result.get("change_reason", "")
|
||||
detected_gender = result.get("gender", "unknown")
|
||||
|
||||
logger.info(f"成功生成有人设特色的印象描述,长度: {len(impression)}")
|
||||
return impression
|
||||
# 限制好感度变化范围(严格:-0.03 到 +0.03)
|
||||
affection_change = max(-0.03, min(0.03, affection_change))
|
||||
|
||||
# 如果印象为空或太短,回退到hint
|
||||
if not impression or len(impression) < 10:
|
||||
logger.warning(f"印象生成结果过短,使用原始hint")
|
||||
impression = impression_hint or existing_impression
|
||||
|
||||
logger.info(f"印象更新: 用户性别判断={detected_gender}, 好感度变化={affection_change:+.3f}")
|
||||
if change_reason:
|
||||
logger.info(f"好感度变化原因: {change_reason}")
|
||||
|
||||
return {
|
||||
"impression": impression,
|
||||
"affection_change": affection_change
|
||||
}
|
||||
|
||||
except Exception as parse_error:
|
||||
logger.warning(f"解析JSON失败: {parse_error},尝试提取文本")
|
||||
# 如果JSON解析失败,尝试直接使用响应作为印象
|
||||
return {
|
||||
"impression": response if len(response) > 10 else (impression_hint or existing_impression),
|
||||
"affection_change": 0.0
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成印象描述失败,回退到原始hint: {e}")
|
||||
# 失败时回退到工具调用模型传入的hint
|
||||
return impression_hint
|
||||
logger.error(f"生成印象和好感度失败: {e}")
|
||||
# 失败时回退
|
||||
return {
|
||||
"impression": impression_hint or existing_impression,
|
||||
"affection_change": 0.0
|
||||
}
|
||||
|
||||
async def _get_user_profile(self, user_id: str) -> dict[str, Any]:
|
||||
"""从数据库获取用户现有画像
|
||||
@@ -232,12 +515,18 @@ class UserProfileTool(BaseTool):
|
||||
profile = result.scalar_one_or_none()
|
||||
|
||||
if profile:
|
||||
# 优先使用新字段 impression_text,如果没有则用旧字段 relationship_text
|
||||
impression = profile.impression_text or profile.relationship_text or ""
|
||||
return {
|
||||
"user_name": profile.user_name or user_id,
|
||||
"user_aliases": profile.user_aliases or "",
|
||||
"relationship_text": profile.relationship_text or "",
|
||||
"relationship_text": impression, # 兼容旧代码
|
||||
"impression_text": impression,
|
||||
"preference_keywords": profile.preference_keywords or "",
|
||||
"relationship_score": float(profile.relationship_score) if profile.relationship_score is not None else global_config.affinity_flow.base_relationship_score,
|
||||
"key_facts": profile.key_facts or "[]",
|
||||
"relationship_score": float(profile.relationship_score) if profile.relationship_score is not None else _get_base_relationship_score(),
|
||||
"relationship_stage": profile.relationship_stage or "stranger",
|
||||
"first_met_time": profile.first_met_time,
|
||||
}
|
||||
else:
|
||||
# 用户不存在,返回默认值
|
||||
@@ -245,8 +534,12 @@ class UserProfileTool(BaseTool):
|
||||
"user_name": user_id,
|
||||
"user_aliases": "",
|
||||
"relationship_text": "",
|
||||
"impression_text": "",
|
||||
"preference_keywords": "",
|
||||
"relationship_score": global_config.affinity_flow.base_relationship_score,
|
||||
"key_facts": "[]",
|
||||
"relationship_score": _get_base_relationship_score(),
|
||||
"relationship_stage": "stranger",
|
||||
"first_met_time": None,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户画像失败: {e}")
|
||||
@@ -254,8 +547,12 @@ class UserProfileTool(BaseTool):
|
||||
"user_name": user_id,
|
||||
"user_aliases": "",
|
||||
"relationship_text": "",
|
||||
"impression_text": "",
|
||||
"preference_keywords": "",
|
||||
"relationship_score": global_config.affinity_flow.base_relationship_score,
|
||||
"key_facts": "[]",
|
||||
"relationship_score": _get_base_relationship_score(),
|
||||
"relationship_stage": "stranger",
|
||||
"first_met_time": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -275,31 +572,81 @@ class UserProfileTool(BaseTool):
|
||||
result = await session.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
# 根据好感度自动计算关系阶段
|
||||
score = profile.get("relationship_score", 0.3)
|
||||
stage = self._calculate_relationship_stage(score)
|
||||
|
||||
if existing:
|
||||
# 更新现有记录
|
||||
existing.user_aliases = profile.get("user_aliases", "")
|
||||
existing.relationship_text = profile.get("relationship_text", "")
|
||||
# 同时更新新旧两个印象字段,保持兼容
|
||||
impression = profile.get("relationship_text", "")
|
||||
existing.relationship_text = impression
|
||||
existing.impression_text = impression
|
||||
existing.preference_keywords = profile.get("preference_keywords", "")
|
||||
existing.relationship_score = profile.get("relationship_score", global_config.affinity_flow.base_relationship_score)
|
||||
existing.relationship_score = score
|
||||
existing.relationship_stage = stage
|
||||
existing.last_impression_update = current_time
|
||||
existing.last_updated = current_time
|
||||
# 如果是首次认识,记录时间
|
||||
if not existing.first_met_time:
|
||||
existing.first_met_time = current_time
|
||||
else:
|
||||
# 创建新记录
|
||||
impression = profile.get("relationship_text", "")
|
||||
new_profile = UserRelationships(
|
||||
user_id=user_id,
|
||||
user_name=user_id,
|
||||
user_aliases=profile.get("user_aliases", ""),
|
||||
relationship_text=profile.get("relationship_text", ""),
|
||||
relationship_text=impression,
|
||||
impression_text=impression,
|
||||
preference_keywords=profile.get("preference_keywords", ""),
|
||||
relationship_score=profile.get("relationship_score", global_config.affinity_flow.base_relationship_score),
|
||||
relationship_score=score,
|
||||
relationship_stage=stage,
|
||||
first_met_time=current_time,
|
||||
last_impression_update=current_time,
|
||||
last_updated=current_time
|
||||
)
|
||||
session.add(new_profile)
|
||||
|
||||
await session.commit()
|
||||
logger.info(f"用户画像已更新到数据库: {user_id}")
|
||||
|
||||
# 清除缓存,确保下次查询获取最新数据
|
||||
try:
|
||||
from src.common.database.optimization.cache_manager import get_cache
|
||||
cache = await get_cache()
|
||||
cache_key = f"user_relationships:filter:[('user_id', '{user_id}')]"
|
||||
await cache.delete(cache_key)
|
||||
logger.debug(f"已清除用户关系缓存: {user_id}")
|
||||
except Exception as cache_err:
|
||||
logger.warning(f"清除缓存失败(不影响数据保存): {cache_err}")
|
||||
|
||||
logger.info(f"用户画像已更新到数据库: {user_id}, 阶段: {stage}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新用户画像到数据库失败: {e}")
|
||||
raise
|
||||
|
||||
def _calculate_relationship_stage(self, score: float) -> str:
|
||||
"""根据好感度分数计算关系阶段
|
||||
|
||||
Args:
|
||||
score: 好感度分数(0-1)
|
||||
|
||||
Returns:
|
||||
str: 关系阶段
|
||||
"""
|
||||
if score >= 0.9:
|
||||
return "bestie" # 挚友
|
||||
elif score >= 0.75:
|
||||
return "close_friend" # 好友
|
||||
elif score >= 0.6:
|
||||
return "friend" # 朋友
|
||||
elif score >= 0.4:
|
||||
return "familiar" # 熟人
|
||||
elif score >= 0.2:
|
||||
return "acquaintance" # 初识
|
||||
else:
|
||||
return "stranger" # 陌生人
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user