memory.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """
  2. Memory 系统(Phase 2+)
  3. 详见 agent/docs/memory-plan.md。核心概念:
  4. - Memory:Agent 身份私有的主观记忆,Markdown 文件,人类可读写
  5. - Dream:记忆反思操作(回顾多个 trace 的执行历史,更新记忆文件)
  6. 本模块只提供 MemoryConfig 数据类和记忆文件加载逻辑。
  7. Dream 操作在 agent/core/dream.py(Phase 3)。
  8. """
  9. from __future__ import annotations
  10. import glob as _glob
  11. import logging
  12. from dataclasses import dataclass, field
  13. from pathlib import Path
  14. from typing import Dict, List, Optional, Tuple
  15. logger = logging.getLogger(__name__)
  16. @dataclass
  17. class MemoryConfig:
  18. """持久化记忆配置(见 agent/docs/memory-plan.md 第五节)"""
  19. base_path: str = ""
  20. # 记忆文件根目录。所有文件路径相对此目录解析。
  21. files: Optional[Dict[str, str]] = None
  22. # {路径模式: 用途说明}
  23. # key 支持两种形式:
  24. # - 直接路径:"core/identity.md"
  25. # - glob 模式:"relationships/*.md"、"journals/2026/**.md"
  26. # value 是人类可读的用途说明(注入时作为文件分隔标题的一部分)。
  27. # 框架只负责按 key 解析文件内容;组织结构由配置者决定。
  28. dream_prompt: str = ""
  29. # Dream 跨 trace 整合 prompt;空则使用默认(Phase 3 定义)
  30. reflect_prompt: str = ""
  31. # Per-trace 记忆反思 prompt;空则使用默认(Phase 3 定义)
  32. def load_memory_files(config: MemoryConfig) -> List[Tuple[str, str, str]]:
  33. """按 MemoryConfig.files 的 key 解析磁盘上的记忆文件。
  34. Returns:
  35. List[(relative_path, purpose, content)],按 files 声明顺序扁平化,
  36. 文件不存在则跳过(记 debug 日志),内容为空也保留(方便人类看到占位)。
  37. """
  38. if not config.base_path or not config.files:
  39. return []
  40. base = Path(config.base_path)
  41. if not base.exists():
  42. logger.debug(f"[Memory] base_path 不存在: {base}")
  43. return []
  44. results: List[Tuple[str, str, str]] = []
  45. seen: set[str] = set() # 去重(多个 glob 可能命中同一个文件)
  46. for key, purpose in config.files.items():
  47. # 展开 glob;直接路径也走 glob(无通配符时返回单条或空)
  48. pattern = str(base / key)
  49. matched_paths = sorted(_glob.glob(pattern, recursive=True))
  50. if not matched_paths:
  51. # 直接路径没命中时给个 debug(可能还没写第一版)
  52. logger.debug(f"[Memory] {key} 没有匹配文件(尚未创建)")
  53. continue
  54. for fs_path in matched_paths:
  55. rel = str(Path(fs_path).relative_to(base))
  56. if rel in seen:
  57. continue
  58. seen.add(rel)
  59. try:
  60. content = Path(fs_path).read_text(encoding="utf-8")
  61. except Exception as e:
  62. logger.warning(f"[Memory] 读取失败 {fs_path}: {e}")
  63. continue
  64. results.append((rel, purpose, content))
  65. return results
  66. def format_memory_injection(files: List[Tuple[str, str, str]]) -> str:
  67. """把加载结果格式化为可注入到上下文的 markdown 段。"""
  68. if not files:
  69. return ""
  70. parts = ["## 你的长期记忆\n\n以下是你作为此 Agent 身份积累的记忆(人类可直接编辑):\n"]
  71. for rel, purpose, content in files:
  72. header = f"### `{rel}`"
  73. if purpose:
  74. header += f" — {purpose}"
  75. parts.append(header)
  76. parts.append(content.rstrip() or "_(空文件,尚未积累内容)_")
  77. parts.append("") # 空行分隔
  78. return "\n".join(parts).rstrip() + "\n"