Compare commits

..

2 Commits

Author SHA1 Message Date
iven
b4e5af7a58 feat(growth): add memory decay + time-weighted scoring + remove dead frontend
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- Add effective_importance() with exponential time decay (30-day half-life)
  and access count boost for fair scoring of stale vs fresh memories
- Add SqliteStorage::decay_memories() for periodic maintenance:
  reduces stored importance per interval, archives (deletes) below threshold
- Update find() scoring to use time-decayed importance in sort
- Add DecayResult type and effective_importance re-export in lib.rs
- Remove dead frontend active-learning.ts (370 lines, zero imports)
2026-04-04 00:45:16 +08:00
iven
276ec3ca94 chore(desktop): remove dead active-learning frontend code
Zero imports across codebase — never wired to any UI or Tauri command.
Rust Growth crate handles memory/summary generation instead.
2026-04-04 00:38:13 +08:00
5 changed files with 102 additions and 434 deletions

View File

@@ -67,6 +67,7 @@ pub mod summarizer;
// Re-export main types for convenience
pub use types::{
DecayResult,
ExtractedMemory,
ExtractionConfig,
GrowthStats,
@@ -75,6 +76,7 @@ pub use types::{
RetrievalConfig,
RetrievalResult,
UriBuilder,
effective_importance,
};
pub use extractor::{LlmDriverForExtraction, MemoryExtractor};

View File

@@ -270,6 +270,74 @@ impl SqliteStorage {
Ok(())
}
/// Decay stale memories: reduce importance for long-unaccessed entries
/// and archive those below the minimum threshold.
///
/// - For every `decay_interval_days` since last access, importance drops by 1.
/// - Memories with importance ≤ `archive_threshold` are deleted.
pub async fn decay_memories(
&self,
decay_interval_days: u32,
archive_threshold: u8,
) -> crate::types::DecayResult {
// Step 1: Reduce importance of stale memories
let decay_result = sqlx::query(
r#"
UPDATE memories
SET importance = MAX(1, importance - CAST(
(julianday('now') - julianday(last_accessed)) / ? AS INTEGER
))
WHERE last_accessed < datetime('now', '-' || ? || ' days')
AND importance > 1
"#,
)
.bind(decay_interval_days)
.bind(decay_interval_days)
.execute(&self.pool)
.await;
let decayed = decay_result
.map(|r| r.rows_affected())
.unwrap_or(0);
// Step 2: Remove memories that fell below archive threshold
// and haven't been accessed in 90+ days
let archive_result = sqlx::query(
r#"
DELETE FROM memories
WHERE importance <= ?
AND last_accessed < datetime('now', '-90 days')
"#,
)
.bind(archive_threshold as i32)
.execute(&self.pool)
.await;
// Also clean up FTS entries for archived memories
let _ = sqlx::query(
r#"
DELETE FROM memories_fts
WHERE uri NOT IN (SELECT uri FROM memories)
"#,
)
.execute(&self.pool)
.await;
let archived = archive_result
.map(|r| r.rows_affected())
.unwrap_or(0);
if decayed > 0 || archived > 0 {
tracing::info!(
"[SqliteStorage] Memory decay: {} decayed, {} archived",
decayed,
archived
);
}
crate::types::DecayResult { decayed, archived }
}
}
impl sqlx::FromRow<'_, SqliteRow> for MemoryRow {
@@ -567,7 +635,7 @@ impl VikingStorage for SqliteStorage {
scorer.is_embedding_available()
};
let mut scored_entries: Vec<(f32, MemoryEntry)> = Vec::new();
let mut scored_entries: Vec<(f32, f32, MemoryEntry)> = Vec::new();
for row in rows {
let entry = self.row_to_entry(&row);
@@ -613,15 +681,18 @@ impl VikingStorage for SqliteStorage {
}
}
scored_entries.push((semantic_score, entry));
// Apply time decay to importance before final scoring
let time_decayed_importance = crate::types::effective_importance(&entry);
scored_entries.push((semantic_score, time_decayed_importance, entry));
}
// Sort by score (descending), then by importance and access count
// Sort by: semantic score → time-decayed importance access count (all descending)
scored_entries.sort_by(|a, b| {
b.0.partial_cmp(&a.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| b.1.importance.cmp(&a.1.importance))
.then_with(|| b.1.access_count.cmp(&a.1.access_count))
.then_with(|| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
.then_with(|| b.2.access_count.cmp(&a.2.access_count))
});
// Apply limit
@@ -629,7 +700,7 @@ impl VikingStorage for SqliteStorage {
scored_entries.truncate(limit);
}
Ok(scored_entries.into_iter().map(|(_, entry)| entry).collect())
Ok(scored_entries.into_iter().map(|(_, _, entry)| entry).collect())
}
async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>> {

View File

@@ -385,6 +385,29 @@ impl UriBuilder {
}
}
/// Result of a memory decay operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayResult {
/// Number of memories whose importance was reduced
pub decayed: u64,
/// Number of memories archived (importance fell below threshold)
pub archived: u64,
}
/// Compute effective importance with time decay.
///
/// Uses exponential decay: each 30-day period of non-access reduces
/// effective importance by ~50%. Frequently accessed memories decay slower
/// thanks to the access_count boost.
pub fn effective_importance(entry: &MemoryEntry) -> f32 {
let days_since = (Utc::now() - entry.last_accessed).num_days().max(0) as f32;
// Half-life: 30 days → decay factor per day ≈ 0.977
let time_decay = 0.977_f32.powf(days_since);
// Access boost: every 10 accesses add 1 to base importance (capped at 10)
let boosted = (entry.importance as f32 + entry.access_count as f32 / 10.0).min(10.0);
boosted * time_decay
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,369 +0,0 @@
/**
* 主动学习引擎 - 从用户交互中学习并改进 Agent 行为
*
* 提供学习事件记录、模式提取和建议生成功能。
* Phase 1: 内存存储Zustand 持久化
* Phase 2: SQLite + 向量化存储
*/
import {
type LearningEvent,
type LearningPattern,
type LearningSuggestion,
type LearningEventType,
type FeedbackSentiment,
} from '../types/active-learning';
import { generateRandomString } from './crypto-utils';
// === 常量 ===
const MAX_EVENTS = 1000;
const PATTERN_CONFIDENCE_THRESHOLD = 0.7;
const SUGGESTION_COOLDOWN_HOURS = 2;
// === 生成 ID ===
function generateEventId(): string {
return `le-${Date.now()}-${generateRandomString(8)}`;
}
// === 分析反馈情感 ===
export function analyzeSentiment(text: string): FeedbackSentiment {
const positive = ['好的', '很棒', '谢谢', '完美', 'excellent', '喜欢', '爱了', 'good', 'great', 'nice', '满意'];
const negative = ['不好', '差', '糟糕', '错误', 'wrong', 'bad', '不喜欢', '讨厌', '问题', '失败', 'fail', 'error'];
const lowerText = text.toLowerCase();
if (positive.some(w => lowerText.includes(w.toLowerCase()))) return 'positive';
if (negative.some(w => lowerText.includes(w.toLowerCase()))) return 'negative';
return 'neutral';
}
// === 分析学习类型 ===
export function analyzeEventType(text: string): LearningEventType {
const lowerText = text.toLowerCase();
if (lowerText.includes('纠正') || lowerText.includes('不对') || lowerText.includes('修改')) {
return 'correction';
}
if (lowerText.includes('喜欢') || lowerText.includes('偏好') || lowerText.includes('风格')) {
return 'preference';
}
if (lowerText.includes('场景') || lowerText.includes('上下文') || lowerText.includes('情况')) {
return 'context';
}
if (lowerText.includes('总是') || lowerText.includes('经常') || lowerText.includes('习惯')) {
return 'behavior';
}
return 'implicit';
}
// === 推断偏好 ===
export function inferPreference(feedback: string, sentiment: FeedbackSentiment): string {
if (sentiment === 'positive') {
if (feedback.includes('简洁')) return '用户偏好简洁的回复';
if (feedback.includes('详细')) return '用户偏好详细的回复';
if (feedback.includes('快速')) return '用户偏好快速响应';
return '用户对当前回复风格满意';
}
if (sentiment === 'negative') {
if (feedback.includes('太长')) return '用户偏好更短的回复';
if (feedback.includes('太短')) return '用户偏好更详细的回复';
if (feedback.includes('不准确')) return '用户偏好更准确的信息';
return '用户对当前回复风格不满意';
}
return '用户反馈中性';
}
// === 学习引擎类 ===
export class ActiveLearningEngine {
private events: LearningEvent[] = [];
private patterns: LearningPattern[] = [];
// Reserved for future learning suggestions feature
private suggestions: LearningSuggestion[] = [];
private initialized: boolean = false;
constructor() {
this.initialized = true;
}
/** Get current suggestions (reserved for future use) */
getSuggestions(): LearningSuggestion[] {
return this.suggestions;
}
/** Check if engine is initialized */
isInitialized(): boolean {
return this.initialized;
}
/**
* 记录学习事件
*/
recordEvent(
event: Omit<LearningEvent, 'id' | 'timestamp' | 'acknowledged' | 'appliedCount'>
): LearningEvent {
// 检查重复事件
const existing = this.events.find(e =>
e.agentId === event.agentId &&
e.messageId === event.messageId &&
e.type === event.type
);
if (existing) {
// 更新现有事件
existing.observation += ' | ' + event.observation;
existing.confidence = (existing.confidence + event.confidence) / 2;
existing.appliedCount++;
return existing;
}
// 创建新事件
const newEvent: LearningEvent = {
...event,
id: generateEventId(),
timestamp: Date.now(),
acknowledged: false,
appliedCount: 0,
};
this.events.push(newEvent);
this.extractPatterns(newEvent);
// 保持事件数量限制
if (this.events.length > MAX_EVENTS) {
this.events = this.events.slice(-MAX_EVENTS);
}
return newEvent;
}
/**
* 从反馈中学习
*/
learnFromFeedback(
agentId: string,
messageId: string,
feedback: string,
context?: string
): LearningEvent {
const sentiment = analyzeSentiment(feedback);
const type = analyzeEventType(feedback);
return this.recordEvent({
type,
agentId,
messageId,
trigger: context || 'User feedback',
observation: feedback,
context,
inferredPreference: inferPreference(feedback, sentiment),
confidence: sentiment === 'positive' ? 0.8 : sentiment === 'negative' ? 0.5 : 0.3,
});
}
/**
* 提取学习模式
*/
private extractPatterns(event: LearningEvent): void {
// 1. 正面反馈 -> 偏好正面回复
if (event.observation.includes('谢谢') || event.observation.includes('好的')) {
this.addPattern({
id: `pat-${Date.now()}-${generateRandomString(8)}`,
type: 'preference',
pattern: 'positive_response_preference',
description: '用户偏好正面回复风格',
examples: [event.observation],
confidence: 0.8,
agentId: event.agentId,
});
}
// 2. 纠正 -> 需要更精确
if (event.type === 'correction') {
this.addPattern({
id: `pat-${Date.now()}-${generateRandomString(8)}`,
type: 'preference',
pattern: 'precision_preference',
description: '用户对精确性有更高要求',
examples: [event.observation],
confidence: 0.9,
agentId: event.agentId,
});
}
// 3. 上下文相关 -> 场景偏好
if (event.context) {
this.addPattern({
id: `pat-${Date.now()}-${generateRandomString(8)}`,
type: 'context',
pattern: 'context_aware',
description: 'Agent 需要关注上下文',
examples: [event.context],
confidence: 0.6,
agentId: event.agentId,
});
}
}
/**
* 添加学习模式
*/
private addPattern(pattern: Omit<LearningPattern, 'updatedAt'>): void {
const existing = this.patterns.find(p =>
p.type === pattern.type &&
p.pattern === pattern.pattern &&
p.agentId === pattern.agentId
);
if (existing) {
// 增强置信度
existing.confidence = Math.min(1, existing.confidence + pattern.confidence * 0.1);
existing.examples.push(pattern.examples[0]);
existing.updatedAt = Date.now();
} else {
this.patterns.push({
...pattern,
updatedAt: Date.now(),
});
}
}
/**
* 生成学习建议
*/
generateSuggestions(agentId: string): LearningSuggestion[] {
const suggestions: LearningSuggestion[] = [];
const now = Date.now();
// 获取该 Agent 的模式
const agentPatterns = this.patterns.filter(p => p.agentId === agentId);
for (const pattern of agentPatterns) {
// 检查冷却时间
const hoursSinceUpdate = (now - (pattern.updatedAt || now)) / (1000 * 60 * 60);
if (hoursSinceUpdate < SUGGESTION_COOLDOWN_HOURS) continue;
// 检查置信度阈值
if (pattern.confidence < PATTERN_CONFIDENCE_THRESHOLD) continue;
// 生成建议
suggestions.push({
id: `sug-${Date.now()}-${generateRandomString(8)}`,
agentId,
type: pattern.type,
pattern: pattern.pattern,
suggestion: this.generateSuggestionContent(pattern),
confidence: pattern.confidence,
createdAt: now,
expiresAt: new Date(now + 7 * 24 * 60 * 60 * 1000),
dismissed: false,
});
}
return suggestions;
}
/**
* 生成建议内容
*/
private generateSuggestionContent(pattern: LearningPattern): string {
const templates: Record<string, string> = {
positive_response_preference:
'用户似乎偏好正面回复。建议在回复时保持积极和确认的语气。',
precision_preference:
'用户对精确性有更高要求。建议在提供信息时更加详细和准确。',
context_aware:
'Agent 需要关注上下文。建议在回复时考虑对话的背景和历史。',
};
return templates[pattern.pattern] || `观察到模式: ${pattern.pattern}`;
}
/**
* 获取统计信息
*/
getStats(agentId: string) {
const agentEvents = this.events.filter(e => e.agentId === agentId);
const agentPatterns = this.patterns.filter(p => p.agentId === agentId);
const eventsByType: Record<LearningEventType, number> = {
preference: 0,
correction: 0,
context: 0,
feedback: 0,
behavior: 0,
implicit: 0,
};
for (const event of agentEvents) {
eventsByType[event.type]++;
}
return {
totalEvents: agentEvents.length,
eventsByType,
totalPatterns: agentPatterns.length,
avgConfidence: agentPatterns.length > 0
? agentPatterns.reduce((sum, p) => sum + p.confidence, 0) / agentPatterns.length
: 0,
};
}
/**
* 获取所有事件
*/
getEvents(agentId?: string): LearningEvent[] {
if (agentId) {
return this.events.filter(e => e.agentId === agentId);
}
return [...this.events];
}
/**
* 获取所有模式
*/
getPatterns(agentId?: string): LearningPattern[] {
if (agentId) {
return this.patterns.filter(p => p.agentId === agentId);
}
return [...this.patterns];
}
/**
* 确认事件
*/
acknowledgeEvent(eventId: string): void {
const event = this.events.find(e => e.id === eventId);
if (event) {
event.acknowledged = true;
}
}
/**
* 清除事件
*/
clearEvents(agentId: string): void {
this.events = this.events.filter(e => e.agentId !== agentId);
this.patterns = this.patterns.filter(p => p.agentId !== agentId);
}
}
// === 单例实例 ===
let engineInstance: ActiveLearningEngine | null = null;
export function getActiveLearningEngine(): ActiveLearningEngine {
if (!engineInstance) {
engineInstance = new ActiveLearningEngine();
}
return engineInstance;
}
export function resetActiveLearningEngine(): void {
engineInstance = null;
}

View File

@@ -1,59 +0,0 @@
/**
* 主动学习引擎类型定义
*/
export type LearningEventType =
| 'preference'
| 'correction'
| 'context'
| 'feedback'
| 'behavior'
| 'implicit';
export type FeedbackSentiment = 'positive' | 'negative' | 'neutral';
export interface LearningEvent {
id: string;
type: LearningEventType;
agentId: string;
messageId: string;
timestamp: number;
trigger: string;
observation: string;
context?: string;
inferredPreference?: string;
confidence: number;
acknowledged: boolean;
appliedCount: number;
}
export interface LearningPattern {
id: string;
type: LearningEventType;
pattern: string;
description: string;
examples: string[];
confidence: number;
agentId: string;
updatedAt: number;
}
export interface LearningSuggestion {
id: string;
agentId: string;
type: LearningEventType;
pattern: string;
suggestion: string;
confidence: number;
createdAt: number;
expiresAt?: Date;
dismissed: boolean;
}
export interface ActiveLearningState {
events: LearningEvent[];
patterns: LearningPattern[];
suggestions: LearningSuggestion[];
isEnabled: boolean;
lastProcessed: number;
}