M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)
This commit is contained in:
1 parent
2a640a901b
commit
9c98c2f85e
69 files changed
+1193
-6033
No files matched your search
@@ -1 +0,0 @@
|
||||
"""大周天记忆/世界抽取领域纯逻辑(造消息 / 解析记忆候选与世界实体草稿,禁止 IO)。"""
|
||||
@@ -1,140 +0,0 @@
|
||||
"""解析模型返回为记忆/世界抽取草稿(纯函数,无 IO)。
|
||||
|
||||
模型输出对象 ``{"memories": [...], "entities": [...]}``。缺省取**安全悲观默认**:
|
||||
记忆 ``sensitivity=low``、世界 ``state_type=inference``(非 fact,召回降权,见
|
||||
[世界模型设计] ../../docs/domain/core-domains.md §8)。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryCandidateDraft:
|
||||
type: str
|
||||
title: str
|
||||
content: str
|
||||
confidence: float = 0.5
|
||||
importance: float = 0.5
|
||||
sensitivity: str = "low"
|
||||
reason: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldEntityDraft:
|
||||
type: str
|
||||
name: str
|
||||
attributes: dict[str, Any] = field(default_factory=dict)
|
||||
state_type: str = "inference"
|
||||
confidence: float = 0.5
|
||||
|
||||
|
||||
_MEMORY_TYPES = {
|
||||
"user",
|
||||
"preference",
|
||||
"event",
|
||||
"relationship",
|
||||
"skill",
|
||||
"goal",
|
||||
"other",
|
||||
}
|
||||
_ENTITY_TYPES = {
|
||||
"self",
|
||||
"person",
|
||||
"object",
|
||||
"place",
|
||||
"device",
|
||||
"project",
|
||||
"task",
|
||||
"concept",
|
||||
}
|
||||
_STATE_TYPES = {"fact", "observation", "inference", "hypothesis", "stale"}
|
||||
_SENSITIVITY = {"low", "medium", "high"}
|
||||
|
||||
|
||||
def parse_extraction(
|
||||
raw: str,
|
||||
) -> tuple[list[MemoryCandidateDraft], list[WorldEntityDraft]]:
|
||||
data = extract_json_object(raw)
|
||||
if not isinstance(data, dict):
|
||||
return [], []
|
||||
return parse_memory_drafts(data.get("memories")), _parse_entities(data.get("entities"))
|
||||
|
||||
|
||||
def parse_memory_drafts(value: Any) -> list[MemoryCandidateDraft]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
result: list[MemoryCandidateDraft] = []
|
||||
for item in value:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
title = str(item.get("title", "")).strip()
|
||||
content = str(item.get("content", "")).strip()
|
||||
if not title or not content:
|
||||
continue
|
||||
result.append(
|
||||
MemoryCandidateDraft(
|
||||
type=_enum(item.get("type"), _MEMORY_TYPES, "other"),
|
||||
title=title,
|
||||
content=content,
|
||||
confidence=_clamp(item.get("confidence"), 0.5),
|
||||
importance=_clamp(item.get("importance"), 0.5),
|
||||
sensitivity=_enum(item.get("sensitivity"), _SENSITIVITY, "low"),
|
||||
reason=str(item.get("reason", "")).strip(),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _parse_entities(value: Any) -> list[WorldEntityDraft]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
result: list[WorldEntityDraft] = []
|
||||
for item in value:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = str(item.get("name", "")).strip()
|
||||
if not name:
|
||||
continue
|
||||
attrs = item.get("attributes")
|
||||
result.append(
|
||||
WorldEntityDraft(
|
||||
type=_enum(item.get("type"), _ENTITY_TYPES, "concept"),
|
||||
name=name,
|
||||
attributes=attrs if isinstance(attrs, dict) else {},
|
||||
state_type=_enum(item.get("state_type"), _STATE_TYPES, "inference"),
|
||||
confidence=_clamp(item.get("confidence"), 0.5),
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def extract_json_object(raw: str) -> dict[str, Any]:
|
||||
text = raw.strip()
|
||||
if text.startswith("```"):
|
||||
text = re.sub(r"^```(?:json)?\s*", "", text)
|
||||
text = re.sub(r"\s*```$", "", text)
|
||||
start = text.find("{")
|
||||
end = text.rfind("}")
|
||||
if start != -1 and end > start:
|
||||
text = text[start : end + 1]
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _enum(value: Any, valid: set[str], default: str) -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
return text if text in valid else default
|
||||
|
||||
|
||||
def _clamp(value: Any, default: float) -> float:
|
||||
try:
|
||||
num = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(0.0, min(1.0, num))
|
||||
@@ -1,46 +0,0 @@
|
||||
"""大周天记忆/世界抽取 prompt 构建(纯函数,无 IO)。
|
||||
|
||||
复用 ``EvaluationSnapshot``(态势快照),产出 ``(system, user)`` 消息对,喂给
|
||||
模型网关。模型返回的 JSON 对象由 ``extract`` 模块解析。
|
||||
|
||||
枚举取值以 [记忆系统设计](../../docs/domain/memory-model.md) §6、
|
||||
[世界模型设计](../../docs/domain/core-domains.md) §8 与
|
||||
[数据库 schema](../../docs/contracts/contract-glossary.md) §20 为准。
|
||||
"""
|
||||
|
||||
from app.modules.big_cycle.prompts import EvaluationSnapshot
|
||||
|
||||
|
||||
def build_extraction_messages(
|
||||
snapshot: EvaluationSnapshot,
|
||||
) -> tuple[str, str]:
|
||||
system = (
|
||||
"你是魂镜的大周天记忆/世界抽取器。基于本轮自我评估的态势快照与近期记忆,"
|
||||
"抽取值得长期记住的记忆候选与世界实体候选(用户、项目、地点、对象等)。"
|
||||
"只抽取有长期价值、非琐碎的信息;低置信度或推断信息标注为 inference 而非 fact,"
|
||||
"不得把推断伪装成已确认事实。"
|
||||
'以 JSON 对象返回:{"memories": [...], "entities": [...]}。'
|
||||
'memories 每项:{"type": str(user/preference/event/relationship/skill/goal/fact/other), '
|
||||
'"title": str, "content": str, "importance": 0~1, '
|
||||
'"sensitivity": "low|medium|high", "confidence": 0~1, "reason": str}。'
|
||||
'entities 每项:{"type": str(self/person/object/place/device/project/task/concept), '
|
||||
'"name": str, "attributes": 对象, '
|
||||
'"state_type": str(fact/observation/inference/hypothesis/stale), "confidence": 0~1}。'
|
||||
'无值得抽取的内容时返回 {"memories": [], "entities": []}。'
|
||||
"只返回 JSON 对象,不要解释或 markdown。"
|
||||
)
|
||||
user = (
|
||||
f"记忆总数:{snapshot.memory_count}\n"
|
||||
f"世界实体数:{snapshot.world_entity_count}\n"
|
||||
f"自我评估结论:{snapshot.conclusion or '(无明显态势)'}\n"
|
||||
f"当前人格摘要:\n{snapshot.personality_summary or '(尚未形成)'}\n"
|
||||
f"近期记忆摘要:\n{_format_memories(snapshot.recent_memories)}"
|
||||
)
|
||||
return system, user
|
||||
|
||||
|
||||
def _format_memories(memories: list[tuple[str, str]]) -> str:
|
||||
if not memories:
|
||||
return "(暂无)"
|
||||
lines = [f"[{idx}] {title}:{content}" for idx, (title, content) in enumerate(memories)]
|
||||
return "\n".join(lines)
|
||||
Whitespace-only changes.
@@ -1,91 +0,0 @@
|
||||
"""记忆上下文组装与检索结果数据结构(纯领域,无 IO)。
|
||||
|
||||
``MemoryRetrievalResult`` 持有已加载的 Memory ORM 实例(由 services/memory 检索后传入),
|
||||
``build_memory_context`` 把它组装成注入上下文的文本。本模块只接收已加载数据、输出结构化对象。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.core.time import utc_now
|
||||
from app.models.memory import Memory
|
||||
|
||||
|
||||
class MemoryRetrievalResult(BaseModel):
|
||||
"""记忆检索结果(持有已加载的 Memory ORM 实例列表)。"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
memories: list[Memory]
|
||||
|
||||
|
||||
def _to_aware(dt: datetime | None) -> datetime | None:
|
||||
"""确保 datetime 有时区信息,naive 视为 UTC。"""
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=UTC)
|
||||
return dt
|
||||
|
||||
|
||||
def build_memory_context(result: MemoryRetrievalResult, *, max_length: int = 3000) -> str | None:
|
||||
"""把检索到的记忆组装成注入上下文的文本;无记忆时返回 None。
|
||||
|
||||
按**重要性降序**排列,每条附带重要性标签和相对时间。
|
||||
超过 ``max_length`` 字符时截断,避免上下文窗口膨胀。
|
||||
"""
|
||||
if not result.memories:
|
||||
return None
|
||||
|
||||
now = utc_now()
|
||||
|
||||
def _mem_sort_key(m: Memory) -> float:
|
||||
"""排序键:重要性(越高越优先),同时辅助时间衰减。"""
|
||||
ca = _to_aware(m.created_at)
|
||||
hours_ago = (now - ca).total_seconds() / 3600 if ca else 0
|
||||
decay = max(0.5, 1.0 - hours_ago / 720) # 30 天内缓慢衰减
|
||||
return (m.importance or 0.5) * decay
|
||||
|
||||
sorted_mems = sorted(result.memories, key=_mem_sort_key, reverse=True)
|
||||
|
||||
lines: list[str] = []
|
||||
total_chars = 0
|
||||
for idx, item in enumerate(sorted_mems):
|
||||
importance_label = _importance_label(item.importance)
|
||||
ca = _to_aware(item.created_at)
|
||||
hours_ago = (now - ca).total_seconds() / 3600 if ca else 0
|
||||
time_label = _time_ago_str(hours_ago)
|
||||
line = f"- 「{item.title}」{time_label} [{importance_label}]\n {item.content}"
|
||||
line_len = len(line) + 1 # +1 for newline
|
||||
if total_chars + line_len > max_length:
|
||||
remaining = len(sorted_mems) - idx
|
||||
if remaining > 0:
|
||||
lines.append(f"…以及 {remaining} 条其他记忆(已截断)")
|
||||
break
|
||||
lines.append(line)
|
||||
total_chars += line_len
|
||||
|
||||
return "相关记忆:\n" + "\n".join(lines)
|
||||
|
||||
|
||||
def _importance_label(importance: float) -> str:
|
||||
if importance >= 0.8:
|
||||
return "★重要"
|
||||
if importance >= 0.5:
|
||||
return "●一般"
|
||||
return "○参考"
|
||||
|
||||
|
||||
def _time_ago_str(hours_ago: float) -> str:
|
||||
if hours_ago < 1:
|
||||
return "刚刚"
|
||||
if hours_ago < 24:
|
||||
return f"{int(hours_ago)}小时前"
|
||||
days = int(hours_ago / 24)
|
||||
if days < 30:
|
||||
return f"{days}天前"
|
||||
months = int(days / 30)
|
||||
return f"{months}个月前"
|
||||
@@ -1 +0,0 @@
|
||||
"""小周天每轮反思领域纯逻辑(造消息 / 解析记忆候选与反思,禁止 IO)。"""
|
||||
@@ -1,22 +0,0 @@
|
||||
"""解析模型返回为小周天记忆候选 + 反思(纯函数,无 IO)。
|
||||
|
||||
模型输出对象 ``{"memories": [...], "reflection": str}``。记忆候选解析复用 extraction
|
||||
的 ``parse_memory_drafts``;``reflection`` 为本轮复盘文本。
|
||||
"""
|
||||
|
||||
from app.modules.extraction.extract import (
|
||||
MemoryCandidateDraft,
|
||||
extract_json_object,
|
||||
parse_memory_drafts,
|
||||
)
|
||||
|
||||
|
||||
def parse_reflection(
|
||||
raw: str,
|
||||
) -> tuple[list[MemoryCandidateDraft], str]:
|
||||
data = extract_json_object(raw)
|
||||
if not isinstance(data, dict):
|
||||
return [], ""
|
||||
memories = parse_memory_drafts(data.get("memories"))
|
||||
reflection = str(data.get("reflection", "")).strip()
|
||||
return memories, reflection
|
||||
@@ -1,22 +0,0 @@
|
||||
"""小周天每轮记忆候选 + 反思 prompt 构建(纯函数,无 IO)。"""
|
||||
|
||||
|
||||
def build_reflection_messages(
|
||||
user_content: str,
|
||||
assistant_content: str,
|
||||
) -> tuple[str, str]:
|
||||
system = (
|
||||
"你是魂镜的小周天反思器。基于本轮对话(用户消息 + 你的回复),抽取值得长期记住的"
|
||||
"记忆候选(用户偏好 / 事件 / 事实等),并生成一条简短的本轮反思"
|
||||
"(做得好 / 不好 / 可改进)。"
|
||||
"只抽取有长期价值、非琐碎的信息;反思是系统内部复盘,不超过两句。"
|
||||
'以 JSON 对象返回:{"memories": [{"type": str'
|
||||
"(user/preference/event/relationship/skill/goal/fact/other), "
|
||||
'"title": str, "content": str, "importance": 0~1, '
|
||||
'"sensitivity": "low|medium|high", "confidence": 0~1, "reason": str}], '
|
||||
'"reflection": str}。'
|
||||
"无值得记忆的内容时 memories 为空数组;reflection 仍需给出。"
|
||||
"只返回 JSON 对象,不要解释或 markdown。"
|
||||
)
|
||||
user = f"用户消息:\n{user_content}\n\n你的回复:\n{assistant_content}"
|
||||
return system, user
|
||||
Reference in new issue
Block a user