mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-09-15 02:56:10 +08:00
conversation: deduplicate profile context
This commit is contained in:
@@ -48,6 +48,7 @@ internal data class ConversationCache(
|
|||||||
val lastActivityAt: Int,
|
val lastActivityAt: Int,
|
||||||
val replyIndex: ReplyIndex,
|
val replyIndex: ReplyIndex,
|
||||||
val imageIndex: ImageIndex,
|
val imageIndex: ImageIndex,
|
||||||
|
val profileInjectionState: UserProfileInjectionState,
|
||||||
) {
|
) {
|
||||||
fun isExpired(ttlSeconds: Int): Boolean =
|
fun isExpired(ttlSeconds: Int): Boolean =
|
||||||
OffsetDateTime.now().toEpochSecond().toInt() - lastActivityAt > ttlSeconds
|
OffsetDateTime.now().toEpochSecond().toInt() - lastActivityAt > ttlSeconds
|
||||||
@@ -153,7 +154,7 @@ internal object ConversationContext {
|
|||||||
return prompt.toString()
|
return prompt.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHistory(event: MessageEvent): String {
|
fun getHistory(event: MessageEvent, profileInjectionState: UserProfileInjectionState): String {
|
||||||
val imageIndex = activeImageIndex(event.subject.id)
|
val imageIndex = activeImageIndex(event.subject.id)
|
||||||
if (!JChatGPT.includeHistory) {
|
if (!JChatGPT.includeHistory) {
|
||||||
return formatRecordContent(event.message, event.subject, imageIndex)
|
return formatRecordContent(event.message, event.subject, imageIndex)
|
||||||
@@ -162,10 +163,20 @@ internal object ConversationContext {
|
|||||||
.minusMinutes(PluginConfig.historyWindowMin.toLong())
|
.minusMinutes(PluginConfig.historyWindowMin.toLong())
|
||||||
.toEpochSecond()
|
.toEpochSecond()
|
||||||
.toInt()
|
.toInt()
|
||||||
return getAfterHistory(beforeTimestamp, event, PluginConfig.historyMessageLimit)
|
return getAfterHistory(
|
||||||
|
time = beforeTimestamp,
|
||||||
|
event = event,
|
||||||
|
profileInjectionState = profileInjectionState,
|
||||||
|
limit = PluginConfig.historyMessageLimit,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAfterHistory(time: Int, event: MessageEvent, limit: Int? = null): String {
|
fun getAfterHistory(
|
||||||
|
time: Int,
|
||||||
|
event: MessageEvent,
|
||||||
|
profileInjectionState: UserProfileInjectionState,
|
||||||
|
limit: Int? = null,
|
||||||
|
): String {
|
||||||
if (!JChatGPT.includeHistory) return ""
|
if (!JChatGPT.includeHistory) return ""
|
||||||
val history = try {
|
val history = try {
|
||||||
ChatHistoryStore.query(
|
ChatHistoryStore.query(
|
||||||
@@ -191,7 +202,7 @@ internal object ConversationContext {
|
|||||||
val replyIndex = activeReplyIndex(event.subject.id)
|
val replyIndex = activeReplyIndex(event.subject.id)
|
||||||
val imageIndex = activeImageIndex(event.subject.id)
|
val imageIndex = activeImageIndex(event.subject.id)
|
||||||
if (event is GroupMessageEvent) {
|
if (event is GroupMessageEvent) {
|
||||||
appendUserProfileContext(result, history, event)
|
appendUserProfileContext(result, history, event, profileInjectionState)
|
||||||
result.appendLine("## 近期群消息(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)")
|
result.appendLine("## 近期群消息(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)")
|
||||||
history.forEach { record ->
|
history.forEach { record ->
|
||||||
val showSender = lastUserId != record.fromId
|
val showSender = lastUserId != record.fromId
|
||||||
@@ -201,7 +212,7 @@ internal object ConversationContext {
|
|||||||
lastTime = record.time.toLong()
|
lastTime = record.time.toLong()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
appendPrivateUserContext(result, event)
|
appendPrivateUserContext(result, event, profileInjectionState)
|
||||||
result.appendLine("## 近期对话(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)")
|
result.appendLine("## 近期对话(更早已隐藏,行首[n]为消息编号;正文[图片n]/[表情包n]中的n为识图或图片编辑编号)")
|
||||||
history.forEach { record ->
|
history.forEach { record ->
|
||||||
val showSender = lastUserId != record.fromId
|
val showSender = lastUserId != record.fromId
|
||||||
@@ -273,6 +284,7 @@ internal object ConversationContext {
|
|||||||
target: StringBuilder,
|
target: StringBuilder,
|
||||||
history: List<ChatMessageRecord>,
|
history: List<ChatMessageRecord>,
|
||||||
event: GroupMessageEvent,
|
event: GroupMessageEvent,
|
||||||
|
profileInjectionState: UserProfileInjectionState,
|
||||||
) {
|
) {
|
||||||
if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return
|
if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return
|
||||||
val candidateIds = buildList {
|
val candidateIds = buildList {
|
||||||
@@ -283,15 +295,21 @@ internal object ConversationContext {
|
|||||||
.distinct()
|
.distinct()
|
||||||
.take(PluginConfig.profileAutoInjectMaxUsers.coerceIn(1, 10))
|
.take(PluginConfig.profileAutoInjectMaxUsers.coerceIn(1, 10))
|
||||||
.toList()
|
.toList()
|
||||||
val profiles = if (PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled &&
|
val shouldLoadProfiles = PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled
|
||||||
UserProfileStore.isAvailable
|
val uncertainProfileIds = mutableSetOf<Long>()
|
||||||
) {
|
val profiles = if (shouldLoadProfiles && UserProfileStore.isAvailable) {
|
||||||
candidateIds.mapNotNull { userId ->
|
candidateIds.mapNotNull { userId ->
|
||||||
runCatching { UserProfileStore.load(userId) }
|
runCatching { UserProfileStore.load(userId) }
|
||||||
.onFailure { JChatGPT.logger.warning("读取用户画像失败: user=$userId", it) }
|
.onFailure {
|
||||||
|
uncertainProfileIds += userId
|
||||||
|
JChatGPT.logger.warning("读取用户画像失败: user=$userId", it)
|
||||||
|
}
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
}
|
}
|
||||||
} else emptyList()
|
} else {
|
||||||
|
if (shouldLoadProfiles) uncertainProfileIds += candidateIds
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
val favorability = if (PluginConfig.enableFavorabilitySystem) {
|
val favorability = if (PluginConfig.enableFavorabilitySystem) {
|
||||||
candidateIds.mapNotNull { id -> PluginData.userFavorability[id]?.let { id to it } }.toMap()
|
candidateIds.mapNotNull { id -> PluginData.userFavorability[id]?.let { id to it } }.toMap()
|
||||||
} else emptyMap()
|
} else emptyMap()
|
||||||
@@ -301,26 +319,41 @@ internal object ConversationContext {
|
|||||||
val names = candidateIds.associateWith { id ->
|
val names = candidateIds.associateWith { id ->
|
||||||
event.group[id]?.nameCardOrNick ?: snapshotNames[id] ?: id.toString()
|
event.group[id]?.nameCardOrNick ?: snapshotNames[id] ?: id.toString()
|
||||||
}
|
}
|
||||||
|
val renderedEntries = UserProfileContextRenderer.renderEntries(
|
||||||
|
profiles = profiles,
|
||||||
|
favorabilityByUserId = favorability,
|
||||||
|
displayNames = names,
|
||||||
|
activeUserIds = candidateIds.toSet(),
|
||||||
|
summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars,
|
||||||
|
)
|
||||||
|
val comparableCandidateIds = candidateIds.filter { userId ->
|
||||||
|
userId !in uncertainProfileIds || !profileInjectionState.hasSeen(userId)
|
||||||
|
}
|
||||||
target.append(
|
target.append(
|
||||||
UserProfileContextRenderer.render(
|
profileInjectionState.renderChanges(
|
||||||
profiles = profiles,
|
candidateUserIds = comparableCandidateIds,
|
||||||
favorabilityByUserId = favorability,
|
renderedEntries = renderedEntries,
|
||||||
displayNames = names,
|
sectionTitle = "你对相关群友的认识",
|
||||||
activeUserIds = candidateIds.toSet(),
|
|
||||||
summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun appendPrivateUserContext(target: StringBuilder, event: MessageEvent) {
|
private fun appendPrivateUserContext(
|
||||||
|
target: StringBuilder,
|
||||||
|
event: MessageEvent,
|
||||||
|
profileInjectionState: UserProfileInjectionState,
|
||||||
|
) {
|
||||||
if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return
|
if (!PluginConfig.profileAutoInjectEnabled && !PluginConfig.enableFavorabilitySystem) return
|
||||||
val userId = event.sender.id
|
val userId = event.sender.id
|
||||||
val profiles = if (PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled &&
|
val shouldLoadProfile = PluginConfig.profileEnabled && PluginConfig.profileAutoInjectEnabled
|
||||||
UserProfileStore.isAvailable
|
var profileReadUncertain = shouldLoadProfile && !UserProfileStore.isAvailable
|
||||||
) {
|
val profiles = if (shouldLoadProfile && UserProfileStore.isAvailable) {
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
runCatching { UserProfileStore.load(userId) }
|
runCatching { UserProfileStore.load(userId) }
|
||||||
.onFailure { JChatGPT.logger.warning("读取用户画像失败: user=$userId", it) }
|
.onFailure {
|
||||||
|
profileReadUncertain = true
|
||||||
|
JChatGPT.logger.warning("读取用户画像失败: user=$userId", it)
|
||||||
|
}
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
)
|
)
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
@@ -330,13 +363,21 @@ internal object ConversationContext {
|
|||||||
val snapshotName = runCatching {
|
val snapshotName = runCatching {
|
||||||
ContactSnapshotStore.loadDisplayName(event.bot.id, null, userId)
|
ContactSnapshotStore.loadDisplayName(event.bot.id, null, userId)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
val renderedEntries = UserProfileContextRenderer.renderEntries(
|
||||||
|
profiles = profiles,
|
||||||
|
favorabilityByUserId = favorability,
|
||||||
|
displayNames = mapOf(userId to (snapshotName ?: event.senderName)),
|
||||||
|
activeUserIds = setOf(userId),
|
||||||
|
summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars,
|
||||||
|
)
|
||||||
target.append(
|
target.append(
|
||||||
UserProfileContextRenderer.render(
|
profileInjectionState.renderChanges(
|
||||||
profiles = profiles,
|
candidateUserIds = if (profileReadUncertain && profileInjectionState.hasSeen(userId)) {
|
||||||
favorabilityByUserId = favorability,
|
emptyList()
|
||||||
displayNames = mapOf(userId to (snapshotName ?: event.senderName)),
|
} else {
|
||||||
activeUserIds = setOf(userId),
|
listOf(userId)
|
||||||
summaryMaxChars = PluginConfig.profileAutoInjectSummaryMaxChars,
|
},
|
||||||
|
renderedEntries = renderedEntries,
|
||||||
sectionTitle = "你对对方的认识",
|
sectionTitle = "你对对方的认识",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -146,16 +146,22 @@ internal object ConversationEngine {
|
|||||||
JChatGPT.logger.info("使用缓存的对话上下文,包含 ${cache.history.size} 条互动消息")
|
JChatGPT.logger.info("使用缓存的对话上下文,包含 ${cache.history.size} 条互动消息")
|
||||||
cache.history
|
cache.history
|
||||||
} else mutableListOf()
|
} else mutableListOf()
|
||||||
|
val profileInjectionState = cache?.profileInjectionState?.takeIf { reuseCache }
|
||||||
|
?: UserProfileInjectionState()
|
||||||
|
|
||||||
if (history.isEmpty() || cache == null) {
|
if (history.isEmpty() || cache == null) {
|
||||||
val prompt = ConversationContext.getSystemPrompt(currentEvent)
|
val prompt = ConversationContext.getSystemPrompt(currentEvent)
|
||||||
if (PluginConfig.logPrompt) JChatGPT.logger.info("Prompt: $prompt")
|
if (PluginConfig.logPrompt) JChatGPT.logger.info("Prompt: $prompt")
|
||||||
history += ChatMessage(ChatRole.System, prompt)
|
history += ChatMessage(ChatRole.System, prompt)
|
||||||
val historyText = ConversationContext.getHistory(currentEvent)
|
val historyText = ConversationContext.getHistory(currentEvent, profileInjectionState)
|
||||||
JChatGPT.logger.info("注入聊天记录:\n$historyText")
|
JChatGPT.logger.info("注入聊天记录:\n$historyText")
|
||||||
history += ChatMessage.User(historyText)
|
history += ChatMessage.User(historyText)
|
||||||
} else {
|
} else {
|
||||||
val newMessages = ConversationContext.getAfterHistory(cache.lastActivityAt, currentEvent)
|
val newMessages = ConversationContext.getAfterHistory(
|
||||||
|
time = cache.lastActivityAt,
|
||||||
|
event = currentEvent,
|
||||||
|
profileInjectionState = profileInjectionState,
|
||||||
|
)
|
||||||
JChatGPT.logger.info("补充聊天记录:\n$newMessages")
|
JChatGPT.logger.info("补充聊天记录:\n$newMessages")
|
||||||
history += ChatMessage.User(
|
history += ChatMessage.User(
|
||||||
if (resumedWait == null) {
|
if (resumedWait == null) {
|
||||||
@@ -281,6 +287,7 @@ internal object ConversationEngine {
|
|||||||
startedAt = startedAt,
|
startedAt = startedAt,
|
||||||
event = currentEvent,
|
event = currentEvent,
|
||||||
pendingTrigger = pendingEvent != null,
|
pendingTrigger = pendingEvent != null,
|
||||||
|
profileInjectionState = profileInjectionState,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
done = false
|
done = false
|
||||||
@@ -288,7 +295,13 @@ internal object ConversationEngine {
|
|||||||
if (PluginConfig.enableContextCache) {
|
if (PluginConfig.enableContextCache) {
|
||||||
ConversationContext.saveCache(
|
ConversationContext.saveCache(
|
||||||
subjectId,
|
subjectId,
|
||||||
ConversationCache(history, startedAt, replyIndex, imageIndex),
|
ConversationCache(
|
||||||
|
history = history,
|
||||||
|
lastActivityAt = startedAt,
|
||||||
|
replyIndex = replyIndex,
|
||||||
|
imageIndex = imageIndex,
|
||||||
|
profileInjectionState = profileInjectionState,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
JChatGPT.logger.debug("已保存对话上下文到缓存")
|
JChatGPT.logger.debug("已保存对话上下文到缓存")
|
||||||
}
|
}
|
||||||
@@ -311,6 +324,7 @@ internal object ConversationEngine {
|
|||||||
startedAt = startedAt,
|
startedAt = startedAt,
|
||||||
event = currentEvent,
|
event = currentEvent,
|
||||||
pendingTrigger = true,
|
pendingTrigger = true,
|
||||||
|
profileInjectionState = profileInjectionState,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
done = false
|
done = false
|
||||||
@@ -492,6 +506,7 @@ internal object ConversationEngine {
|
|||||||
startedAt: Int,
|
startedAt: Int,
|
||||||
event: MessageEvent,
|
event: MessageEvent,
|
||||||
pendingTrigger: Boolean,
|
pendingTrigger: Boolean,
|
||||||
|
profileInjectionState: UserProfileInjectionState,
|
||||||
): String = buildString {
|
): String = buildString {
|
||||||
appendLine("## 系统提示")
|
appendLine("## 系统提示")
|
||||||
append("本次运行最多还剩").append(remainingRounds).appendLine("轮。")
|
append("本次运行最多还剩").append(remainingRounds).appendLine("轮。")
|
||||||
@@ -499,8 +514,16 @@ internal object ConversationEngine {
|
|||||||
appendLine("如果没有什么要做的,可以提前结束。")
|
appendLine("如果没有什么要做的,可以提前结束。")
|
||||||
if (pendingTrigger) appendLine("运行期间收到了新的显式触发,请优先处理水位后的新消息。")
|
if (pendingTrigger) appendLine("运行期间收到了新的显式触发,请优先处理水位后的新消息。")
|
||||||
appendLine("当前时间:${dateTimeFormatter.format(OffsetDateTime.now())}")
|
appendLine("当前时间:${dateTimeFormatter.format(OffsetDateTime.now())}")
|
||||||
val messages = ConversationContext.getAfterHistory(startedAt, event).ifEmpty {
|
val messages = ConversationContext.getAfterHistory(
|
||||||
if (pendingTrigger && !JChatGPT.includeHistory) ConversationContext.getHistory(event) else ""
|
time = startedAt,
|
||||||
|
event = event,
|
||||||
|
profileInjectionState = profileInjectionState,
|
||||||
|
).ifEmpty {
|
||||||
|
if (pendingTrigger && !JChatGPT.includeHistory) {
|
||||||
|
ConversationContext.getHistory(event, profileInjectionState)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (messages.isNotEmpty()) append("## 以下是上次运行至今的新消息\n\n$messages")
|
if (messages.isNotEmpty()) append("## 以下是上次运行至今的新消息\n\n$messages")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package top.jie65535.mirai.conversation
|
||||||
|
|
||||||
|
internal class UserProfileInjectionState {
|
||||||
|
private val renderedByUserId = mutableMapOf<Long, String?>()
|
||||||
|
private var guidanceInjected = false
|
||||||
|
|
||||||
|
fun hasSeen(userId: Long): Boolean = renderedByUserId.containsKey(userId)
|
||||||
|
|
||||||
|
fun renderChanges(
|
||||||
|
candidateUserIds: Collection<Long>,
|
||||||
|
renderedEntries: Map<Long, String>,
|
||||||
|
sectionTitle: String,
|
||||||
|
): String {
|
||||||
|
val changedEntries = mutableListOf<String>()
|
||||||
|
val clearedUserIds = mutableListOf<Long>()
|
||||||
|
|
||||||
|
candidateUserIds.distinct().forEach { userId ->
|
||||||
|
val current = renderedEntries[userId]
|
||||||
|
if (!renderedByUserId.containsKey(userId)) {
|
||||||
|
renderedByUserId[userId] = current
|
||||||
|
if (current != null) changedEntries += current
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderedByUserId[userId] == current) return@forEach
|
||||||
|
renderedByUserId[userId] = current
|
||||||
|
if (current == null) clearedUserIds += userId else changedEntries += current
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedEntries.isEmpty() && clearedUserIds.isEmpty()) return ""
|
||||||
|
|
||||||
|
val firstInjection = !guidanceInjected
|
||||||
|
guidanceInjected = true
|
||||||
|
return buildString {
|
||||||
|
append("## ").append(sectionTitle)
|
||||||
|
if (!firstInjection) append("(更新)")
|
||||||
|
appendLine()
|
||||||
|
if (firstInjection) {
|
||||||
|
appendLine(
|
||||||
|
"好感度、代号和主观印象代表你的关系状态;画像认识来自可修正的历史归纳。" +
|
||||||
|
"仅在当前话题相关时自然运用,不要逐条复述或提及信息来源。"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
appendLine("以下仅列出新增或发生变化的条目;同一用户以本段为准,未列出的认识保持不变。")
|
||||||
|
}
|
||||||
|
changedEntries.forEach(::append)
|
||||||
|
clearedUserIds.forEach { userId ->
|
||||||
|
append("- 用户(").append(userId)
|
||||||
|
.appendLine("):当前已无可用的关系状态或画像认识,请忽略此前对应信息。")
|
||||||
|
}
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,30 @@ object UserProfileContextRenderer {
|
|||||||
summaryMaxChars: Int,
|
summaryMaxChars: Int,
|
||||||
sectionTitle: String = "你对相关群友的认识",
|
sectionTitle: String = "你对相关群友的认识",
|
||||||
): String {
|
): String {
|
||||||
|
val entries = renderEntries(
|
||||||
|
profiles = profiles,
|
||||||
|
favorabilityByUserId = favorabilityByUserId,
|
||||||
|
displayNames = displayNames,
|
||||||
|
activeUserIds = activeUserIds,
|
||||||
|
summaryMaxChars = summaryMaxChars,
|
||||||
|
)
|
||||||
|
if (entries.isEmpty()) return ""
|
||||||
|
|
||||||
|
return buildString {
|
||||||
|
append("## ").appendLine(sectionTitle)
|
||||||
|
appendLine(CONTEXT_GUIDANCE)
|
||||||
|
entries.values.forEach(::append)
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun renderEntries(
|
||||||
|
profiles: List<UserProfileSnapshot>,
|
||||||
|
favorabilityByUserId: Map<Long, FavorabilityInfo>,
|
||||||
|
displayNames: Map<Long, String>,
|
||||||
|
activeUserIds: Set<Long>,
|
||||||
|
summaryMaxChars: Int,
|
||||||
|
): Map<Long, String> {
|
||||||
val profilesByUserId = profiles
|
val profilesByUserId = profiles
|
||||||
.filter { profile ->
|
.filter { profile ->
|
||||||
profile.reliable &&
|
profile.reliable &&
|
||||||
@@ -20,50 +44,53 @@ object UserProfileContextRenderer {
|
|||||||
val userIds = activeUserIds.filter { userId ->
|
val userIds = activeUserIds.filter { userId ->
|
||||||
userId in profilesByUserId || favorabilityByUserId[userId]?.hasVisibleContext() == true
|
userId in profilesByUserId || favorabilityByUserId[userId]?.hasVisibleContext() == true
|
||||||
}
|
}
|
||||||
if (userIds.isEmpty()) return ""
|
if (userIds.isEmpty()) return emptyMap()
|
||||||
|
|
||||||
val maxChars = summaryMaxChars.coerceAtLeast(50)
|
val maxChars = summaryMaxChars.coerceAtLeast(50)
|
||||||
return buildString {
|
return buildMap {
|
||||||
append("## ").appendLine(sectionTitle)
|
|
||||||
appendLine("好感度、代号和主观印象代表你的关系状态;画像认识来自可修正的历史归纳。仅在当前话题相关时自然运用,不要逐条复述或提及信息来源。")
|
|
||||||
userIds.forEach { userId ->
|
userIds.forEach { userId ->
|
||||||
val profile = profilesByUserId[userId]
|
val profile = profilesByUserId[userId]
|
||||||
val favorability = favorabilityByUserId[userId]
|
val favorability = favorabilityByUserId[userId]
|
||||||
val name = favorability?.name.orEmpty().ifBlank {
|
val name = favorability?.name.orEmpty().ifBlank {
|
||||||
displayNames[userId].orEmpty().ifBlank { userId.toString() }
|
displayNames[userId].orEmpty().ifBlank { userId.toString() }
|
||||||
}
|
}
|
||||||
append("- ").append(name).append('(').append(userId).append(')')
|
put(userId, buildString {
|
||||||
favorability?.takeIf { it.hasVisibleContext() }?.let { info ->
|
append("- ").append(name).append('(').append(userId).append(')')
|
||||||
append(" | 好感度").append(if (info.value >= 0) "+" else "").append(info.value)
|
favorability?.takeIf { it.hasVisibleContext() }?.let { info ->
|
||||||
if (info.tags.isNotEmpty()) append(" | 标签:").append(info.tags.joinToString("、"))
|
append(" | 好感度").append(if (info.value >= 0) "+" else "").append(info.value)
|
||||||
if (info.impression.isNotBlank()) append(" | 主观印象:").append(info.impression.normalized())
|
if (info.tags.isNotEmpty()) append(" | 标签:").append(info.tags.joinToString("、"))
|
||||||
}
|
if (info.impression.isNotBlank()) append(" | 主观印象:").append(info.impression.normalized())
|
||||||
profile?.summary?.let(ProfilePersistentText::summaryForDisplay)
|
}
|
||||||
?.takeIf(String::isNotBlank)?.let { summary ->
|
profile?.summary?.let(ProfilePersistentText::summaryForDisplay)
|
||||||
append(" | 画像认识")
|
?.takeIf(String::isNotBlank)?.let { summary ->
|
||||||
append("(").append(profile.items.size).append("条):")
|
append(" | 画像认识")
|
||||||
.append(summary.normalized().take(maxChars))
|
append("(").append(profile.items.size).append("条):")
|
||||||
} ?: profile?.takeIf { it.items.isNotEmpty() }?.let {
|
.append(summary.normalized().take(maxChars))
|
||||||
append(" | 画像认识:已有").append(it.items.size).append("条记录,可用 queryUserProfile 查询详情")
|
} ?: profile?.takeIf { it.items.isNotEmpty() }?.let {
|
||||||
}
|
append(" | 画像认识:已有").append(it.items.size)
|
||||||
|
.append("条记录,可用 queryUserProfile 查询详情")
|
||||||
|
}
|
||||||
|
|
||||||
profile?.items?.asSequence()
|
profile?.items?.asSequence()
|
||||||
?.filter { item ->
|
?.filter { item ->
|
||||||
item.category == ProfileCategory.RELATIONSHIP_NOTE &&
|
item.category == ProfileCategory.RELATIONSHIP_NOTE &&
|
||||||
item.relatedUserId != null && item.relatedUserId in activeUserIds
|
item.relatedUserId != null && item.relatedUserId in activeUserIds
|
||||||
}
|
|
||||||
?.take(2)
|
|
||||||
?.forEach { item ->
|
|
||||||
val relatedId = checkNotNull(item.relatedUserId)
|
|
||||||
val relatedName = favorabilityByUserId[relatedId]?.name.orEmpty().ifBlank {
|
|
||||||
displayNames[relatedId].orEmpty().ifBlank { relatedId.toString() }
|
|
||||||
}
|
}
|
||||||
append(";与").append(relatedName).append(":")
|
?.take(2)
|
||||||
.append(ProfilePersistentText.itemForDisplay(item.content, relationship = true).normalized())
|
?.forEach { item ->
|
||||||
}
|
val relatedId = checkNotNull(item.relatedUserId)
|
||||||
appendLine()
|
val relatedName = favorabilityByUserId[relatedId]?.name.orEmpty().ifBlank {
|
||||||
|
displayNames[relatedId].orEmpty().ifBlank { relatedId.toString() }
|
||||||
|
}
|
||||||
|
append(";与").append(relatedName).append(":")
|
||||||
|
.append(
|
||||||
|
ProfilePersistentText.itemForDisplay(item.content, relationship = true)
|
||||||
|
.normalized()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
appendLine()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
appendLine()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +98,8 @@ object UserProfileContextRenderer {
|
|||||||
value != 0 || name.isNotBlank() || tags.isNotEmpty() || impression.isNotBlank()
|
value != 0 || name.isNotBlank() || tags.isNotEmpty() || impression.isNotBlank()
|
||||||
|
|
||||||
private fun String.normalized(): String = trim().replace(Regex("\\s+"), " ")
|
private fun String.normalized(): String = trim().replace(Regex("\\s+"), " ")
|
||||||
|
|
||||||
|
private const val CONTEXT_GUIDANCE =
|
||||||
|
"好感度、代号和主观印象代表你的关系状态;画像认识来自可修正的历史归纳。" +
|
||||||
|
"仅在当前话题相关时自然运用,不要逐条复述或提及信息来源。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package top.jie65535.mirai.conversation
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContains
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
|
||||||
|
class UserProfileInjectionStateTest {
|
||||||
|
@Test
|
||||||
|
fun injectsVisibleEntriesOnce() {
|
||||||
|
val state = UserProfileInjectionState()
|
||||||
|
val entries = mapOf(
|
||||||
|
100L to "- 小明(100) | 好感度+3\n",
|
||||||
|
200L to "- 小王(200) | 画像认识:熟悉 Kotlin\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
val initial = state.renderChanges(entries.keys, entries, "你对相关群友的认识")
|
||||||
|
|
||||||
|
assertContains(initial, "## 你对相关群友的认识")
|
||||||
|
assertContains(initial, entries.getValue(100L).trim())
|
||||||
|
assertContains(initial, entries.getValue(200L).trim())
|
||||||
|
assertEquals("", state.renderChanges(entries.keys, entries, "你对相关群友的认识"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emitsOnlyNewAndChangedUsers() {
|
||||||
|
val state = UserProfileInjectionState()
|
||||||
|
state.renderChanges(
|
||||||
|
candidateUserIds = listOf(100L, 200L),
|
||||||
|
renderedEntries = mapOf(
|
||||||
|
100L to "- 小明(100) | 好感度+3\n",
|
||||||
|
200L to "- 小王(200) | 好感度+1\n",
|
||||||
|
),
|
||||||
|
sectionTitle = "你对相关群友的认识",
|
||||||
|
)
|
||||||
|
|
||||||
|
val update = state.renderChanges(
|
||||||
|
candidateUserIds = listOf(100L, 200L, 300L),
|
||||||
|
renderedEntries = mapOf(
|
||||||
|
100L to "- 小明(100) | 好感度+3\n",
|
||||||
|
200L to "- 小王(200) | 好感度+5\n",
|
||||||
|
300L to "- 小李(300) | 主观印象:表达直接\n",
|
||||||
|
),
|
||||||
|
sectionTitle = "你对相关群友的认识",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertContains(update, "## 你对相关群友的认识(更新)")
|
||||||
|
assertContains(update, "小王(200) | 好感度+5")
|
||||||
|
assertContains(update, "小李(300) | 主观印象:表达直接")
|
||||||
|
assertFalse(update.contains("小明(100)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun explicitlyClearsPreviouslyVisibleContext() {
|
||||||
|
val state = UserProfileInjectionState()
|
||||||
|
state.renderChanges(
|
||||||
|
candidateUserIds = listOf(100L),
|
||||||
|
renderedEntries = mapOf(100L to "- 小明(100) | 好感度-2\n"),
|
||||||
|
sectionTitle = "你对相关群友的认识",
|
||||||
|
)
|
||||||
|
|
||||||
|
val update = state.renderChanges(
|
||||||
|
candidateUserIds = listOf(100L),
|
||||||
|
renderedEntries = emptyMap(),
|
||||||
|
sectionTitle = "你对相关群友的认识",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertContains(update, "用户(100)")
|
||||||
|
assertContains(update, "请忽略此前对应信息")
|
||||||
|
assertEquals(
|
||||||
|
"",
|
||||||
|
state.renderChanges(listOf(100L), emptyMap(), "你对相关群友的认识"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun delaysGuidanceUntilAnInvisibleUserGetsContext() {
|
||||||
|
val state = UserProfileInjectionState()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"",
|
||||||
|
state.renderChanges(listOf(100L), emptyMap(), "你对对方的认识"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val firstVisible = state.renderChanges(
|
||||||
|
candidateUserIds = listOf(100L),
|
||||||
|
renderedEntries = mapOf(100L to "- 小明(100) | 画像认识:熟悉数据库\n"),
|
||||||
|
sectionTitle = "你对对方的认识",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertContains(firstVisible, "## 你对对方的认识\n")
|
||||||
|
assertFalse(firstVisible.contains("(更新)"))
|
||||||
|
assertContains(firstVisible, "仅在当前话题相关时自然运用")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package top.jie65535.mirai.profile
|
|||||||
import top.jie65535.mirai.data.FavorabilityInfo
|
import top.jie65535.mirai.data.FavorabilityInfo
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertContains
|
import kotlin.test.assertContains
|
||||||
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
|
|
||||||
class UserProfileContextRendererTest {
|
class UserProfileContextRendererTest {
|
||||||
@@ -80,6 +81,21 @@ class UserProfileContextRendererTest {
|
|||||||
assertContains(rendered, "主观印象:长期活跃的老群友")
|
assertContains(rendered, "主观印象:长期活跃的老群友")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun perUserEntriesIgnoreFavorabilityFieldsHiddenFromTheModel() {
|
||||||
|
fun render(reasons: List<String>) = UserProfileContextRenderer.renderEntries(
|
||||||
|
profiles = emptyList(),
|
||||||
|
favorabilityByUserId = mapOf(
|
||||||
|
100L to FavorabilityInfo(userId = 100, value = 5, reasons = reasons)
|
||||||
|
),
|
||||||
|
displayNames = mapOf(100L to "小明"),
|
||||||
|
activeUserIds = setOf(100L),
|
||||||
|
summaryMaxChars = 300,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(render(listOf("旧原因")), render(listOf("更新但不展示的原因")))
|
||||||
|
}
|
||||||
|
|
||||||
private fun relationship(id: String, relatedUserId: Long, content: String) = UserProfileItem(
|
private fun relationship(id: String, relatedUserId: Long, content: String) = UserProfileItem(
|
||||||
id = id,
|
id = id,
|
||||||
category = ProfileCategory.RELATIONSHIP_NOTE,
|
category = ProfileCategory.RELATIONSHIP_NOTE,
|
||||||
|
|||||||
Reference in New Issue
Block a user