mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(topics): tolerate conflicting message assignments
This commit is contained in:
1 parent
c170b6f480
commit
35492a0ec6
2 files changed
+90
-40
No files matched your search
@@ -196,43 +196,88 @@ test('exact message assignments preserve interleaved topic membership beyond rep
|
||||
)
|
||||
})
|
||||
|
||||
test('one message cannot be assigned to multiple topics in the same block', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
applyTopicOperations(
|
||||
createEmptyTopicLedger(),
|
||||
test('the first primary assignment wins when the model repeats a message across topics', () => {
|
||||
const ledger = applyTopicOperations(
|
||||
createEmptyTopicLedger(),
|
||||
{
|
||||
operations: [
|
||||
{
|
||||
operations: [
|
||||
{
|
||||
operation: 'create',
|
||||
localId: 'first',
|
||||
title: '第一个话题',
|
||||
summary: '第一个话题。',
|
||||
state: 'active',
|
||||
evidence: [{ messageId: 1, timestamp: 100, role: 'primary' }],
|
||||
},
|
||||
{
|
||||
operation: 'create',
|
||||
localId: 'second',
|
||||
title: '第二个话题',
|
||||
summary: '第二个话题。',
|
||||
state: 'active',
|
||||
evidence: [{ messageId: 2, timestamp: 200, role: 'primary' }],
|
||||
},
|
||||
],
|
||||
assignments: [
|
||||
{ topicRef: 'first', messageIds: [1, 2] },
|
||||
{ topicRef: 'second', messageIds: [2] },
|
||||
],
|
||||
operation: 'create',
|
||||
localId: 'first',
|
||||
title: '第一个话题',
|
||||
summary: '第一个话题。',
|
||||
state: 'active',
|
||||
evidence: [{ messageId: 1, timestamp: 100, role: 'primary' }],
|
||||
},
|
||||
{
|
||||
sessionId: 'session',
|
||||
dayKey: '2026-08-09',
|
||||
localIdNamespace: 'block:0',
|
||||
currentMessages: messages,
|
||||
}
|
||||
),
|
||||
/multiple topics/
|
||||
operation: 'create',
|
||||
localId: 'second',
|
||||
title: '第二个话题',
|
||||
summary: '第二个话题。',
|
||||
state: 'active',
|
||||
evidence: [{ messageId: 3, timestamp: 8_000, role: 'primary' }],
|
||||
},
|
||||
],
|
||||
assignments: [
|
||||
{ topicRef: 'first', messageIds: [1, 2] },
|
||||
{ topicRef: 'second', messageIds: [2, 3] },
|
||||
],
|
||||
},
|
||||
{
|
||||
sessionId: 'session',
|
||||
dayKey: '2026-08-09',
|
||||
localIdNamespace: 'block:0',
|
||||
currentMessages: messages,
|
||||
}
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
materializeChatTopics(ledger, messages).map((topic) => topic.messageIds),
|
||||
[[1, 2], [3]]
|
||||
)
|
||||
})
|
||||
|
||||
test('primary assignments override conflicting representative evidence without failing the day', () => {
|
||||
const ledger = applyTopicOperations(
|
||||
createEmptyTopicLedger(),
|
||||
{
|
||||
operations: [
|
||||
{
|
||||
operation: 'create',
|
||||
localId: 'travel',
|
||||
title: '出行安排',
|
||||
summary: '讨论假期出行安排。',
|
||||
state: 'active',
|
||||
evidence: [{ messageId: 1, timestamp: 100, role: 'primary' }],
|
||||
},
|
||||
{
|
||||
operation: 'create',
|
||||
localId: 'hiking',
|
||||
title: '徒步备选',
|
||||
summary: '讨论徒步备选方案。',
|
||||
state: 'active',
|
||||
evidence: [
|
||||
{ messageId: 2, timestamp: 200, role: 'primary' },
|
||||
{ messageId: 3, timestamp: 8_000, role: 'supporting' },
|
||||
],
|
||||
},
|
||||
],
|
||||
assignments: [
|
||||
{ topicRef: 'travel', messageIds: [1, 2] },
|
||||
{ topicRef: 'hiking', messageIds: [3] },
|
||||
],
|
||||
},
|
||||
{
|
||||
sessionId: 'session',
|
||||
dayKey: '2026-08-09',
|
||||
localIdNamespace: 'block:0',
|
||||
currentMessages: messages,
|
||||
}
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
materializeChatTopics(ledger, messages).map((topic) => topic.messageIds),
|
||||
[[1, 2], [3]]
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ export function applyTopicOperations(
|
||||
const localTopicIds = new Map<string, string>()
|
||||
const mergedTopicIds = new Map<string, string>()
|
||||
const assignedTopicsByMessageId = new Map<number, string>()
|
||||
const evidenceMembershipFallbacks: Array<{ evidence: ChatTopicEvidence[]; topicId: string }> = []
|
||||
|
||||
for (const operation of response.operations) {
|
||||
validateNewEvidence(operation.evidence, currentMessageIds)
|
||||
@@ -151,6 +152,12 @@ export function applyTopicOperations(
|
||||
for (const messageId of assignment.messageIds) assignMessage(messageId, topicId)
|
||||
}
|
||||
|
||||
// assignments define primary membership. Evidence only fills omissions and never overrides
|
||||
// the model's explicit assignment, because one message may support a secondary topic.
|
||||
for (const fallback of evidenceMembershipFallbacks) {
|
||||
for (const item of fallback.evidence) assignMessage(item.messageId, fallback.topicId)
|
||||
}
|
||||
|
||||
for (const [messageId, topicId] of assignedTopicsByMessageId) {
|
||||
const topic = requireLedgerTopic(next, followMergedTopicId(topicId))
|
||||
topic.messageIds = deduplicateMessageIds([...topic.messageIds, messageId])
|
||||
@@ -177,7 +184,7 @@ export function applyTopicOperations(
|
||||
}
|
||||
|
||||
function assignEvidence(evidence: ChatTopicEvidence[], topicId: string): void {
|
||||
for (const item of evidence) assignMessage(item.messageId, topicId)
|
||||
evidenceMembershipFallbacks.push({ evidence, topicId })
|
||||
}
|
||||
|
||||
function assignMessage(messageId: number, topicId: string): void {
|
||||
@@ -185,11 +192,9 @@ export function applyTopicOperations(
|
||||
throw new Error(`Assigned message is not in the current block: ${messageId}`)
|
||||
}
|
||||
const resolvedTopicId = followMergedTopicId(topicId)
|
||||
const existingTopicId = assignedTopicsByMessageId.get(messageId)
|
||||
if (existingTopicId && followMergedTopicId(existingTopicId) !== resolvedTopicId) {
|
||||
throw new Error(`Message ${messageId} is assigned to multiple topics`)
|
||||
}
|
||||
assignedTopicsByMessageId.set(messageId, resolvedTopicId)
|
||||
// Keep the first explicit assignment (or evidence fallback) deterministically when the
|
||||
// model repeats an ID. A recoverable classification conflict must not fail the whole day.
|
||||
if (!assignedTopicsByMessageId.has(messageId)) assignedTopicsByMessageId.set(messageId, resolvedTopicId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user