Files
JChatGPT/src/test/kotlin/profile/UserProfileReducerTest.kt
T

369 lines
14 KiB
Kotlin

package top.jie65535.mirai.profile
import net.mamoe.mirai.message.data.MessageSourceKind
import top.jie65535.mirai.data.ChatMessageRecord
import top.jie65535.mirai.data.ContactGroupMemberHint
import top.jie65535.mirai.data.ContactProfileHint
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class UserProfileReducerTest {
@Test
fun addsItemFromTargetAuthoredEvidence() {
val batch = batchOf(
message(ref = 1, fromId = TARGET, text = "我平时会写 Kotlin"),
message(ref = 2, fromId = OTHER, text = "确实"),
)
val current = emptyProfile()
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.ADD,
category = ProfileCategory.INTEREST,
content = "持续关注 Kotlin 开发",
confidence = ProfileConfidence.MEDIUM,
evidenceRefs = listOf(1, 2),
)
),
summary = "关注 Kotlin 开发。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals(1, reduction.profile.items.size)
assertEquals("持续关注 Kotlin 开发", reduction.profile.items.single().content)
assertEquals(batch.endTime, reduction.profile.cursorTime)
assertEquals("关注 Kotlin 开发。", reduction.profile.summary)
}
@Test
fun skipsItemSupportedOnlyByAnotherUser() {
val batch = batchOf(message(ref = 1, fromId = OTHER, text = "我平时会写 Kotlin"))
val reduction = UserProfileReducer.reduce(
current = emptyProfile(),
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.ADD,
category = ProfileCategory.NOTABLE_FACT,
content = "从事 Kotlin 开发",
confidence = ProfileConfidence.HIGH,
evidenceRefs = listOf(1),
)
),
summary = "从事 Kotlin 开发。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertTrue(reduction.operations.isEmpty())
assertTrue(reduction.profile.items.isEmpty())
assertTrue(reduction.skippedOperations.single().contains("目标用户"))
assertEquals(batch.endTime, reduction.profile.cursorTime)
}
@Test
fun ignoresSummaryRewriteWhenThereIsNoValidOperation() {
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "今天天气不错"))
val current = emptyProfile().copy(summary = "原摘要")
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(summary = "凭空出现的新摘要"),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals("原摘要", reduction.profile.summary)
assertEquals(0, reduction.profile.version)
assertEquals(batch.endTime, reduction.profile.cursorTime)
}
@Test
fun conversationUpdateDoesNotAdvanceHistoricalCursor() {
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我平时会写 Kotlin"))
val current = emptyProfile()
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.ADD,
category = ProfileCategory.INTEREST,
content = "持续关注 Kotlin 开发",
confidence = ProfileConfidence.MEDIUM,
evidenceRefs = listOf(1),
)
),
summary = "关注 Kotlin 开发。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
advanceBackfillCursor = false,
)
assertEquals(current.cursorTime, reduction.profile.cursorTime)
assertEquals(1, reduction.profile.version)
}
@Test
fun resolvesPromptLocalItemReferenceWithoutExposingStoredId() {
val item = existingItem()
val current = emptyProfile().copy(items = listOf(item), reliable = true)
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我现在仍然关注 Kotlin"))
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.CONFIRM,
itemRef = "P1",
confidence = ProfileConfidence.HIGH,
evidenceRefs = listOf(1),
)
),
summary = "TARGET 仍然关注 Kotlin。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals(item.id, reduction.profile.items.single().id)
assertEquals(ProfileConfidence.HIGH, reduction.profile.items.single().confidence)
assertEquals("该用户仍然关注 Kotlin。", reduction.profile.summary)
val prompt = ProfilePromptStore.buildUserPrompt(current, batch)
assertTrue(prompt.contains("[P1]"))
assertFalse(prompt.contains(item.id))
}
@Test
fun rendersContactSnapshotAsNonEvidenceAndIncludesItemCount() {
val profile = emptyProfile().copy(items = listOf(existingItem()))
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我平时会写 Kotlin")).copy(
contactHints = mapOf(
TARGET to ContactProfileHint(
userId = TARGET,
nickname = "公开昵称",
sign = "公开签名",
memberships = listOf(
ContactGroupMemberHint(
groupId = 10,
groupName = "测试群",
nameCard = "群名片",
role = "admin",
)
),
)
),
)
val prompt = ProfilePromptStore.buildUserPrompt(profile, batch)
assertTrue(prompt.contains("当前条目数: 1"))
assertTrue(prompt.contains("联系人快照(辅助识别,不是画像证据)"))
assertTrue(prompt.contains("昵称=公开昵称"))
assertTrue(prompt.contains("群名片=群名片"))
assertTrue(ProfilePromptStore.systemPrompt.contains("不得仅凭昵称、群名片"))
}
@Test
fun keepsComprehensiveSummaryWhenBatchOnlyConfirmsOneItem() {
val interest = existingItem()
val work = UserProfileItem(
id = "work-item",
category = ProfileCategory.NOTABLE_FACT,
content = "从事上位机开发",
confidence = ProfileConfidence.HIGH,
firstSeenAt = 70,
lastConfirmedAt = 70,
)
val current = emptyProfile().copy(
summary = "从事上位机开发,并持续关注 Kotlin 生态。",
reliable = true,
items = listOf(interest, work),
)
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我还在关注 Kotlin"))
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.CONFIRM,
itemRef = "P1",
evidenceRefs = listOf(1),
)
),
summary = "仍在关注 Kotlin。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals(current.summary, reduction.profile.summary)
}
@Test
fun skipsUnknownPromptLocalItemReference() {
val current = emptyProfile().copy(items = listOf(existingItem()), reliable = true)
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我仍然关注 Kotlin"))
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.CONFIRM,
itemRef = "P2",
evidenceRefs = listOf(1),
)
),
summary = "关注 Kotlin。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals(current.items, reduction.profile.items)
assertTrue(reduction.operations.isEmpty())
assertTrue(reduction.skippedOperations.single().contains("P2"))
}
@Test
fun appliesSafeOperationAndSkipsInvalidEvidenceWithoutRewritingSummary() {
val current = emptyProfile().copy(summary = "原摘要")
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "我平时会写 Kotlin"))
val reduction = UserProfileReducer.reduce(
current = current,
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.ADD,
category = ProfileCategory.NOTABLE_FACT,
content = "日常使用 Kotlin 开发",
confidence = ProfileConfidence.MEDIUM,
evidenceRefs = listOf(1),
),
ProfileModelOperation(
action = ProfileOperationAction.ADD,
category = ProfileCategory.INTEREST,
content = "关注本地大模型",
confidence = ProfileConfidence.LOW,
evidenceRefs = listOf(323),
),
),
summary = "使用 Kotlin 并关注本地大模型。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals(1, reduction.operations.size)
assertEquals("日常使用 Kotlin 开发", reduction.profile.items.single().content)
assertTrue(reduction.skippedOperations.single().contains("e:323"))
assertEquals("原摘要", reduction.profile.summary)
}
@Test
fun removesTemporaryAliasesFromPersistedRelationshipText() {
val batch = batchOf(message(ref = 1, fromId = TARGET, text = "先找到下家再离职"))
val reduction = UserProfileReducer.reduce(
current = emptyProfile(),
batch = batch,
response = ProfileModelResponse(
operations = listOf(
ProfileModelOperation(
action = ProfileOperationAction.ADD,
category = ProfileCategory.RELATIONSHIP_NOTE,
content = "TARGET 在 U1 提及离职时会建议 U1 先找到下家",
confidence = ProfileConfidence.LOW,
relatedUserAlias = "U1",
evidenceRefs = listOf(1),
)
),
summary = "TARGET 会给 U1 提供务实建议。",
),
model = "test-model",
promptVersion = "test-prompt",
summaryMaxLength = 500,
)
assertEquals("本人在对方提及离职时会建议对方先找到下家", reduction.profile.items.single().content)
assertEquals("该用户会给其他用户提供务实建议。", reduction.profile.summary)
assertFalse(reduction.profile.items.single().content.contains("U1"))
}
private fun emptyProfile() = UserProfileSnapshot(
userId = TARGET,
cursorTime = 100,
snapshotEndTime = 1_000,
)
private fun existingItem() = UserProfileItem(
id = "b287070d-b7c0-4d50-a18c-fa8348932048",
category = ProfileCategory.INTEREST,
content = "关注 Kotlin 开发",
confidence = ProfileConfidence.MEDIUM,
firstSeenAt = 80,
lastConfirmedAt = 80,
)
private fun batchOf(vararg messages: ProfilePromptMessage) = ProfileHistoryBatch(
userId = TARGET,
startTime = 100,
endTime = 200,
messages = messages.toList(),
aliases = mapOf(TARGET to "TARGET", OTHER to "U1"),
inputHash = "hash",
)
private fun message(ref: Int, fromId: Long, text: String) = ProfilePromptMessage(
record = ChatMessageRecord(
botId = 1,
fromId = fromId,
targetId = 10,
ids = null,
internalIds = null,
time = 120 + ref,
kind = MessageSourceKind.GROUP,
code = text,
),
text = text,
evidenceRef = ref,
episodeIndex = 1,
)
companion object {
private const val TARGET = 100L
private const val OTHER = 200L
}
}