1274 lines
46 KiB
Python
1274 lines
46 KiB
Python
"""
|
||
Agent Memory V2 - 核心数据模型
|
||
|
||
包含:
|
||
- 枚举类型: MemoryLayer, MemoryStatus, SourceType, UpdateType,
|
||
SchemaStatus, TriggerType, IntentionPriority, MetaCognitionTag, GapType
|
||
- 核心模型: MemoryNode (统一基类)
|
||
- 子类模型: VersionedFact, SchemaNode, IntentionNode
|
||
- 辅助模型: KnowledgeGap, TemporalEvent, UserTimeline, LifeStage
|
||
- 输出协议: MemoryContextPackage 及其子结构
|
||
- 兼容别名: MemoryEntry, MemoryMetadata, MemoryScore (保留旧接口)
|
||
"""
|
||
|
||
from enum import Enum
|
||
from typing import Optional, Dict, Any, List
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
import uuid
|
||
|
||
|
||
# ============================================================
|
||
# 枚举类型
|
||
# ============================================================
|
||
|
||
class MemoryLayer(str, Enum):
|
||
"""
|
||
七层记忆模型 (V2 Memory Hierarchy)
|
||
|
||
L1 RAW — 原始对话层 (Append-Only, 向量数据库)
|
||
L2 FACT — 原子事实层 (版本化不可变记录)
|
||
L3 SUMMARY — 会话摘要层
|
||
L4 IDENTITY — 身份画像层 (核心画像)
|
||
L5 KNOWLEDGE — 知识图谱层 (实体/关系/主题, Graph 层, 暂未实现)
|
||
L6 SCHEMA — 心智模型层 (抽象叙事模板, Graph 层)
|
||
L7 INTENTION — 前瞻意图层 (未来待触发的意图, Graph 层)
|
||
|
||
存储分界: L0-L4 在 VDB, L5-L7 在 Graph (ultra 模式)
|
||
"""
|
||
L0_BASIC_INFO = "l0_basic_info"
|
||
L1_RAW = "l1_raw"
|
||
L2_FACT = "l2_fact"
|
||
L3_SUMMARY = "l3_summary"
|
||
L4_IDENTITY = "l4_identity"
|
||
L5_KNOWLEDGE = "l5_knowledge"
|
||
L6_SCHEMA = "l6_schema"
|
||
L7_INTENTION = "l7_intention"
|
||
|
||
# --- 旧编号兼容别名(已存储数据中的值) ---
|
||
# v2 → v3 重编号兼容
|
||
L6_IDENTITY = "l4_identity" # 旧 l6_identity → 新 l4_identity
|
||
L4_KNOWLEDGE = "l5_knowledge" # 旧 l4_knowledge → 新 l5_knowledge
|
||
L5_SCHEMA = "l6_schema" # 旧 l5_schema → 新 l6_schema
|
||
# v1 → v2 旧兼容(仍需保留)
|
||
L4_5_SCHEMA = "l6_schema" # 旧 l4_5_schema → 新 l6_schema
|
||
L5_IDENTITY = "l4_identity" # 旧 l5_identity → 新 l4_identity
|
||
L6_INTENTION = "l7_intention" # 旧 l6_intention → 新 l7_intention
|
||
|
||
# --- V1 兼容别名 ---
|
||
PROFILE = "l4_identity"
|
||
DIALOGUE = "l2_fact"
|
||
SUMMARY = "l3_summary"
|
||
KNOWLEDGE = "l5_knowledge"
|
||
RAW = "l1_raw"
|
||
|
||
@classmethod
|
||
def from_string(cls, value: str) -> "MemoryLayer":
|
||
"""从字符串创建 MemoryLayer,支持 V1/V2/V3 命名和旧编号"""
|
||
value = value.lower().strip()
|
||
# 旧编号 → 新编号映射(兼容已存储数据的 payload)
|
||
legacy_mapping = {
|
||
# v2 → v3
|
||
"l6_identity": cls.L4_IDENTITY,
|
||
"l4_knowledge": cls.L5_KNOWLEDGE,
|
||
"l5_schema": cls.L6_SCHEMA,
|
||
# v1 → v2 → v3
|
||
"l4_5_schema": cls.L6_SCHEMA,
|
||
"l5_identity": cls.L4_IDENTITY,
|
||
"l6_intention": cls.L7_INTENTION,
|
||
}
|
||
if value in legacy_mapping:
|
||
return legacy_mapping[value]
|
||
# V1 兼容映射
|
||
v1_mapping = {
|
||
"profile": cls.L4_IDENTITY,
|
||
"dialogue": cls.L2_FACT,
|
||
"summary": cls.L3_SUMMARY,
|
||
"knowledge": cls.L5_KNOWLEDGE,
|
||
"raw": cls.L1_RAW,
|
||
}
|
||
if value in v1_mapping:
|
||
return v1_mapping[value]
|
||
for layer in cls:
|
||
if layer.value == value:
|
||
return layer
|
||
raise ValueError(f"Invalid memory layer: {value}")
|
||
|
||
@classmethod
|
||
def all_layers(cls) -> List["MemoryLayer"]:
|
||
"""返回所有记忆层 (不含别名)"""
|
||
return [
|
||
cls.L0_BASIC_INFO,
|
||
cls.L1_RAW, cls.L2_FACT, cls.L3_SUMMARY,
|
||
cls.L4_IDENTITY, cls.L5_KNOWLEDGE,
|
||
cls.L6_SCHEMA, cls.L7_INTENTION,
|
||
]
|
||
|
||
|
||
|
||
class ContentType(str, Enum):
|
||
"""记忆内容类型(保留用于旧代码兼容,MemoryNode 不再使用此字段)"""
|
||
FACT = "fact"
|
||
SUMMARY = "summary"
|
||
ENTITY = "entity"
|
||
SCHEMA = "schema"
|
||
PROFILE = "profile"
|
||
INTENTION = "intention"
|
||
RAW = "raw"
|
||
HYPOTHESIS = "hypothesis"
|
||
|
||
|
||
class MemoryStatus(str, Enum):
|
||
"""记忆状态"""
|
||
ACTIVE = "active" # 当前有效
|
||
SUPERSEDED = "superseded" # 已被新版本取代(EVOLVE 演化链上的旧节点)
|
||
NEGATED = "negated" # 已被否定
|
||
CONFLICTED = "conflicted" # 存在冲突
|
||
SHADOW = "shadow" # 影子状态:低置信度推断 或 reconcile DELETE 逻辑删除
|
||
# 两种场景都不参与召回(is_latest 通常为 False)
|
||
ARCHIVED = "archived" # 已归档
|
||
|
||
|
||
class SourceType(str, Enum):
|
||
"""信息来源类型"""
|
||
EXPLICIT = "explicit" # 用户明确陈述
|
||
INFERRED = "inferred" # 系统推断
|
||
COMPOSITE = "composite" # 多证据综合
|
||
|
||
|
||
class UpdateType(str, Enum):
|
||
"""版本化事实的更新类型"""
|
||
OVERRIDE = "override" # 新值替代旧值
|
||
SUPPLEMENT = "supplement" # 补充信息
|
||
TEMPORAL = "temporal" # 时间窗口变化
|
||
NEGATE = "negate" # 否定旧信息
|
||
CONFLICT = "conflict" # 新旧冲突
|
||
|
||
|
||
class SchemaStatus(str, Enum):
|
||
"""Schema 状态"""
|
||
FORMING = "forming" # 形成中 (证据不足)
|
||
STABLE = "stable" # 稳定 (多证据支撑)
|
||
EVOLVING = "evolving" # 演化中 (有新证据修改)
|
||
DEPRECATED = "deprecated" # 已过时
|
||
|
||
|
||
class TriggerType(str, Enum):
|
||
"""意图触发类型"""
|
||
TIME_BASED = "time_based" # 时间触发
|
||
EVENT_BASED = "event_based" # 事件触发
|
||
|
||
|
||
class IntentionPriority(str, Enum):
|
||
"""意图优先级"""
|
||
HIGH = "high"
|
||
MEDIUM = "medium"
|
||
LOW = "low"
|
||
|
||
|
||
class MetaCognitionTag(str, Enum):
|
||
"""
|
||
元认知信号标记
|
||
|
||
附加在记忆条目上,让调用方 LLM 感知记忆的可靠程度和特殊性质。
|
||
"""
|
||
# 置信度相关
|
||
HIGH_CONFIDENCE = "high_confidence"
|
||
LOW_CONFIDENCE = "low_confidence"
|
||
INFERRED_NOT_STATED = "inferred_not_stated"
|
||
STALE = "stale"
|
||
|
||
# 冲突与不确定
|
||
NEEDS_CLARIFICATION = "needs_clarification"
|
||
CONFLICTED = "conflicted"
|
||
CONTEXT_DEPENDENT = "context_dependent"
|
||
|
||
# 情绪与敏感
|
||
EMOTIONALLY_SENSITIVE = "emotionally_sensitive"
|
||
POSITIVE_MEMORY = "positive_memory"
|
||
NEGATIVE_MEMORY = "negative_memory"
|
||
GOTCHA = "gotcha"
|
||
|
||
# 时间特殊性
|
||
RECENTLY_CHANGED = "recently_changed"
|
||
HISTORICAL = "historical"
|
||
TEMPORAL_CONTEXT = "temporal_context"
|
||
|
||
# 知识缺口
|
||
KNOWLEDGE_GAP = "knowledge_gap"
|
||
INCOMPLETE = "incomplete"
|
||
|
||
# 价值标记
|
||
LONG_TAIL = "long_tail"
|
||
SCHEMA_ACTIVATED = "schema_activated"
|
||
|
||
# 前瞻标记
|
||
PROACTIVE_CARE = "proactive_care"
|
||
FOLLOW_UP = "follow_up"
|
||
|
||
|
||
class GapType(str, Enum):
|
||
"""知识缺口的四种类型"""
|
||
PROFILE_INCOMPLETE = "profile_incomplete"
|
||
UNRESOLVED_CONFLICT = "unresolved_conflict"
|
||
INFERRED_UNVERIFIED = "inferred_unverified"
|
||
STALE_UNVERIFIED = "stale_unverified"
|
||
|
||
|
||
# ============================================================
|
||
# 核心模型: MemoryNode (统一基类)
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class MemoryNode:
|
||
"""
|
||
所有层级记忆的统一基类 (V2)
|
||
|
||
字段分组:
|
||
- 基础标识 (node_id, user_id, layer, content)
|
||
- 业务隔离 (agent_id, session_id)
|
||
- 时空维度 (memory_at, temporal_anchor, valid_from/until, gmt_created/modified)
|
||
- 演化图谱 (supersedes, superseded_by, is_latest) — EVOLVE 语义
|
||
- 推断注解 (speculate) — 不参与 embed
|
||
- Summary 锚点 (source_raw_memory_id) — L3_SUMMARY/L4_IDENTITY 专用
|
||
- 状态 (status)
|
||
- 置信与校准 (confidence, evidence_count, source_type)
|
||
- 情绪标记 (emotional_valence, emotional_arousal)
|
||
- 检索元数据 (access_count, last_accessed_at, embedding)
|
||
- 长尾标记 (specificity_score, rarity_score, longtail_flag)
|
||
- 元认知标记 (meta_tags, meta_hints)
|
||
- 出处追溯 (source_session_id, source_turn_index, evidence_chain)
|
||
- 扩展 (custom, tags)
|
||
"""
|
||
|
||
# === 基础标识 ===
|
||
node_id: str = ""
|
||
user_id: str = ""
|
||
layer: MemoryLayer = MemoryLayer.L1_RAW
|
||
content: str = "" # 核心语义内容,embed 只用此字段
|
||
|
||
# === 业务隔离 ===
|
||
agent_id: str = ""
|
||
session_id: str = ""
|
||
# 产生该记忆的调用来源(openclaw / cursor / codex 等)。不要与
|
||
# source_type(显式/推断/复合)或检索通道混用。
|
||
source: str = "default"
|
||
|
||
# === 归属(owner)===
|
||
# 该记忆属于谁:'user'(关于用户/用户陈述)| 'agent'(assistant 提供/用户要 agent 做的事)。
|
||
# 仅 L2_FACT / L7_INTENTION 写入;其他层(L0/L1/L3/L5/L6)留空(None)。
|
||
owner: Optional[str] = None
|
||
|
||
# === 时空维度 ===
|
||
memory_at: Optional[datetime] = None # 记忆发生时间(用户指定,可为空)
|
||
temporal_anchor: Optional[str] = None # 保留字段兼容历史数据;v0.3.45_v0 post8 起不再写入
|
||
valid_from: Optional[datetime] = None
|
||
valid_until: Optional[datetime] = None
|
||
|
||
# === 数据管理时间 ===
|
||
gmt_created: Optional[datetime] = None # 数据写入时间(系统自动)
|
||
gmt_modified: Optional[datetime] = None # 数据最后修改时间(系统自动)
|
||
|
||
# === 演化图谱(EVOLVE 语义)===
|
||
supersedes: Optional[List[str]] = None # 本节点取代的旧节点 ID 列表,全新信息为 None
|
||
superseded_by: Optional[List[str]] = None # 被哪些新节点取代(系统写入,不透出给调用方)
|
||
is_latest: bool = True # 是否链条末端;被取代时系统自动置 False
|
||
|
||
# === 推断注解(不参与 embed)===
|
||
speculate: Optional[str] = None # LLM 对复杂/模糊信号的推断注解
|
||
|
||
# === Summary 锚点(L3_SUMMARY / L4_IDENTITY)===
|
||
source_raw_memory_id: Optional[str] = None # 对应的 L1_RAW 节点 ID
|
||
|
||
# === 状态 ===
|
||
status: MemoryStatus = MemoryStatus.ACTIVE
|
||
|
||
# === 置信与校准 ===
|
||
confidence: float = 1.0
|
||
evidence_count: int = 1
|
||
source_type: SourceType = SourceType.EXPLICIT
|
||
|
||
# === 情绪标记 ===
|
||
emotional_valence: float = 0.0 # [-1, 1] 情感效价
|
||
emotional_arousal: float = 0.0 # [0, 1] 情感唤醒度
|
||
|
||
# === 检索元数据 ===
|
||
access_count: int = 0
|
||
last_accessed_at: Optional[datetime] = None
|
||
embedding: Optional[List[float]] = None
|
||
|
||
# === 长尾标记 ===
|
||
specificity_score: float = 0.0 # [0, 1] 具体度
|
||
rarity_score: float = 0.0 # [0, 1] 稀有度
|
||
longtail_flag: bool = False
|
||
|
||
# === 元认知标记 ===
|
||
meta_tags: List[MetaCognitionTag] = field(default_factory=list)
|
||
meta_hints: Dict[str, str] = field(default_factory=dict)
|
||
|
||
# === Episode(内部会话)===
|
||
# 一件事往往跨越好几次 write(十几二十轮),从中抽出的 memory 属于同一次讨论。
|
||
# 打同一个 episode_id,读侧就能用它**框出一片候选**做加权 —— 比建 O(n²) 时间边
|
||
# 便宜得多,也比"同一天"语义准确。
|
||
#
|
||
# 注意与 session_id 的区别:session_id 是**调用方给的业务隔离键**(进 isolation_key),
|
||
# 可能很长(整段会话)或干脆是 default;episode_id 是 SDK **内部按时间间隔切**的
|
||
# "一次讨论",粒度更细,且不参与隔离。
|
||
episode_id: str = ""
|
||
|
||
# === 出处追溯 ===
|
||
source_session_id: str = ""
|
||
source_turn_index: Optional[int] = None
|
||
evidence_chain: List[str] = field(default_factory=list)
|
||
|
||
# === 扩展字段 ===
|
||
custom: Dict[str, Any] = field(default_factory=dict)
|
||
tags: List[str] = field(default_factory=list)
|
||
# 与 tags 等长的 NER label(GPE/PERSON/...);主题词为空串。写入时标注,读侧 boost 用。
|
||
tag_ners: List[str] = field(default_factory=list)
|
||
|
||
def __post_init__(self):
|
||
from ..utils.source_filter import normalize_source
|
||
self.source = normalize_source(self.source)
|
||
if not self.node_id:
|
||
self.node_id = str(uuid.uuid4())
|
||
now = datetime.now()
|
||
# gmt_created/gmt_modified 是系统时间,自动填写
|
||
if self.gmt_created is None:
|
||
self.gmt_created = now
|
||
if self.gmt_modified is None:
|
||
self.gmt_modified = now
|
||
# memory_at 是记忆发生时间,由调用方指定,不自动填写
|
||
# valid_from 用 gmt_created 作为默认(数据有效从写入时开始)
|
||
if self.valid_from is None:
|
||
self.valid_from = now
|
||
|
||
@property
|
||
def memory_id(self) -> str:
|
||
"""V1 兼容: memory_id == node_id"""
|
||
return self.node_id
|
||
|
||
@property
|
||
def uid(self) -> str:
|
||
"""V1 兼容: uid == user_id"""
|
||
return self.user_id
|
||
|
||
def get_isolation_key(self) -> str:
|
||
"""隔离键: {user_id}::{agent_id}::{session_id}
|
||
|
||
session_id 为空时使用 'default_session',与 build_isolation_key 对齐。
|
||
"""
|
||
sid = self.session_id or "default_session"
|
||
return f"{self.user_id}::{self.agent_id}::{sid}"
|
||
|
||
def get_topic_scope_key(self) -> str:
|
||
"""Topic 的隔离键: {user_id}::{agent_id} —— **不含 session_id**。
|
||
|
||
为什么 Topic 要比记忆本身更宽:主题是**跨会话累积**的概念。"关于你的健康我知道
|
||
这些"不该按会话切开。而 `get_isolation_key()` 带 session,用它当 Topic 的
|
||
隔离键会让同名 tag 在每个 session 里各建一个互不相识的 Topic 节点。
|
||
|
||
如果按 session 隔离,同名 Topic 会分裂成多个节点,导致时序脊建不出边、
|
||
topic summary 反复总结同名碎片、跨会话 tag 召回不连通。
|
||
"""
|
||
return f"{self.user_id}::{self.agent_id}"
|
||
|
||
@staticmethod
|
||
def build_topic_scope_key(uid: str, agent_id: str) -> str:
|
||
"""构建 Topic 隔离键: {uid}::{agent_id}(不含 session,见 get_topic_scope_key)"""
|
||
return f"{uid}::{agent_id}"
|
||
|
||
@staticmethod
|
||
def topic_scope_of(isolation_key: str) -> str:
|
||
"""把带 session 的 isolation_key 削成 Topic 作用域(取前两段)。
|
||
|
||
供只拿到 isolation_key 的读侧使用 —— 建 Topic 与查 Topic 必须用同一个键,
|
||
否则查不到(两个后端都是 `isolation_key` 精确匹配)。
|
||
"""
|
||
if not isolation_key:
|
||
return isolation_key
|
||
parts = isolation_key.split("::")
|
||
return "::".join(parts[:2]) if len(parts) >= 2 else isolation_key
|
||
|
||
@staticmethod
|
||
def build_isolation_key(uid: str, agent_id: str, session_id: str = "default_session") -> str:
|
||
"""构建隔离键: {uid}::{agent_id}::{session_id}"""
|
||
sid = session_id or "default_session"
|
||
return f"{uid}::{agent_id}::{sid}"
|
||
|
||
@staticmethod
|
||
def parse_isolation_key(key: str) -> tuple:
|
||
"""
|
||
解析隔离键,返回 (user_id, agent_id, session_id)。
|
||
|
||
兼容多种格式:
|
||
- 4 段: (旧格式带 appid) 跳过首段 → uid::agent_id::session_id
|
||
- 3 段: uid::agent_id::session_id (标准格式)
|
||
- 2 段: uid::agent_id (session_id 默认 "default_session")
|
||
"""
|
||
parts = key.split("::")
|
||
if len(parts) >= 4:
|
||
# 兼容旧格式: 跳过 appid
|
||
return parts[1], parts[2], parts[3]
|
||
elif len(parts) == 3:
|
||
return parts[0], parts[1], parts[2]
|
||
elif len(parts) == 2:
|
||
return parts[0], parts[1], "default_session"
|
||
return "", "", ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""序列化为字典"""
|
||
return {
|
||
"node_id": self.node_id,
|
||
"user_id": self.user_id,
|
||
"agent_id": self.agent_id,
|
||
"session_id": self.session_id,
|
||
"source": self.source,
|
||
"episode_id": self.episode_id,
|
||
"owner": self.owner,
|
||
"layer": self.layer.value,
|
||
"content": self.content,
|
||
"memory_at": int(self.memory_at.timestamp()) if self.memory_at else None,
|
||
"temporal_anchor": self.temporal_anchor,
|
||
"gmt_created": int(self.gmt_created.timestamp()) if self.gmt_created else None,
|
||
"gmt_modified": int(self.gmt_modified.timestamp()) if self.gmt_modified else None,
|
||
"valid_from": int(self.valid_from.timestamp()) if self.valid_from else None,
|
||
"valid_until": int(self.valid_until.timestamp()) if self.valid_until else None,
|
||
# 演化图谱
|
||
"supersedes": self.supersedes,
|
||
"superseded_by": self.superseded_by,
|
||
"is_latest": self.is_latest,
|
||
# 推断注解
|
||
"speculate": self.speculate,
|
||
# Summary 锚点
|
||
"source_raw_memory_id": self.source_raw_memory_id,
|
||
# 状态
|
||
"status": self.status.value,
|
||
# 置信与校准
|
||
"confidence": self.confidence,
|
||
"evidence_count": self.evidence_count,
|
||
"source_type": self.source_type.value,
|
||
# 情绪
|
||
"emotional_valence": self.emotional_valence,
|
||
"emotional_arousal": self.emotional_arousal,
|
||
# 检索元数据
|
||
"access_count": self.access_count,
|
||
"last_accessed_at": int(self.last_accessed_at.timestamp()) if self.last_accessed_at else None,
|
||
# 长尾标记
|
||
"specificity_score": self.specificity_score,
|
||
"rarity_score": self.rarity_score,
|
||
"longtail_flag": self.longtail_flag,
|
||
# 元认知
|
||
"meta_tags": [t.value for t in self.meta_tags],
|
||
"meta_hints": self.meta_hints,
|
||
# 出处追溯
|
||
"source_session_id": self.source_session_id,
|
||
"source_turn_index": self.source_turn_index,
|
||
"evidence_chain": self.evidence_chain,
|
||
# 扩展
|
||
"custom": self.custom,
|
||
"tags": self.tags,
|
||
"tag_ners": list(getattr(self, "tag_ners", None) or []),
|
||
# 向量(可选,clone / 序列化场景需要携带)
|
||
"embedding": self.embedding,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "MemoryNode":
|
||
"""从字典反序列化(兼容旧 payload 中的废弃字段,忽略即可)"""
|
||
def parse_dt(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v
|
||
if isinstance(v, (int, float)):
|
||
return datetime.fromtimestamp(v)
|
||
if isinstance(v, str):
|
||
return datetime.fromisoformat(v)
|
||
return None
|
||
|
||
def parse_enum(enum_cls, v, default):
|
||
if v is None:
|
||
return default
|
||
if isinstance(v, enum_cls):
|
||
return v
|
||
try:
|
||
return enum_cls(v)
|
||
except (ValueError, KeyError):
|
||
return default
|
||
|
||
meta_tags_raw = data.get("meta_tags", [])
|
||
meta_tags = []
|
||
for t in meta_tags_raw:
|
||
if isinstance(t, MetaCognitionTag):
|
||
meta_tags.append(t)
|
||
else:
|
||
try:
|
||
meta_tags.append(MetaCognitionTag(t))
|
||
except (ValueError, KeyError):
|
||
pass
|
||
|
||
return cls(
|
||
node_id=data.get("node_id", data.get("memory_id", "")),
|
||
user_id=data.get("user_id", data.get("uid", "")),
|
||
agent_id=data.get("agent_id", ""),
|
||
session_id=data.get("session_id", ""),
|
||
source=data.get("source", "default") or "default",
|
||
episode_id=data.get("episode_id", "") or "",
|
||
owner=data.get("owner") or None,
|
||
layer=parse_enum(MemoryLayer, data.get("layer"), MemoryLayer.L1_RAW),
|
||
content=data.get("content", ""),
|
||
# memory_at: 优先读 memory_at,兼容旧 key created_at
|
||
memory_at=parse_dt(data.get("memory_at") or data.get("created_at")),
|
||
temporal_anchor=data.get("temporal_anchor"),
|
||
gmt_created=parse_dt(data.get("gmt_created")),
|
||
gmt_modified=parse_dt(data.get("gmt_modified")),
|
||
valid_from=parse_dt(data.get("valid_from")),
|
||
valid_until=parse_dt(data.get("valid_until")),
|
||
# 演化图谱
|
||
supersedes=data.get("supersedes"),
|
||
superseded_by=data.get("superseded_by"),
|
||
is_latest=data.get("is_latest", True),
|
||
# 推断注解
|
||
speculate=data.get("speculate"),
|
||
# Summary 锚点
|
||
source_raw_memory_id=data.get("source_raw_memory_id"),
|
||
# 状态
|
||
status=parse_enum(MemoryStatus, data.get("status"), MemoryStatus.ACTIVE),
|
||
# 置信
|
||
confidence=data.get("confidence", 1.0),
|
||
evidence_count=data.get("evidence_count", 1),
|
||
source_type=parse_enum(SourceType, data.get("source_type"), SourceType.EXPLICIT),
|
||
# 情绪
|
||
emotional_valence=data.get("emotional_valence", 0.0),
|
||
emotional_arousal=data.get("emotional_arousal", 0.0),
|
||
# 检索元数据
|
||
access_count=data.get("access_count", 0),
|
||
last_accessed_at=parse_dt(data.get("last_accessed_at")),
|
||
embedding=data.get("embedding"),
|
||
# 长尾
|
||
specificity_score=data.get("specificity_score", 0.0),
|
||
rarity_score=data.get("rarity_score", 0.0),
|
||
longtail_flag=data.get("longtail_flag", False),
|
||
# 元认知
|
||
meta_tags=meta_tags,
|
||
meta_hints=data.get("meta_hints", {}),
|
||
# 出处
|
||
source_session_id=data.get("source_session_id", ""),
|
||
source_turn_index=data.get("source_turn_index"),
|
||
evidence_chain=data.get("evidence_chain", []),
|
||
# 扩展
|
||
custom=data.get("custom", {}),
|
||
tags=data.get("tags", []) or [],
|
||
tag_ners=list(data.get("tag_ners") or []),
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# 子类模型
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class VersionedFact(MemoryNode):
|
||
"""
|
||
L2 层的原子事实 — 版本化不可变记录
|
||
|
||
V2 §3.2: 不直接修改事实内容,而是通过 status 变迁 + SUPERSEDED_BY 边来追踪变更。
|
||
"""
|
||
update_type: Optional[UpdateType] = None
|
||
|
||
def __post_init__(self):
|
||
super().__post_init__()
|
||
if self.layer == MemoryLayer.L1_RAW:
|
||
self.layer = MemoryLayer.L2_FACT
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
d = super().to_dict()
|
||
d["update_type"] = self.update_type.value if self.update_type else None
|
||
return d
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "VersionedFact":
|
||
base = MemoryNode.from_dict(data)
|
||
update_type = None
|
||
if data.get("update_type"):
|
||
try:
|
||
update_type = UpdateType(data["update_type"])
|
||
except (ValueError, KeyError):
|
||
pass
|
||
return cls(**{**base.__dict__, "update_type": update_type})
|
||
|
||
|
||
@dataclass
|
||
class SchemaNode(MemoryNode):
|
||
"""
|
||
L6 层: 心智模型 — 从多个关联事实中自动归纳的抽象叙事模板 (Graph 层)
|
||
|
||
V3 §2.2
|
||
"""
|
||
central_proposition: str = ""
|
||
supporting_evidence: List[str] = field(default_factory=list)
|
||
expected_inferences: List[str] = field(default_factory=list)
|
||
activation_threshold: float = 0.7
|
||
activation_count: int = 0
|
||
last_activated_at: Optional[datetime] = None
|
||
schema_status: SchemaStatus = SchemaStatus.FORMING
|
||
|
||
def __post_init__(self):
|
||
super().__post_init__()
|
||
self.layer = MemoryLayer.L6_SCHEMA
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
d = super().to_dict()
|
||
d.update({
|
||
"central_proposition": self.central_proposition,
|
||
"supporting_evidence": self.supporting_evidence,
|
||
"expected_inferences": self.expected_inferences,
|
||
"activation_threshold": self.activation_threshold,
|
||
"activation_count": self.activation_count,
|
||
"last_activated_at": int(self.last_activated_at.timestamp()) if self.last_activated_at else None,
|
||
"schema_status": self.schema_status.value,
|
||
})
|
||
return d
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "SchemaNode":
|
||
base = MemoryNode.from_dict(data)
|
||
def parse_dt(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v
|
||
if isinstance(v, (int, float)):
|
||
return datetime.fromtimestamp(v)
|
||
if isinstance(v, str):
|
||
return datetime.fromisoformat(v)
|
||
return None
|
||
schema_status = SchemaStatus.FORMING
|
||
if data.get("schema_status"):
|
||
try:
|
||
schema_status = SchemaStatus(data["schema_status"])
|
||
except (ValueError, KeyError):
|
||
pass
|
||
return cls(
|
||
**{**base.__dict__,
|
||
"central_proposition": data.get("central_proposition", ""),
|
||
"supporting_evidence": data.get("supporting_evidence", []),
|
||
"expected_inferences": data.get("expected_inferences", []),
|
||
"activation_threshold": data.get("activation_threshold", 0.7),
|
||
"activation_count": data.get("activation_count", 0),
|
||
"last_activated_at": parse_dt(data.get("last_activated_at")),
|
||
"schema_status": schema_status,
|
||
})
|
||
|
||
|
||
@dataclass
|
||
class IntentionNode(MemoryNode):
|
||
"""
|
||
L7 层: 前瞻性记忆 — 记住"将来要做什么" (Graph 层)
|
||
|
||
V3 §2.2
|
||
"""
|
||
trigger_type: TriggerType = TriggerType.EVENT_BASED
|
||
trigger_condition: str = ""
|
||
|
||
# 时间触发
|
||
trigger_time: Optional[datetime] = None
|
||
|
||
# 事件触发
|
||
trigger_event_pattern: Optional[str] = None
|
||
trigger_event_embedding: Optional[List[float]] = None
|
||
|
||
# 意图内容
|
||
intention_content: str = ""
|
||
priority: IntentionPriority = IntentionPriority.MEDIUM
|
||
expiry: Optional[datetime] = None
|
||
triggered: bool = False
|
||
triggered_at: Optional[datetime] = None
|
||
|
||
def __post_init__(self):
|
||
super().__post_init__()
|
||
self.layer = MemoryLayer.L7_INTENTION
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
d = super().to_dict()
|
||
d.update({
|
||
"trigger_type": self.trigger_type.value,
|
||
"trigger_condition": self.trigger_condition,
|
||
"trigger_time": int(self.trigger_time.timestamp()) if self.trigger_time else None,
|
||
"trigger_event_pattern": self.trigger_event_pattern,
|
||
"intention_content": self.intention_content,
|
||
"priority": self.priority.value,
|
||
"expiry": int(self.expiry.timestamp()) if self.expiry else None,
|
||
"triggered": self.triggered,
|
||
"triggered_at": int(self.triggered_at.timestamp()) if self.triggered_at else None,
|
||
})
|
||
return d
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "IntentionNode":
|
||
base = MemoryNode.from_dict(data)
|
||
def parse_dt(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v
|
||
if isinstance(v, (int, float)):
|
||
return datetime.fromtimestamp(v)
|
||
if isinstance(v, str):
|
||
return datetime.fromisoformat(v)
|
||
return None
|
||
trigger_type = TriggerType.EVENT_BASED
|
||
if data.get("trigger_type"):
|
||
try:
|
||
trigger_type = TriggerType(data["trigger_type"])
|
||
except (ValueError, KeyError):
|
||
pass
|
||
priority = IntentionPriority.MEDIUM
|
||
if data.get("priority"):
|
||
try:
|
||
priority = IntentionPriority(data["priority"])
|
||
except (ValueError, KeyError):
|
||
pass
|
||
return cls(
|
||
**{**base.__dict__,
|
||
"trigger_type": trigger_type,
|
||
"trigger_condition": data.get("trigger_condition", ""),
|
||
"trigger_time": parse_dt(data.get("trigger_time")),
|
||
"trigger_event_pattern": data.get("trigger_event_pattern"),
|
||
"trigger_event_embedding": data.get("trigger_event_embedding"),
|
||
"intention_content": data.get("intention_content", ""),
|
||
"priority": priority,
|
||
"expiry": parse_dt(data.get("expiry")),
|
||
"triggered": data.get("triggered", False),
|
||
"triggered_at": parse_dt(data.get("triggered_at")),
|
||
})
|
||
|
||
|
||
# ============================================================
|
||
# 辅助模型: KnowledgeGap
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class KnowledgeGap:
|
||
"""
|
||
知识缺口记录 — 描述"某个维度的信息缺失或不可靠"
|
||
|
||
V2 §5.6.2
|
||
"""
|
||
gap_id: str = ""
|
||
user_id: str = ""
|
||
domain: str = ""
|
||
gap_type: GapType = GapType.PROFILE_INCOMPLETE
|
||
importance: str = "medium" # high / medium / low
|
||
description: str = ""
|
||
related_node_ids: List[str] = field(default_factory=list)
|
||
hint: str = ""
|
||
created_at: Optional[datetime] = None
|
||
last_scanned_at: Optional[datetime] = None
|
||
resolved: bool = False
|
||
resolved_at: Optional[datetime] = None
|
||
resolve_trigger: Optional[str] = None
|
||
|
||
def __post_init__(self):
|
||
if not self.gap_id:
|
||
self.gap_id = f"gap_{uuid.uuid4().hex[:12]}"
|
||
now = datetime.now()
|
||
if self.created_at is None:
|
||
self.created_at = now
|
||
if self.last_scanned_at is None:
|
||
self.last_scanned_at = now
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"gap_id": self.gap_id,
|
||
"user_id": self.user_id,
|
||
"domain": self.domain,
|
||
"gap_type": self.gap_type.value,
|
||
"importance": self.importance,
|
||
"description": self.description,
|
||
"related_node_ids": self.related_node_ids,
|
||
"hint": self.hint,
|
||
"created_at": int(self.created_at.timestamp()) if self.created_at else None,
|
||
"last_scanned_at": int(self.last_scanned_at.timestamp()) if self.last_scanned_at else None,
|
||
"resolved": self.resolved,
|
||
"resolved_at": int(self.resolved_at.timestamp()) if self.resolved_at else None,
|
||
"resolve_trigger": self.resolve_trigger,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "KnowledgeGap":
|
||
def parse_dt(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v
|
||
if isinstance(v, (int, float)):
|
||
return datetime.fromtimestamp(v)
|
||
if isinstance(v, str):
|
||
return datetime.fromisoformat(v)
|
||
return None
|
||
gap_type = GapType.PROFILE_INCOMPLETE
|
||
if data.get("gap_type"):
|
||
try:
|
||
gap_type = GapType(data["gap_type"])
|
||
except (ValueError, KeyError):
|
||
pass
|
||
return cls(
|
||
gap_id=data.get("gap_id", ""),
|
||
user_id=data.get("user_id", ""),
|
||
domain=data.get("domain", ""),
|
||
gap_type=gap_type,
|
||
importance=data.get("importance", "medium"),
|
||
description=data.get("description", ""),
|
||
related_node_ids=data.get("related_node_ids", []),
|
||
hint=data.get("hint", ""),
|
||
created_at=parse_dt(data.get("created_at")),
|
||
last_scanned_at=parse_dt(data.get("last_scanned_at")),
|
||
resolved=data.get("resolved", False),
|
||
resolved_at=parse_dt(data.get("resolved_at")),
|
||
resolve_trigger=data.get("resolve_trigger"),
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# 辅助模型: 时间系统 (Temporal)
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class LifeStage:
|
||
"""用户的一个生命阶段"""
|
||
name: str = "" # "大学时期", "在上海工作"
|
||
start_time: Optional[datetime] = None
|
||
end_time: Optional[datetime] = None
|
||
description: str = ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"name": self.name,
|
||
"start_time": int(self.start_time.timestamp()) if self.start_time else None,
|
||
"end_time": int(self.end_time.timestamp()) if self.end_time else None,
|
||
"description": self.description,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "LifeStage":
|
||
def parse_dt(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v
|
||
if isinstance(v, (int, float)):
|
||
return datetime.fromtimestamp(v)
|
||
if isinstance(v, str):
|
||
return datetime.fromisoformat(v)
|
||
return None
|
||
return cls(
|
||
name=data.get("name", ""),
|
||
start_time=parse_dt(data.get("start_time")),
|
||
end_time=parse_dt(data.get("end_time")),
|
||
description=data.get("description", ""),
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class TemporalEvent:
|
||
"""
|
||
时间线上的一个事件
|
||
|
||
V2 §6.1
|
||
"""
|
||
event_id: str = "" # 关联的 fact_id
|
||
event_description: str = ""
|
||
event_time: Optional[datetime] = None
|
||
time_precision: str = "approximate" # exact / month / quarter / year / approximate
|
||
time_description: str = "" # 原始时间描述
|
||
life_stage: Optional[str] = None
|
||
is_milestone: bool = False
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"event_id": self.event_id,
|
||
"event_description": self.event_description,
|
||
"event_time": int(self.event_time.timestamp()) if self.event_time else None,
|
||
"time_precision": self.time_precision,
|
||
"time_description": self.time_description,
|
||
"life_stage": self.life_stage,
|
||
"is_milestone": self.is_milestone,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "TemporalEvent":
|
||
def parse_dt(v):
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, datetime):
|
||
return v
|
||
if isinstance(v, (int, float)):
|
||
return datetime.fromtimestamp(v)
|
||
if isinstance(v, str):
|
||
return datetime.fromisoformat(v)
|
||
return None
|
||
return cls(
|
||
event_id=data.get("event_id", ""),
|
||
event_description=data.get("event_description", ""),
|
||
event_time=parse_dt(data.get("event_time")),
|
||
time_precision=data.get("time_precision", "approximate"),
|
||
time_description=data.get("time_description", ""),
|
||
life_stage=data.get("life_stage"),
|
||
is_milestone=data.get("is_milestone", False),
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class UserTimeline:
|
||
"""
|
||
用户的完整时间线
|
||
|
||
V2 §6.1
|
||
"""
|
||
user_id: str = ""
|
||
events: List[TemporalEvent] = field(default_factory=list)
|
||
life_stages: List[LifeStage] = field(default_factory=list)
|
||
milestone_events: Dict[str, TemporalEvent] = field(default_factory=dict)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"user_id": self.user_id,
|
||
"events": [e.to_dict() for e in self.events],
|
||
"life_stages": [s.to_dict() for s in self.life_stages],
|
||
"milestone_events": {k: v.to_dict() for k, v in self.milestone_events.items()},
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "UserTimeline":
|
||
return cls(
|
||
user_id=data.get("user_id", ""),
|
||
events=[TemporalEvent.from_dict(e) for e in data.get("events", [])],
|
||
life_stages=[LifeStage.from_dict(s) for s in data.get("life_stages", [])],
|
||
milestone_events={
|
||
k: TemporalEvent.from_dict(v)
|
||
for k, v in data.get("milestone_events", {}).items()
|
||
},
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# 输出协议: MemoryContextPackage
|
||
# ============================================================
|
||
|
||
@dataclass
|
||
class ProfileSummary:
|
||
"""用户画像摘要 (Layer 0, 始终返回)"""
|
||
core: str = "" # "张三, 30岁, 北京, 后端工程师"
|
||
personality: str = "" # "务实, 技术导向"
|
||
active_schemas: List[str] = field(default_factory=list)
|
||
gotchas: List[str] = field(default_factory=list)
|
||
note: str = ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"core": self.core,
|
||
"personality": self.personality,
|
||
"active_schemas": self.active_schemas,
|
||
"gotchas": self.gotchas,
|
||
"note": self.note,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class MemoryIndexEntry:
|
||
"""索引视图条目 (Layer 1, ~100 tokens)"""
|
||
node_id: str = ""
|
||
date: str = ""
|
||
content_type: str = ""
|
||
title: str = ""
|
||
relevance: float = 0.0
|
||
meta_tags: List[str] = field(default_factory=list)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"id": self.node_id,
|
||
"date": self.date,
|
||
"type": self.content_type,
|
||
"title": self.title,
|
||
"relevance": self.relevance,
|
||
"meta_tags": self.meta_tags,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class MemorySummaryEntry:
|
||
"""摘要视图条目 (Layer 2, ~500 tokens)"""
|
||
node_id: str = ""
|
||
summary: str = ""
|
||
confidence: float = 0.0
|
||
meta_tags: List[str] = field(default_factory=list)
|
||
meta_hints: Dict[str, str] = field(default_factory=dict)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"id": self.node_id,
|
||
"summary": self.summary,
|
||
"confidence": self.confidence,
|
||
"meta_tags": self.meta_tags,
|
||
"meta_hints": self.meta_hints,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class MetaCognitionReport:
|
||
"""元认知报告"""
|
||
confidence_summary: str = ""
|
||
knowledge_gaps: List[Dict[str, Any]] = field(default_factory=list)
|
||
unresolved_conflicts: List[Dict[str, Any]] = field(default_factory=list)
|
||
stale_memories: List[Dict[str, Any]] = field(default_factory=list)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"confidence_summary": self.confidence_summary,
|
||
"knowledge_gaps": self.knowledge_gaps,
|
||
"unresolved_conflicts": self.unresolved_conflicts,
|
||
"stale_memories": self.stale_memories,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class TriggeredIntention:
|
||
"""被触发的前瞻性意图"""
|
||
trigger_reason: str = ""
|
||
intention: str = ""
|
||
priority: str = "medium"
|
||
meta_tags: List[str] = field(default_factory=list)
|
||
source_intention_id: str = ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"trigger_reason": self.trigger_reason,
|
||
"intention": self.intention,
|
||
"priority": self.priority,
|
||
"meta_tags": self.meta_tags,
|
||
"source_intention_id": self.source_intention_id,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class ActivatedSchema:
|
||
"""被激活的 Schema"""
|
||
schema: str = ""
|
||
evidence_summary: str = ""
|
||
expected_inferences: List[str] = field(default_factory=list)
|
||
meta_tags: List[str] = field(default_factory=list)
|
||
source_schema_id: str = ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"schema": self.schema,
|
||
"evidence_summary": self.evidence_summary,
|
||
"expected_inferences": self.expected_inferences,
|
||
"meta_tags": self.meta_tags,
|
||
"source_schema_id": self.source_schema_id,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class TemporalContext:
|
||
"""时间线上下文 (query 涉及时间时返回)"""
|
||
relevant_timeline: List[Dict[str, str]] = field(default_factory=list)
|
||
temporal_note: str = ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"relevant_timeline": self.relevant_timeline,
|
||
"temporal_note": self.temporal_note,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class MemoryContextPackage:
|
||
"""
|
||
返回给调用方 LLM 的完整记忆上下文包
|
||
|
||
V2 §7.1: 三层渐进式披露 + 元认知信号 + 前瞻意图 + Schema + 时间线
|
||
"""
|
||
# Layer 0: 用户画像摘要 (始终返回)
|
||
profile_summary: ProfileSummary = field(default_factory=ProfileSummary)
|
||
|
||
# Layer 1: 索引视图 (~100 tokens)
|
||
index_view: List[MemoryIndexEntry] = field(default_factory=list)
|
||
|
||
# Layer 2: 摘要视图 (~500 tokens, 按需展开)
|
||
summary_view: List[MemorySummaryEntry] = field(default_factory=list)
|
||
|
||
# Layer 3: 完整内容 (按需懒加载)
|
||
full_content_ids: List[str] = field(default_factory=list)
|
||
|
||
# 元认知信号
|
||
meta_cognition: MetaCognitionReport = field(default_factory=MetaCognitionReport)
|
||
|
||
# 前瞻性意图
|
||
triggered_intentions: List[TriggeredIntention] = field(default_factory=list)
|
||
|
||
# 激活的 Schema
|
||
activated_schemas: List[ActivatedSchema] = field(default_factory=list)
|
||
|
||
# 时间线上下文
|
||
temporal_context: Optional[TemporalContext] = None
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"profile_summary": self.profile_summary.to_dict(),
|
||
"index_view": [e.to_dict() for e in self.index_view],
|
||
"summary_view": [e.to_dict() for e in self.summary_view],
|
||
"full_content_ids": self.full_content_ids,
|
||
"meta_cognition": self.meta_cognition.to_dict(),
|
||
"triggered_intentions": [t.to_dict() for t in self.triggered_intentions],
|
||
"activated_schemas": [s.to_dict() for s in self.activated_schemas],
|
||
"temporal_context": self.temporal_context.to_dict() if self.temporal_context else None,
|
||
}
|
||
|
||
|
||
# ============================================================
|
||
# V1 兼容类型别名
|
||
# ============================================================
|
||
|
||
# MemoryEntry 作为 MemoryNode 的别名,保持旧代码不报错
|
||
MemoryEntry = MemoryNode
|
||
|
||
|
||
@dataclass
|
||
class MemoryMetadata:
|
||
"""
|
||
V1 兼容: 记忆元数据
|
||
|
||
新代码应直接使用 MemoryNode 的字段。此类保留用于与旧代码兼容。
|
||
"""
|
||
uid: str = ""
|
||
agent_id: str = ""
|
||
layer: MemoryLayer = MemoryLayer.L1_RAW
|
||
|
||
event_time: Optional[datetime] = None
|
||
created_at: Optional[datetime] = None
|
||
updated_at: Optional[datetime] = None
|
||
expires_at: Optional[datetime] = None
|
||
|
||
importance: float = 1.0
|
||
recency_weight: float = 1.0
|
||
access_count: int = 0
|
||
last_accessed: Optional[datetime] = None
|
||
|
||
source: Optional[str] = None
|
||
parent_id: Optional[str] = None
|
||
related_ids: List[str] = field(default_factory=list)
|
||
|
||
custom: Dict[str, Any] = field(default_factory=dict)
|
||
tags: List[str] = field(default_factory=list)
|
||
|
||
def __post_init__(self):
|
||
now = datetime.now()
|
||
if self.created_at is None:
|
||
self.created_at = now
|
||
if self.updated_at is None:
|
||
self.updated_at = now
|
||
if self.event_time is None:
|
||
self.event_time = now
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"uid": self.uid,
|
||
"agent_id": self.agent_id,
|
||
"layer": self.layer.value,
|
||
"event_time": int(self.event_time.timestamp()) if self.event_time else None,
|
||
"created_at": int(self.created_at.timestamp()) if self.created_at else None,
|
||
"updated_at": int(self.updated_at.timestamp()) if self.updated_at else None,
|
||
"expires_at": int(self.expires_at.timestamp()) if self.expires_at else None,
|
||
"importance": self.importance,
|
||
"recency_weight": self.recency_weight,
|
||
"access_count": self.access_count,
|
||
"last_accessed": int(self.last_accessed.timestamp()) if self.last_accessed else None,
|
||
"source": self.source,
|
||
"parent_id": self.parent_id,
|
||
"related_ids": self.related_ids,
|
||
"custom": self.custom,
|
||
"tags": self.tags,
|
||
"tag_ners": list(self.tag_ners or []),
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "MemoryMetadata":
|
||
def parse_datetime(value):
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, datetime):
|
||
return value
|
||
if isinstance(value, (int, float)):
|
||
return datetime.fromtimestamp(value)
|
||
if isinstance(value, str):
|
||
return datetime.fromisoformat(value)
|
||
return None
|
||
|
||
layer = data.get("layer", MemoryLayer.L1_RAW)
|
||
if isinstance(layer, str):
|
||
layer = MemoryLayer.from_string(layer)
|
||
|
||
return cls(
|
||
uid=data.get("uid", ""),
|
||
agent_id=data.get("agent_id", ""),
|
||
layer=layer,
|
||
event_time=parse_datetime(data.get("event_time")),
|
||
created_at=parse_datetime(data.get("created_at")),
|
||
updated_at=parse_datetime(data.get("updated_at")),
|
||
expires_at=parse_datetime(data.get("expires_at")),
|
||
importance=data.get("importance", 1.0),
|
||
recency_weight=data.get("recency_weight", 1.0),
|
||
access_count=data.get("access_count", 0),
|
||
last_accessed=parse_datetime(data.get("last_accessed")),
|
||
source=data.get("source"),
|
||
parent_id=data.get("parent_id"),
|
||
related_ids=data.get("related_ids", []),
|
||
custom=data.get("custom", {}),
|
||
tags=data.get("tags", []),
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class MemoryScore:
|
||
"""
|
||
V1 兼容: 记忆评分
|
||
|
||
V2 使用更复杂的评分公式 (参见 core/scorer.py),此类保留兼容。
|
||
"""
|
||
semantic_score: float = 0.0
|
||
recency_score: float = 0.0
|
||
importance_score: float = 0.0
|
||
access_score: float = 0.0
|
||
|
||
semantic_weight: float = 0.5
|
||
recency_weight: float = 0.3
|
||
importance_weight: float = 0.15
|
||
access_weight: float = 0.05
|
||
|
||
@property
|
||
def final_score(self) -> float:
|
||
return (
|
||
self.semantic_score * self.semantic_weight
|
||
+ self.recency_score * self.recency_weight
|
||
+ self.importance_score * self.importance_weight
|
||
+ self.access_score * self.access_weight
|
||
)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"semantic_score": self.semantic_score,
|
||
"recency_score": self.recency_score,
|
||
"importance_score": self.importance_score,
|
||
"access_score": self.access_score,
|
||
"final_score": self.final_score,
|
||
"weights": {
|
||
"semantic": self.semantic_weight,
|
||
"recency": self.recency_weight,
|
||
"importance": self.importance_weight,
|
||
"access": self.access_weight,
|
||
},
|
||
}
|