Files
mao1/anchor_decay.py

197 lines
7.5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Anchor Decay Engine
移植 Ombre-Brain 遗忘曲线算法模拟人类自然遗忘
定期扫描记忆文件计算活跃度得分自动归档低活跃记忆
"""
import os
import math
import yaml
import re
import random
from datetime import datetime, timedelta
VAULT_DIR = "/Users/fyah/Documents/如梦初醒/memory/test/Anchor-Memory"
ARCHIVE_DIR = os.path.join(VAULT_DIR, "Archive")
# 遗忘曲线参数(来自 Ombre-Brain
DECAY_LAMBDA = 0.05 # 衰减速率
THRESHOLD = 0.3 # 归档阈值
EMOTION_BASE = 1.0 # 情感基础权重
AROUSAL_BOOST = 0.8 # 唤醒度加成
RESURRECTION_CHANCE = 0.1 # 返场概率 (10%):被遗忘的记忆有几率“诈尸”复活
def parse_yaml_frontmatter(content):
"""极简 YAML 解析"""
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 2:
try:
return yaml.safe_load(parts[1]), parts[2]
except:
pass
return {}, content
def update_yaml_frontmatter(content, metadata):
"""更新 YAML 头"""
if content.startswith("---"):
parts = content.split("---", 2)
if len(parts) >= 2:
return f"---\n{yaml.dump(metadata, allow_unicode=True)}---{parts[2]}"
return content
def calc_time_weight(days_since: float) -> float:
"""新鲜度加成1.0 + e^(-t/36), t 为小时"""
hours = days_since * 24.0
return 1.0 + 1.0 * math.exp(-hours / 36.0)
def calculate_score(metadata: dict) -> float:
"""计算记忆活跃度得分"""
if not isinstance(metadata, dict):
return 0.0
# 钉选/永久记忆不衰减
if metadata.get("pinned") or metadata.get("type") == "permanent":
return 999.0
importance = max(1, min(10, int(metadata.get("importance", 5))))
activation_count = max(1.0, float(metadata.get("activation_count", 1)))
# 计算天数
last_active_str = metadata.get("last_active", metadata.get("date", ""))
try:
# 兼容多种日期格式
last_active = datetime.strptime(str(last_active_str), "%Y-%m-%d %H:%M")
days_since = max(0.0, (datetime.now() - last_active).total_seconds() / 86400)
except:
days_since = 30.0
# 情感权重
try:
arousal = max(0.0, min(1.0, float(metadata.get("arousal", 0.3))))
except:
arousal = 0.3
emotion_weight = EMOTION_BASE + arousal * AROUSAL_BOOST
# 时间权重
time_weight = calc_time_weight(days_since)
# 短期/长期分离
if days_since <= 3.0:
combined_weight = time_weight * 0.7 + emotion_weight * 0.3
else:
combined_weight = emotion_weight * 0.7 + time_weight * 0.3
# 核心公式
base_score = (
importance
* (activation_count ** 0.3)
* math.exp(-DECAY_LAMBDA * days_since)
* combined_weight
)
return base_score
def run_decay():
"""执行遗忘扫描"""
print(f"[*] 开始扫描遗忘曲线: {VAULT_DIR}")
archived_count = 0
# 确保归档目录存在
os.makedirs(ARCHIVE_DIR, exist_ok=True)
# 遍历所有 .md 文件
for root, dirs, files in os.walk(VAULT_DIR):
# 跳过归档目录本身
if "Archive" in root:
continue
for file in files:
if not file.endswith(".md"):
continue
filepath = os.path.join(root, file)
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
metadata, body = parse_yaml_frontmatter(content)
if not metadata:
continue
score = calculate_score(metadata)
# 如果得分低于阈值,且当前状态不是已归档,则归档
if score < THRESHOLD and metadata.get("status") != "archived":
# 随机返场判定
if random.random() < RESURRECTION_CHANCE:
print(f" [✨ 返场] {file} (得分: {score:.3f}) - 触发随机复活!")
metadata["status"] = "resurrected"
metadata["last_resurrected"] = datetime.now().strftime("%Y-%m-%d %H:%M")
new_content = update_yaml_frontmatter(content, metadata)
with open(filepath, "w", encoding="utf-8") as f:
f.write(new_content)
else:
# 正常归档逻辑
metadata["status"] = "archived"
metadata["archived_date"] = datetime.now().strftime("%Y-%m-%d %H:%M")
metadata["decay_score"] = round(score, 3)
metadata["original_path"] = os.path.relpath(filepath, VAULT_DIR)
new_content = update_yaml_frontmatter(content, metadata)
with open(filepath, "w", encoding="utf-8") as f:
f.write(new_content)
# 移动文件到归档目录
dest_path = os.path.join(ARCHIVE_DIR, file)
os.rename(filepath, dest_path)
archived_count += 1
print(f" [归档] {file} (得分: {score:.3f})")
except Exception as e:
print(f" [!] 处理失败 {file}: {e}")
print(f"[+] 遗忘扫描完成。共归档 {archived_count} 个文件。")
print(f"[+] 遗忘扫描完成。共归档 {archived_count} 个文件。")
# --- 第二阶段:扫描归档目录,寻找返场机会 ---
print(f"[*] 扫描归档目录寻找返场机会: {ARCHIVE_DIR}")
resurrected_count = 0
if os.path.exists(ARCHIVE_DIR):
for file in os.listdir(ARCHIVE_DIR):
if not file.endswith(".md"):
continue
filepath = os.path.join(ARCHIVE_DIR, file)
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
metadata, body = parse_yaml_frontmatter(content)
# 只针对已归档的文件
if metadata.get("status") == "archived":
# 掷骰子10% 概率复活
if random.random() < RESURRECTION_CHANCE:
print(f" [✨ 返场] {file} - 从归档中复活!")
metadata["status"] = "resurrected"
metadata["last_resurrected"] = datetime.now().strftime("%Y-%m-%d %H:%M")
new_content = update_yaml_frontmatter(content, metadata)
with open(filepath, "w", encoding="utf-8") as f:
f.write(new_content)
# 恢复原路径
orig_path = metadata.get("original_path", file)
dest_path = os.path.join(VAULT_DIR, orig_path)
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
os.rename(filepath, dest_path)
resurrected_count += 1
except Exception as e:
print(f" [!] 处理归档文件 {file} 失败: {e}")
print(f"[+] 返场扫描完成。共复活 {resurrected_count} 个文件。")
if __name__ == "__main__":
run_decay()