mem1 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mem1/config.py CHANGED
@@ -41,6 +41,9 @@ class MemoryConfig(BaseModel):
41
41
  save_assistant_messages: bool # 是否保存 assistant 回复
42
42
  max_assistant_chars: int # assistant 回复超过此长度触发摘要
43
43
  context_days_limit: int # get_context 检索最近几天的对话
44
+ # 时间衰减配置(借鉴 supermemory Smart Forgetting)
45
+ decay_recent_days: int # [近期] 阈值,默认 7 天
46
+ decay_earlier_days: int # [较早] 阈值,默认 30 天(超过为 [久远])
44
47
 
45
48
 
46
49
  class ESConfig(BaseModel):
@@ -49,6 +52,18 @@ class ESConfig(BaseModel):
49
52
  index_name: str
50
53
 
51
54
 
55
+ class SQLiteConfig(BaseModel):
56
+ """SQLite 配置"""
57
+ db_path: str
58
+
59
+
60
+ class StorageConfig(BaseModel):
61
+ """存储配置"""
62
+ backend: str # sqlite / elasticsearch
63
+ sqlite: Optional[SQLiteConfig] = None
64
+ es: Optional[ESConfig] = None
65
+
66
+
52
67
  class ImagesConfig(BaseModel):
53
68
  """图片存储配置"""
54
69
  images_dir: str
@@ -59,59 +74,88 @@ class Mem1Config(BaseModel):
59
74
  llm: LLMConfig
60
75
  vl: VLConfig
61
76
  memory: MemoryConfig
62
- es: ESConfig
77
+ storage: StorageConfig
63
78
  images: ImagesConfig
64
79
 
80
+ # 兼容旧接口
81
+ @property
82
+ def es(self) -> Optional[ESConfig]:
83
+ return self.storage.es
84
+
65
85
  @classmethod
66
86
  def from_env(cls) -> "Mem1Config":
67
87
  """从环境变量加载配置
68
88
 
89
+ 存储后端配置:
90
+ - MEM1_STORAGE_BACKEND: sqlite(默认)/ elasticsearch
91
+ - MEM1_SQLITE_PATH: SQLite 数据库路径(默认 {memory_dir}/mem1.db)
92
+ - MEM1_ES_HOSTS: ES 地址(仅 elasticsearch 后端需要)
93
+ - MEM1_ES_INDEX: ES 索引名(仅 elasticsearch 后端需要)
94
+
69
95
  必需的环境变量:
70
96
  - MEM1_LLM_API_KEY: LLM API 密钥
71
97
  - MEM1_LLM_BASE_URL: LLM API 地址(OpenAI 兼容)
72
98
  - MEM1_LLM_MODEL: LLM 模型名
73
- - MEM1_ES_HOSTS: ES 地址
74
- - MEM1_ES_INDEX: ES 索引名
75
99
  - MEM1_MEMORY_DIR: 记忆存储目录
76
- - MEM1_AUTO_UPDATE_PROFILE: 是否自动更新画像 (true/false)
77
- - MEM1_MAX_PROFILE_CHARS: 画像最大字符数
78
- - MEM1_UPDATE_INTERVAL_ROUNDS: 画像更新间隔轮数
79
- - MEM1_UPDATE_INTERVAL_MINUTES: 画像更新间隔分钟数
80
100
 
81
- 可选的环境变量(VL 模型,使用 dashscope SDK):
82
- - MEM1_VL_MODEL: VL 模型名(如 qwen-vl-max),配置即启用
83
- - MEM1_VL_API_KEY: dashscope API 密钥
101
+ 可选的环境变量(VL 模型):
102
+ - MEM1_VL_PROVIDER: qwen / doubao
103
+ - MEM1_VL_MODEL: VL 模型名
104
+ - MEM1_VL_API_KEY: API 密钥
84
105
  """
85
- # 必需配置检查
106
+ # 存储后端选择
107
+ storage_backend = os.getenv("MEM1_STORAGE_BACKEND", "sqlite").lower()
108
+
109
+ # 基础必需配置
86
110
  required_vars = {
87
111
  "MEM1_LLM_API_KEY": os.getenv("MEM1_LLM_API_KEY"),
88
112
  "MEM1_LLM_BASE_URL": os.getenv("MEM1_LLM_BASE_URL"),
89
113
  "MEM1_LLM_MODEL": os.getenv("MEM1_LLM_MODEL"),
90
- "MEM1_ES_HOSTS": os.getenv("MEM1_ES_HOSTS"),
91
- "MEM1_ES_INDEX": os.getenv("MEM1_ES_INDEX"),
92
114
  "MEM1_MEMORY_DIR": os.getenv("MEM1_MEMORY_DIR"),
93
- "MEM1_AUTO_UPDATE_PROFILE": os.getenv("MEM1_AUTO_UPDATE_PROFILE"),
94
- "MEM1_MAX_PROFILE_CHARS": os.getenv("MEM1_MAX_PROFILE_CHARS"),
95
- "MEM1_UPDATE_INTERVAL_ROUNDS": os.getenv("MEM1_UPDATE_INTERVAL_ROUNDS"),
96
- "MEM1_UPDATE_INTERVAL_MINUTES": os.getenv("MEM1_UPDATE_INTERVAL_MINUTES"),
97
- "MEM1_SAVE_ASSISTANT_MESSAGES": os.getenv("MEM1_SAVE_ASSISTANT_MESSAGES"),
98
- "MEM1_MAX_ASSISTANT_CHARS": os.getenv("MEM1_MAX_ASSISTANT_CHARS"),
99
- "MEM1_LLM_TEMPERATURE": os.getenv("MEM1_LLM_TEMPERATURE"),
100
- "MEM1_CONTEXT_DAYS_LIMIT": os.getenv("MEM1_CONTEXT_DAYS_LIMIT"),
115
+ "MEM1_AUTO_UPDATE_PROFILE": os.getenv("MEM1_AUTO_UPDATE_PROFILE", "true"),
116
+ "MEM1_MAX_PROFILE_CHARS": os.getenv("MEM1_MAX_PROFILE_CHARS", "3000"),
117
+ "MEM1_UPDATE_INTERVAL_ROUNDS": os.getenv("MEM1_UPDATE_INTERVAL_ROUNDS", "5"),
118
+ "MEM1_UPDATE_INTERVAL_MINUTES": os.getenv("MEM1_UPDATE_INTERVAL_MINUTES", "3"),
119
+ "MEM1_SAVE_ASSISTANT_MESSAGES": os.getenv("MEM1_SAVE_ASSISTANT_MESSAGES", "true"),
120
+ "MEM1_MAX_ASSISTANT_CHARS": os.getenv("MEM1_MAX_ASSISTANT_CHARS", "500"),
121
+ "MEM1_LLM_TEMPERATURE": os.getenv("MEM1_LLM_TEMPERATURE", "0.2"),
122
+ "MEM1_CONTEXT_DAYS_LIMIT": os.getenv("MEM1_CONTEXT_DAYS_LIMIT", "31"),
123
+ "MEM1_DECAY_RECENT_DAYS": os.getenv("MEM1_DECAY_RECENT_DAYS", "7"),
124
+ "MEM1_DECAY_EARLIER_DAYS": os.getenv("MEM1_DECAY_EARLIER_DAYS", "31"),
101
125
  }
102
126
 
103
- missing = [k for k, v in required_vars.items() if not v]
127
+ # 检查基础必需配置
128
+ base_required = ["MEM1_LLM_API_KEY", "MEM1_LLM_BASE_URL", "MEM1_LLM_MODEL", "MEM1_MEMORY_DIR"]
129
+ missing = [k for k in base_required if not required_vars.get(k)]
104
130
  if missing:
105
131
  raise ValueError(f"缺少必需的环境变量: {', '.join(missing)}")
106
132
 
107
- # ES hosts 支持逗号分隔的多个地址
108
- es_hosts = [h.strip() for h in required_vars["MEM1_ES_HOSTS"].split(",")]
109
-
110
- # 图片目录基于记忆目录
111
133
  memory_dir = required_vars["MEM1_MEMORY_DIR"]
112
134
  images_dir = f"{memory_dir}/images"
113
135
 
114
- # VL 模型配置(可选,配置了 provider 即启用)
136
+ # 构建存储配置
137
+ storage_config: StorageConfig
138
+ if storage_backend == "elasticsearch":
139
+ es_hosts = os.getenv("MEM1_ES_HOSTS")
140
+ es_index = os.getenv("MEM1_ES_INDEX")
141
+ if not es_hosts or not es_index:
142
+ raise ValueError("elasticsearch 后端需要配置 MEM1_ES_HOSTS 和 MEM1_ES_INDEX")
143
+ storage_config = StorageConfig(
144
+ backend="elasticsearch",
145
+ es=ESConfig(
146
+ hosts=[h.strip() for h in es_hosts.split(",")],
147
+ index_name=es_index
148
+ )
149
+ )
150
+ else:
151
+ # 默认 SQLite
152
+ sqlite_path = os.getenv("MEM1_SQLITE_PATH", f"{memory_dir}/mem1.db")
153
+ storage_config = StorageConfig(
154
+ backend="sqlite",
155
+ sqlite=SQLiteConfig(db_path=sqlite_path)
156
+ )
157
+
158
+ # VL 模型配置(可选)
115
159
  vl_config = VLConfig(
116
160
  provider=os.getenv("MEM1_VL_PROVIDER", ""),
117
161
  model=os.getenv("MEM1_VL_MODEL", ""),
@@ -136,12 +180,11 @@ class Mem1Config(BaseModel):
136
180
  update_interval_minutes=int(required_vars["MEM1_UPDATE_INTERVAL_MINUTES"]),
137
181
  save_assistant_messages=required_vars["MEM1_SAVE_ASSISTANT_MESSAGES"].lower() == "true",
138
182
  max_assistant_chars=int(required_vars["MEM1_MAX_ASSISTANT_CHARS"]),
139
- context_days_limit=int(required_vars["MEM1_CONTEXT_DAYS_LIMIT"])
140
- ),
141
- es=ESConfig(
142
- hosts=es_hosts,
143
- index_name=required_vars["MEM1_ES_INDEX"]
183
+ context_days_limit=int(required_vars["MEM1_CONTEXT_DAYS_LIMIT"]),
184
+ decay_recent_days=int(required_vars["MEM1_DECAY_RECENT_DAYS"]),
185
+ decay_earlier_days=int(required_vars["MEM1_DECAY_EARLIER_DAYS"])
144
186
  ),
187
+ storage=storage_config,
145
188
  images=ImagesConfig(
146
189
  images_dir=images_dir
147
190
  )
mem1/memory.py CHANGED
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  from mem1.config import Mem1Config
11
11
  from mem1.llm import LLMClient, VLClient
12
12
  from mem1.prompts import ProfileTemplate, RECALL_DECISION_PROMPT, IMAGE_SEARCH_PROMPT, ASSISTANT_SUMMARY_PROMPT, CONTEXT_SUFFICIENT_PROMPT
13
- from mem1.storage import StorageBackend, ESStorage
13
+ from mem1.storage import StorageBackend, ESStorage, SQLiteStorage
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -55,8 +55,11 @@ class Mem1Memory:
55
55
  # 存储后端(可插拔)
56
56
  if storage:
57
57
  self.storage = storage
58
+ elif config.storage.backend == "elasticsearch":
59
+ self.storage = ESStorage(config.storage.es.hosts, config.storage.es.index_name)
58
60
  else:
59
- self.storage = ESStorage(config.es.hosts, config.es.index_name)
61
+ # 默认 SQLite
62
+ self.storage = SQLiteStorage(config.storage.sqlite.db_path)
60
63
 
61
64
  # LLM 客户端
62
65
  self.llm = LLMClient(config.llm)
@@ -74,6 +77,9 @@ class Mem1Memory:
74
77
  self.update_interval_minutes = config.memory.update_interval_minutes
75
78
  self.save_assistant_messages = config.memory.save_assistant_messages
76
79
  self.max_assistant_chars = config.memory.max_assistant_chars
80
+ # 时间衰减配置
81
+ self.decay_recent_days = config.memory.decay_recent_days
82
+ self.decay_earlier_days = config.memory.decay_earlier_days
77
83
 
78
84
  # ========== 图片处理 ==========
79
85
 
@@ -546,14 +552,37 @@ class Mem1Memory:
546
552
  return summary
547
553
 
548
554
  def _format_conversations_for_llm(self, conversations: List[Dict[str, Any]]) -> str:
549
- """格式化对话记录为文本(带引用 ID)"""
555
+ """格式化对话记录为文本(带引用 ID 和时间衰减标记)
556
+
557
+ 时间衰减设计(借鉴 supermemory Smart Forgetting):
558
+ - [近期] decay_recent_days 天内:权重高,优先保留
559
+ - [较早] decay_recent_days ~ decay_earlier_days 天:权重中等
560
+ - [久远] 超过 decay_earlier_days 天:权重低,Dynamic 信息可淘汰
561
+ """
562
+ from datetime import datetime
563
+ now = datetime.now()
564
+
550
565
  output = []
551
566
  for idx, conv in enumerate(conversations, start=1):
552
567
  conv_id = f"对话-{idx:03d}" # 001, 002, 003...
553
- timestamp = conv.get("timestamp", "未知时间")
568
+ timestamp_str = conv.get("timestamp", "未知时间")
554
569
  metadata = conv.get("metadata", {})
555
570
 
556
- title = f"### [{conv_id}] {timestamp}"
571
+ # 计算时间衰减标记
572
+ decay_tag = ""
573
+ try:
574
+ conv_time = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
575
+ days_ago = (now - conv_time).days
576
+ if days_ago <= self.decay_recent_days:
577
+ decay_tag = "[近期]"
578
+ elif days_ago <= self.decay_earlier_days:
579
+ decay_tag = "[较早]"
580
+ else:
581
+ decay_tag = "[久远]"
582
+ except (ValueError, TypeError):
583
+ decay_tag = ""
584
+
585
+ title = f"### [{conv_id}] {decay_tag} {timestamp_str}"
557
586
  if metadata:
558
587
  tags = " ".join([f"[{k}:{v}]" for k, v in metadata.items()])
559
588
  title += f" {tags}"
mem1/prompts.py CHANGED
@@ -20,41 +20,45 @@ class ProfileTemplate:
20
20
  - 业务场景通过 ProfileTemplate 定义画像结构和提示词
21
21
  """
22
22
 
23
- # 默认通用模板
24
- DEFAULT_SECTIONS = """## 基本信息
23
+ # 默认通用模板(借鉴 supermemory 的 static/dynamic 设计)
24
+ DEFAULT_SECTIONS = """## 🔒 稳定信息(Static)
25
+ > 核心事实,很少变化,压缩时必须保留
26
+
25
27
  - 姓名/称呼:
26
28
  - 身份/角色:
27
29
  - 所属组织:
30
+ - 长期偏好:(沟通风格、关注重点等)
28
31
  - 背景信息:
29
32
 
30
- ## 偏好习惯
31
- - 沟通风格:(详细/简洁、正式/随意等)
32
- - 关注重点:
33
- - 特殊要求:
33
+ ## 🔄 动态信息(Dynamic)
34
+ > 近期状态,经常更新,可精简合并
35
+
36
+ ### 当前关注
37
+ - 进行中项目:
38
+ - 近期兴趣:
39
+ - 当前任务:
34
40
 
35
- ## 周期性任务
36
- (固定日程和重复性工作,格式:[周期] 任务内容)
37
- - 例:[每周一] 提交周报、[每周五] 制定下周计划、[每月底] 月度汇总
41
+ ### 周期性任务
42
+ (固定日程,格式:[周期] 任务内容)
38
43
 
39
- ## 关键数据
40
- (重要的数字、金额、数量,用加粗标记)
41
- - 例:处置 **97起** 案件、涉及金额 **230万元**、完成 **365个** 检查点
44
+ ### 关键数据
45
+ (重要数字,用 **加粗** 标记)
42
46
 
43
- ## 任务时间线
44
- (用户提到的计划、截止日期、里程碑,格式:[YYYY-MM-DD] 事项)
47
+ ### 任务时间线
48
+ (计划/截止日期,格式:[YYYY-MM-DD] 事项)
45
49
 
46
- ## 待办事项
47
- (用户提出但未完成的请求,格式:- [ ] 事项(MM-DD 提出))
50
+ ### 待办事项
51
+ (未完成请求,格式:- [ ] 事项(MM-DD 提出))
48
52
 
49
- ## 待澄清事项
50
- (用户前后矛盾或模糊的信息,需要下次确认,格式:⚠️ 矛盾描述)
53
+ ## ⚠️ 待澄清事项
54
+ (前后矛盾的信息,格式:⚠️ 矛盾描述)
51
55
 
52
- ## 用户反馈
53
- - 正向反馈:(满意的地方)
54
- - 负向反馈:(不满意的地方、需改进的问题)
56
+ ## 📝 用户反馈
57
+ - 正向反馈:
58
+ - 负向反馈:
55
59
 
56
- ## 注意事项
57
- (需要规避的话题、曾经的误解、用户不喜欢的做法等)"""
60
+ ## 🚫 注意事项
61
+ (规避话题、历史问题等)"""
58
62
 
59
63
  def __init__(
60
64
  self,
@@ -100,7 +104,7 @@ class ProfileTemplate:
100
104
 
101
105
  DEFAULT_PROFILE_UPDATE_PROMPT = """你是用户画像分析专家。从对话记录中提取用户信息,更新用户画像。
102
106
 
103
- ## 对话记录
107
+ ## 对话记录(带时间衰减标记)
104
108
  {normal_content}
105
109
 
106
110
  ## 现有用户画像
@@ -112,33 +116,53 @@ DEFAULT_PROFILE_UPDATE_PROMPT = """你是用户画像分析专家。从对话记
112
116
  ## 输出格式
113
117
  直接输出完整的 Markdown 格式用户画像,保持现有画像的章节结构。
114
118
 
119
+ ## Static vs Dynamic 分类原则(借鉴 supermemory)
120
+ 画像分为两类信息,更新时需区分处理:
121
+
122
+ ### 🔒 稳定信息(Static)—— 必须保留
123
+ - 核心身份:姓名、职业、所属组织
124
+ - 长期偏好:沟通风格、关注重点
125
+ - 背景信息:专业领域、工作经历
126
+ - 特点:很少变化,除非用户明确否定
127
+
128
+ ### 🔄 动态信息(Dynamic)—— 可更新/淘汰
129
+ - 当前状态:进行中项目、近期兴趣
130
+ - 时效信息:任务时间线、待办事项
131
+ - 临时数据:具体事件、阶段性目标
132
+ - 特点:随时间变化,旧信息可被新信息替换
133
+
134
+ ## 记忆关系处理(借鉴 supermemory Graph Memory)
135
+ - **Updates(替换)**:新信息与旧信息矛盾时,用新的替换旧的(仅限 Dynamic 部分)
136
+ - **Extends(补充)**:新信息丰富旧信息时,合并保留
137
+ - **矛盾检测**:Static 信息出现矛盾时,记录到「待澄清事项」,不直接覆盖
138
+
139
+ ## 时间衰减处理
140
+ - 对话记录中标注了 [近期]、[较早]、[久远] 等时间标记
141
+ - [近期] 信息权重高,优先保留
142
+ - [久远] 的 Dynamic 信息可以精简或删除
143
+ - Static 信息不受时间衰减影响
144
+
115
145
  ## 整理原则
116
- 1. 不能丢失现有画像中的信息,除非对话中明确否定
117
- 2. 新信息是补充,不是替换
146
+ 1. Static 信息:不能丢失,除非用户明确否定
147
+ 2. Dynamic 信息:新信息可替换旧信息
118
148
  3. 用户反馈(表扬或批评)必须保留
119
149
  4. 只记录用户明确表达的信息,不要推测
120
- 5. 如果某个章节没有信息,保留标题但内容留空
121
-
122
- ## 周期性任务提取
123
- 6. 识别固定日程:用户提到"每周一"、"每周五"、"每天"、"每月"等周期性任务
124
- 7. 记录到「周期性任务」章节,格式:[周期] 任务内容
125
- 8. 示例:[每周一] 提交周报、[每周五] 制定下周计划
150
+ 5. 空章节保留标题
126
151
 
127
152
  ## 关键数字保留【重要】
128
- 9. 所有具体数字必须原样保留并用 **加粗** 标记,禁止概括为"多个"、"若干"、"大量"
129
- 10. 记录到「关键数据」章节,格式:事项描述 **数字**
130
- 11. 正确示例:处置 **97起** 案件、涉及金额 **230万元**
131
- 12. 错误示例:处置多起案件、涉及大额资金
153
+ - 所有具体数字必须原样保留并用 **加粗** 标记
154
+ - 禁止概括为"多个"、"若干"、"大量"
155
+ - 正确:处置 **97起** 案件
156
+ - 错误:处置多起案件
132
157
 
133
- ## 时间敏感信息提取
134
- 13. 任务时间线:提取用户提到的日期、截止时间、计划安排、里程碑,记录到「任务时间线」
135
- 14. 待办事项:识别用户说"下次"、"回头"、"先这样"、"稍后"、"改天"等挂起信号,将未完成的请求记录到「待办事项」
136
- 15. 已完成事项:如果对话中确认某个待办已完成,将其从「待办事项」移除或标记为 [x]
158
+ ## 时间敏感信息
159
+ - 任务时间线:提取日期、截止时间、里程碑
160
+ - 待办事项:识别"下次"、"回头"、"稍后"等挂起信号
161
+ - 已完成事项:标记为 [x] 或移除
137
162
 
138
163
  ## 矛盾检测
139
- 16. 发现用户前后说法矛盾时(如偏好、身份、需求不一致),不要直接覆盖旧信息
140
- 17. 将矛盾记录到「待澄清事项」,格式:⚠️ 用户曾说"..."(日期),但又说"..."(日期)
141
- 18. 只有用户明确澄清后,才能更新对应信息并移除待澄清标记
164
+ - Static 信息矛盾:记录到「待澄清事项」,格式:⚠️ 用户曾说"...",但又说"..."
165
+ - Dynamic 信息矛盾:直接用新信息替换旧信息
142
166
 
143
167
  ---
144
168
  *最后更新: {timestamp}*
@@ -152,12 +176,31 @@ DEFAULT_PROFILE_COMPRESS_PROMPT = """你是用户画像压缩专家。当前用
152
176
  ## 任务
153
177
  将用户画像压缩到 {max_chars} 字符以内,保留最重要的信息。
154
178
 
155
- ## 压缩原则
156
- 1. 保留核心身份信息
157
- 2. 保留关键偏好(最重要的 2-3 条)
158
- 3. 合并相似条目
159
- 4. 删除过时信息
160
- 5. 保留负向反馈
179
+ ## 压缩优先级(借鉴 supermemory 的 Static/Dynamic 设计)
180
+
181
+ ### 🔒 必须保留(Static)
182
+ 1. 核心身份信息(姓名、职业、组织)
183
+ 2. 长期偏好(沟通风格、关注重点)
184
+ 3. 负向反馈(用户不满意的地方)
185
+ 4. 注意事项(规避话题)
186
+
187
+ ### 🔄 可精简(Dynamic)
188
+ 5. 当前关注:只保留最近的 2-3 项
189
+ 6. 周期性任务:合并相似项
190
+ 7. 关键数据:只保留最重要的数字
191
+ 8. 任务时间线:删除已过期的事项
192
+
193
+ ### ❌ 可删除
194
+ 9. 已完成的待办事项
195
+ 10. 过期的时间线事项
196
+ 11. 重复或相似的条目
197
+ 12. 具体事件细节(Episodes)
198
+
199
+ ## 压缩技巧
200
+ - 合并相似条目
201
+ - 用概括替代列举(但保留关键数字)
202
+ - 删除冗余描述
203
+ - 保持章节结构完整
161
204
 
162
205
  ## 输出格式
163
206
  直接输出压缩后的完整 Markdown 格式用户画像。
@@ -249,105 +292,109 @@ IMAGE_SEARCH_PROMPT = """根据用户查询,从图片列表中找出匹配的
249
292
  # 舆情行业模板
250
293
  YUQING_PROFILE_TEMPLATE = ProfileTemplate(
251
294
  description="舆情行业用户画像",
252
- sections="""## 基本信息
295
+ sections="""## 🔒 稳定信息(Static)
253
296
  - 姓名/称呼:
254
297
  - 职位/角色:
255
298
  - 所属机构:
256
299
  - 职责范围:
257
-
258
- ## 偏好习惯
259
- - 报告风格:(详细/简洁、图表/文字等)
300
+ - 报告风格偏好:(详细/简洁、图表/文字等)
260
301
  - 沟通方式:(直接/委婉、正式/随意等)
302
+
303
+ ## 🔄 动态信息(Dynamic)
304
+
305
+ ### 当前关注
261
306
  - 关注重点:(时效性/准确性/全面性等)
262
- - 图表偏好:
307
+ - 进行中任务:
263
308
 
264
- ## 周期性任务
265
- (固定日程和重复性工作,格式:[周期] 任务内容)
266
- - 例:[每周一] 提交周报、[每周五] 制定下周计划、[每月底] 月度汇总
309
+ ### 周期性任务
310
+ (格式:[周期] 任务内容)
267
311
 
268
- ## 关键数据
269
- (重要的数字、金额、数量,用加粗标记)
270
- - 例:处置 **97起** 案件、涉及金额 **230万元**
312
+ ### 关键数据
313
+ (用 **加粗** 标记重要数字)
271
314
 
272
- ## 任务时间线
273
- (报告截止日期、汇报计划、定期任务,格式:[YYYY-MM-DD] 事项)
315
+ ### 任务时间线
316
+ (报告截止、汇报计划,格式:[YYYY-MM-DD] 事项)
274
317
 
275
- ## 待办事项
276
- (用户提出但未完成的请求,格式:- [ ] 事项(MM-DD 提出))
318
+ ### 待办事项
319
+ (格式:- [ ] 事项(MM-DD 提出))
277
320
 
278
- ## 待澄清事项
279
- (用户前后矛盾或模糊的信息,需要下次确认)
321
+ ## ⚠️ 待澄清事项
280
322
 
281
- ## 用户反馈
323
+ ## 📝 用户反馈
282
324
  - 正向反馈:
283
325
  - 负向反馈:
284
326
 
285
- ## 注意事项
286
- (敏感话题、规避要点、历史问题等)"""
327
+ ## 🚫 注意事项
328
+ (敏感话题、规避要点等)"""
287
329
  )
288
330
 
289
331
 
290
332
  # 电商客服模板
291
333
  ECOMMERCE_PROFILE_TEMPLATE = ProfileTemplate(
292
334
  description="电商客户画像",
293
- sections="""## 基本信息
335
+ sections="""## 🔒 稳定信息(Static)
294
336
  - 称呼:
295
337
  - 会员等级:
296
338
  - 常用收货地址:
297
-
298
- ## 购物偏好
299
339
  - 常购品类:
300
- - 价格敏感度:
301
340
  - 品牌偏好:
341
+ - 沟通风格偏好:
342
+
343
+ ## 🔄 动态信息(Dynamic)
302
344
 
303
- ## 服务记录
345
+ ### 当前关注
346
+ - 近期购买意向:
347
+ - 进行中订单:
348
+
349
+ ### 服务记录
304
350
  - 历史订单问题:
305
351
  - 退换货记录:
306
- - 投诉记录:
307
352
 
308
- ## 待办事项
309
- (未处理的售后、待跟进的问题,格式:- [ ] 事项(MM-DD 提出))
353
+ ### 待办事项
354
+ (未处理售后,格式:- [ ] 事项(MM-DD 提出))
310
355
 
311
- ## 待澄清事项
312
- (用户前后矛盾的需求,如地址、商品规格不一致等)
356
+ ## ⚠️ 待澄清事项
357
+ (地址、规格等矛盾信息)
313
358
 
314
- ## 沟通偏好
315
- - 响应速度要求:
316
- - 沟通风格:
359
+ ## 📝 用户反馈
360
+ - 正向反馈:
361
+ - 负向反馈:
317
362
 
318
- ## 注意事项
319
- (特殊需求、敏感话题等)"""
363
+ ## 🚫 注意事项"""
320
364
  )
321
365
 
322
366
 
323
367
  # 医疗助手模板
324
368
  MEDICAL_PROFILE_TEMPLATE = ProfileTemplate(
325
369
  description="患者健康档案摘要",
326
- sections="""## 基本信息
370
+ sections="""## 🔒 稳定信息(Static)
327
371
  - 称呼:
328
372
  - 年龄段:
329
- - 主要健康关注:
330
-
331
- ## 健康背景
332
373
  - 已知病史:
333
374
  - 过敏信息:
334
- - 用药情况:
375
+ - 咨询偏好:(信息详细程度、关注重点)
335
376
 
336
- ## 任务时间线
337
- (复诊日期、用药周期、检查计划,格式:[YYYY-MM-DD] 事项)
377
+ ## 🔄 动态信息(Dynamic)
378
+
379
+ ### 当前关注
380
+ - 主要健康关注:
381
+ - 当前用药:
338
382
 
339
- ## 待办事项
340
- (待预约的检查、待确认的用药调整等)
383
+ ### 任务时间线
384
+ (复诊、用药周期,格式:[YYYY-MM-DD] 事项)
341
385
 
342
- ## 待澄清事项
343
- (症状描述前后不一致、用药信息矛盾等,需下次确认)
386
+ ### 待办事项
387
+ (待预约检查、待确认用药等)
344
388
 
345
- ## 咨询偏好
346
- - 信息详细程度:
347
- - 关注重点:
389
+ ## ⚠️ 待澄清事项
390
+ (症状、用药信息矛盾)
391
+
392
+ ## 📝 用户反馈
393
+ - 正向反馈:
394
+ - 负向反馈:
348
395
 
349
- ## 注意事项
350
- (沟通禁忌、敏感话题等)"""
396
+ ## 🚫 注意事项
397
+ (沟通禁忌等)"""
351
398
  )
352
399
 
353
400
 
mem1/storage.py CHANGED
@@ -397,3 +397,241 @@ class ESStorage(StorageBackend):
397
397
  sort=[{"timestamp": {"order": "asc"}}]
398
398
  )
399
399
  return [hit["_source"] for hit in response["hits"]["hits"]]
400
+
401
+
402
+ class SQLiteStorage(StorageBackend):
403
+ """SQLite 存储后端(零依赖,适合单机/开发环境)"""
404
+
405
+ def __init__(self, db_path: str):
406
+ """
407
+ Args:
408
+ db_path: 数据库文件路径
409
+ """
410
+ import sqlite3
411
+ import json
412
+ from pathlib import Path
413
+
414
+ self.db_path = db_path
415
+ self.json = json
416
+
417
+ # 确保目录存在
418
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
419
+
420
+ self.conn = sqlite3.connect(db_path, check_same_thread=False)
421
+ self.conn.row_factory = sqlite3.Row
422
+ self.ensure_schema()
423
+
424
+ def ensure_schema(self) -> None:
425
+ """创建表结构"""
426
+ cursor = self.conn.cursor()
427
+
428
+ # 对话记录表
429
+ cursor.execute("""
430
+ CREATE TABLE IF NOT EXISTS conversations (
431
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
432
+ user_id TEXT NOT NULL,
433
+ topic_id TEXT NOT NULL,
434
+ timestamp TEXT NOT NULL,
435
+ messages TEXT NOT NULL,
436
+ metadata TEXT,
437
+ images TEXT,
438
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
439
+ )
440
+ """)
441
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_conv_user ON conversations(user_id)")
442
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_conv_topic ON conversations(user_id, topic_id)")
443
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_conv_time ON conversations(timestamp)")
444
+
445
+ # 用户画像表
446
+ cursor.execute("""
447
+ CREATE TABLE IF NOT EXISTS profiles (
448
+ user_id TEXT PRIMARY KEY,
449
+ content TEXT NOT NULL,
450
+ updated_at TEXT NOT NULL
451
+ )
452
+ """)
453
+
454
+ # 用户状态表
455
+ cursor.execute("""
456
+ CREATE TABLE IF NOT EXISTS user_states (
457
+ user_id TEXT PRIMARY KEY,
458
+ rounds INTEGER NOT NULL DEFAULT 0,
459
+ last_update TEXT
460
+ )
461
+ """)
462
+
463
+ self.conn.commit()
464
+
465
+ # ========== 对话记录 ==========
466
+
467
+ def save_conversation(self, conversation: Dict[str, Any]) -> str:
468
+ cursor = self.conn.cursor()
469
+ cursor.execute("""
470
+ INSERT INTO conversations (user_id, topic_id, timestamp, messages, metadata, images)
471
+ VALUES (?, ?, ?, ?, ?, ?)
472
+ """, (
473
+ conversation["user_id"],
474
+ conversation["topic_id"],
475
+ conversation["timestamp"],
476
+ self.json.dumps(conversation.get("messages", []), ensure_ascii=False),
477
+ self.json.dumps(conversation.get("metadata", {}), ensure_ascii=False),
478
+ self.json.dumps(conversation.get("images", []), ensure_ascii=False) if conversation.get("images") else None
479
+ ))
480
+ self.conn.commit()
481
+ return str(cursor.lastrowid)
482
+
483
+ def get_conversations(
484
+ self,
485
+ user_id: str,
486
+ topic_id: Optional[str] = None,
487
+ start_time: Optional[datetime] = None,
488
+ end_time: Optional[datetime] = None,
489
+ metadata_filter: Optional[Dict[str, Any]] = None,
490
+ limit: int = 1000
491
+ ) -> List[Dict[str, Any]]:
492
+ query = "SELECT * FROM conversations WHERE user_id = ?"
493
+ params = [user_id]
494
+
495
+ if topic_id:
496
+ query += " AND topic_id = ?"
497
+ params.append(topic_id)
498
+
499
+ if start_time:
500
+ query += " AND timestamp >= ?"
501
+ params.append(start_time.strftime('%Y-%m-%d %H:%M:%S'))
502
+
503
+ if end_time:
504
+ query += " AND timestamp < ?"
505
+ params.append(end_time.strftime('%Y-%m-%d %H:%M:%S'))
506
+
507
+ query += " ORDER BY timestamp ASC LIMIT ?"
508
+ params.append(limit)
509
+
510
+ cursor = self.conn.cursor()
511
+ cursor.execute(query, params)
512
+
513
+ results = []
514
+ for row in cursor.fetchall():
515
+ conv = {
516
+ "user_id": row["user_id"],
517
+ "topic_id": row["topic_id"],
518
+ "timestamp": row["timestamp"],
519
+ "messages": self.json.loads(row["messages"]),
520
+ "metadata": self.json.loads(row["metadata"]) if row["metadata"] else {}
521
+ }
522
+ if row["images"]:
523
+ conv["images"] = self.json.loads(row["images"])
524
+
525
+ # metadata_filter 过滤
526
+ if metadata_filter:
527
+ match = all(conv.get("metadata", {}).get(k) == v for k, v in metadata_filter.items())
528
+ if not match:
529
+ continue
530
+
531
+ results.append(conv)
532
+
533
+ return results
534
+
535
+ def delete_conversations(self, user_id: str, topic_id: Optional[str] = None) -> int:
536
+ query = "DELETE FROM conversations WHERE user_id = ?"
537
+ params = [user_id]
538
+
539
+ if topic_id:
540
+ query += " AND topic_id = ?"
541
+ params.append(topic_id)
542
+
543
+ cursor = self.conn.cursor()
544
+ cursor.execute(query, params)
545
+ self.conn.commit()
546
+ return cursor.rowcount
547
+
548
+ # ========== 用户画像 ==========
549
+
550
+ def get_profile(self, user_id: str) -> Optional[Dict[str, Any]]:
551
+ cursor = self.conn.cursor()
552
+ cursor.execute("SELECT content, updated_at FROM profiles WHERE user_id = ?", (user_id,))
553
+ row = cursor.fetchone()
554
+ if row:
555
+ return {"content": row["content"], "updated_at": row["updated_at"]}
556
+ return None
557
+
558
+ def save_profile(self, user_id: str, content: str) -> None:
559
+ cursor = self.conn.cursor()
560
+ cursor.execute("""
561
+ INSERT OR REPLACE INTO profiles (user_id, content, updated_at)
562
+ VALUES (?, ?, ?)
563
+ """, (user_id, content, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
564
+ self.conn.commit()
565
+
566
+ def delete_profile(self, user_id: str) -> bool:
567
+ cursor = self.conn.cursor()
568
+ cursor.execute("DELETE FROM profiles WHERE user_id = ?", (user_id,))
569
+ self.conn.commit()
570
+ return cursor.rowcount > 0
571
+
572
+ # ========== 用户状态 ==========
573
+
574
+ def get_user_state(self, user_id: str) -> Optional[Dict[str, Any]]:
575
+ cursor = self.conn.cursor()
576
+ cursor.execute("SELECT rounds, last_update FROM user_states WHERE user_id = ?", (user_id,))
577
+ row = cursor.fetchone()
578
+ if row:
579
+ return {"user_id": user_id, "rounds": row["rounds"], "last_update": row["last_update"]}
580
+ return None
581
+
582
+ def save_user_state(self, user_id: str, rounds: int, last_update: Optional[str] = None) -> None:
583
+ cursor = self.conn.cursor()
584
+ cursor.execute("""
585
+ INSERT OR REPLACE INTO user_states (user_id, rounds, last_update)
586
+ VALUES (?, ?, ?)
587
+ """, (user_id, rounds, last_update))
588
+ self.conn.commit()
589
+
590
+ def delete_user_state(self, user_id: str) -> bool:
591
+ cursor = self.conn.cursor()
592
+ cursor.execute("DELETE FROM user_states WHERE user_id = ?", (user_id,))
593
+ self.conn.commit()
594
+ return cursor.rowcount > 0
595
+
596
+ # ========== 聚合查询 ==========
597
+
598
+ def get_user_list(self) -> List[str]:
599
+ cursor = self.conn.cursor()
600
+ cursor.execute("SELECT DISTINCT user_id FROM conversations")
601
+ return [row["user_id"] for row in cursor.fetchall()]
602
+
603
+ def get_topic_list(self, user_id: str) -> List[Dict[str, Any]]:
604
+ cursor = self.conn.cursor()
605
+ cursor.execute("""
606
+ SELECT topic_id, COUNT(*) as count, MAX(timestamp) as latest
607
+ FROM conversations
608
+ WHERE user_id = ?
609
+ GROUP BY topic_id
610
+ """, (user_id,))
611
+
612
+ return [{
613
+ "topic_id": row["topic_id"],
614
+ "conversation_count": row["count"],
615
+ "last_active": row["latest"]
616
+ } for row in cursor.fetchall()]
617
+
618
+ def get_conversations_with_images(self, user_id: str) -> List[Dict[str, Any]]:
619
+ """获取用户所有带图片的对话"""
620
+ cursor = self.conn.cursor()
621
+ cursor.execute("""
622
+ SELECT * FROM conversations
623
+ WHERE user_id = ? AND images IS NOT NULL AND images != '[]'
624
+ ORDER BY timestamp ASC
625
+ """, (user_id,))
626
+
627
+ results = []
628
+ for row in cursor.fetchall():
629
+ results.append({
630
+ "user_id": row["user_id"],
631
+ "topic_id": row["topic_id"],
632
+ "timestamp": row["timestamp"],
633
+ "messages": self.json.loads(row["messages"]),
634
+ "metadata": self.json.loads(row["metadata"]) if row["metadata"] else {},
635
+ "images": self.json.loads(row["images"])
636
+ })
637
+ return results
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mem1
3
- Version: 0.1.1
4
- Summary: 基于云服务的用户记忆系统
3
+ Version: 0.1.2
4
+ Summary: AI 用户记忆系统:Static/Dynamic 画像分层 + 时间衰减遗忘 + SQLite/ES 可插拔存储 + 图片记忆 + 话题隔离。借鉴 supermemory 设计,不用向量数据库。
5
5
  Project-URL: Homepage, https://github.com/sougannkyou/mem1
6
6
  Project-URL: Repository, https://github.com/sougannkyou/mem1
7
7
  Author: Song
8
8
  License: MIT
9
- Keywords: langchain,llm,memory,user-profile
9
+ Keywords: langchain,llm,memory,supermemory,user-profile
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
@@ -14,13 +14,18 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Python: >=3.12
17
- Requires-Dist: dashscope>=1.14.0
18
- Requires-Dist: elasticsearch>=8.0.0
19
17
  Requires-Dist: openai>=1.0.0
20
18
  Requires-Dist: pydantic>=2.0.0
21
19
  Requires-Dist: python-dotenv>=1.0.0
20
+ Provides-Extra: all
21
+ Requires-Dist: dashscope>=1.14.0; extra == 'all'
22
+ Requires-Dist: elasticsearch>=8.0.0; extra == 'all'
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: ipython>=8.0.0; extra == 'dev'
25
+ Provides-Extra: elasticsearch
26
+ Requires-Dist: elasticsearch>=8.0.0; extra == 'elasticsearch'
27
+ Provides-Extra: vl
28
+ Requires-Dist: dashscope>=1.14.0; extra == 'vl'
24
29
  Description-Content-Type: text/markdown
25
30
 
26
31
  # mem1 - 用户记忆系统
@@ -0,0 +1,12 @@
1
+ mem1/__init__.py,sha256=Xr8VYSkb4fnvgHHqVyDnK9B5TX9CxAMtppiSn-_TG6s,582
2
+ mem1/config.py,sha256=u21hC2VqjQUxi5OHrG-9Y324VvUptpcaAffbE9BeFJs,7349
3
+ mem1/langchain_middleware.py,sha256=h2mG7K2Tq1N7IovXMvCyvOhsAwTWOR1NAqivF4db2AE,6648
4
+ mem1/llm.py,sha256=MgqlefPrXpIf0FpGruDEjz8SbI7iKu-wxpiJCPCX6co,4439
5
+ mem1/memory.py,sha256=YTAQqIeq5aUUjI4Ynn1-wLq9OLWe14v-sBaED65uK7M,25872
6
+ mem1/memory_md.py,sha256=uu_TvdBoUpAncT1eissOSe1Y3vCy3iWMcuvCy3vCjEA,26258
7
+ mem1/memory_tools.py,sha256=b1YBiRNet0gXnW-KGIZ2KQclluB9Q6dli_DbWLS571k,3646
8
+ mem1/prompts.py,sha256=B-i8u8eRsoVzSsS8zm2rtCj01znQe3p7s5dD4s0P4Lk,11218
9
+ mem1/storage.py,sha256=wf4mYKvJ42tIponBrKD2TNxC5emYoxCvQkBUG1V_Pys,21477
10
+ mem1-0.1.2.dist-info/METADATA,sha256=i0LQ4ocDjvRxBynfbg-PHtRDHd0d7yNk7S7A2DkAfCM,10913
11
+ mem1-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ mem1-0.1.2.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- mem1/__init__.py,sha256=Xr8VYSkb4fnvgHHqVyDnK9B5TX9CxAMtppiSn-_TG6s,582
2
- mem1/config.py,sha256=BqF3O4FQnhmnr7iCynK6g5BUxaqnZ0cVnggR4hNsDcQ,5665
3
- mem1/langchain_middleware.py,sha256=h2mG7K2Tq1N7IovXMvCyvOhsAwTWOR1NAqivF4db2AE,6648
4
- mem1/llm.py,sha256=MgqlefPrXpIf0FpGruDEjz8SbI7iKu-wxpiJCPCX6co,4439
5
- mem1/memory.py,sha256=NPsPzphb1tYMp8rK9pxr1rtipocVNIrDueihU-DOWPU,24515
6
- mem1/memory_md.py,sha256=uu_TvdBoUpAncT1eissOSe1Y3vCy3iWMcuvCy3vCjEA,26258
7
- mem1/memory_tools.py,sha256=b1YBiRNet0gXnW-KGIZ2KQclluB9Q6dli_DbWLS571k,3646
8
- mem1/prompts.py,sha256=5HUG-yvTD7iBUzzXwO-WnRomDLkz0UJWox3z3zcT0kI,10599
9
- mem1/storage.py,sha256=J2JUTjPEXe3dO21LVoj3sl8_78qKOECad2Ol5R9kvCU,12774
10
- mem1-0.1.1.dist-info/METADATA,sha256=chkdyE8MYG-3izQHSRbAO1gMMZDyALYszm4vFdZGMnc,10541
11
- mem1-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- mem1-0.1.1.dist-info/RECORD,,
File without changes