Files
WeFlow/electron/services/agentMemoryQuality.ts
2026-09-28 21:16:43 +08:00

100 lines
4.1 KiB
TypeScript

export type AgentMemoryQualityEntry = {
title: string
detail: string
category: string
}
export type AgentMemoryQualityIssue =
| 'non_operational_category'
| 'episodic_time_anchor'
| 'quoted_source_material'
| 'timeline_narrative'
| 'numeric_detail_density'
| 'profile_framing'
| 'missing_ai_guidance'
| 'multi_paragraph_detail'
| 'excessive_detail'
export type AgentMemoryQualityRejection<T> = {
entry: T
issues: AgentMemoryQualityIssue[]
}
export type AgentMemoryQualityPatchOperation<T extends AgentMemoryQualityEntry = AgentMemoryQualityEntry> =
| { op: 'add'; entry: T }
| { op: 'update'; targetId: string; entry: T }
| { op: 'delete'; targetId: string }
const OPERATIONAL_CATEGORIES = new Set([
'preference',
'project',
'communication',
'goal',
])
const AI_GUIDANCE_PATTERN = /(?:AI|Agent|回答|解释|建议|检索|核实|沟通|协作|呈现|表达|追问|引用|区分|避免|优先|应当|应该|需要|可以|不要|不能|适合|希望|偏好|关注)/iu
const ABSOLUTE_DATE_PATTERN = /(?:\b(?:19|20)\d{2}(?:[-/.年]\d{1,2})?(?:[-/.月]\d{1,2}日?)?\b|\b\d{1,2}[-/.月]\d{1,2}日?\b)/u
const CLOCK_TIME_PATTERN = /(?:\b\d{1,2}:\d{2}\b|(?:上午|下午|傍晚|晚上|凌晨)\s*\d{1,2}\s*[点时])/u
// 短术语、指代词和界面文案可以帮助描述触发条件;只有较长引文才像在保存原始聊天。
const QUOTED_MATERIAL_PATTERN = /(?:“[^”\n]{16,}”|"[^"\n]{16,}")/u
const TIMELINE_MARKER_PATTERN = /(?:起初|随后|后来|之后|此前|当时|期间|最终|最后|先是|紧接着)/gu
const PROFILE_TITLE_PATTERN = /^(?:你|用户)(?:的|在|会|有|是|正|曾|习惯|偏好|要求|希望|授权|保存|经常|通常)/u
export function inspectAutomaticMemoryEntry(entry: AgentMemoryQualityEntry): AgentMemoryQualityIssue[] {
const title = String(entry.title || '').trim()
const detail = String(entry.detail || '').trim()
const combined = `${title}\n${detail}`
const issues: AgentMemoryQualityIssue[] = []
if (!OPERATIONAL_CATEGORIES.has(String(entry.category || ''))) issues.push('non_operational_category')
if (ABSOLUTE_DATE_PATTERN.test(combined) || CLOCK_TIME_PATTERN.test(combined)) issues.push('episodic_time_anchor')
if (QUOTED_MATERIAL_PATTERN.test(combined)) issues.push('quoted_source_material')
if (Array.from(detail.matchAll(TIMELINE_MARKER_PATTERN)).length >= 2) issues.push('timeline_narrative')
if (PROFILE_TITLE_PATTERN.test(title)) issues.push('profile_framing')
const digitCount = Array.from(combined).filter((character) => /\d/u.test(character)).length
if (digitCount >= 8 || (combined.length > 0 && digitCount / combined.length > 0.06)) {
issues.push('numeric_detail_density')
}
if (!AI_GUIDANCE_PATTERN.test(detail)) issues.push('missing_ai_guidance')
if (/\n\s*\n/u.test(detail)) issues.push('multi_paragraph_detail')
if (detail.length > 140) issues.push('excessive_detail')
return Array.from(new Set(issues))
}
export function filterAutomaticMemoryEntries<T extends AgentMemoryQualityEntry>(entries: T[]): {
accepted: T[]
rejected: Array<AgentMemoryQualityRejection<T>>
} {
const accepted: T[] = []
const rejected: Array<AgentMemoryQualityRejection<T>> = []
for (const entry of entries) {
const issues = inspectAutomaticMemoryEntry(entry)
if (issues.length === 0) accepted.push(entry)
else rejected.push({ entry, issues })
}
return { accepted, rejected }
}
export function filterAutomaticMemoryPatch<T extends AgentMemoryQualityEntry>(
operations: Array<AgentMemoryQualityPatchOperation<T>>,
limit = 2,
): {
accepted: Array<AgentMemoryQualityPatchOperation<T>>
rejected: Array<AgentMemoryQualityRejection<T>>
} {
const accepted: Array<AgentMemoryQualityPatchOperation<T>> = []
const rejected: Array<AgentMemoryQualityRejection<T>> = []
for (const operation of operations) {
if (operation.op === 'delete') {
accepted.push(operation)
continue
}
const issues = inspectAutomaticMemoryEntry(operation.entry)
if (issues.length === 0) accepted.push(operation)
else rejected.push({ entry: operation.entry, issues })
}
return { accepted: accepted.slice(0, Math.max(0, limit)), rejected }
}