Coverage for open_webui/models/memories.py: 45%

166 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 05:07 +0000

1"""Long-term memory storage for per-user context recall.""" 

2 

3from __future__ import annotations 

4 

5import time 

6import uuid 

7from typing import Literal 

8 

9from open_webui.internal.db import Base, get_async_db_context 

10from pydantic import BaseModel, ConfigDict 

11from sqlalchemy import JSON, BigInteger, Column, Index, String, Text, delete, select 

12from sqlalchemy.ext.asyncio import AsyncSession 

13 

14 

15class Memory(Base): # user memory store 

16 """Stores user-created memory entries linked to a vector collection.""" 

17 

18 __tablename__ = 'memory' 

19 __table_args__ = (Index('ix_memory_id_user_id', 'id', 'user_id'),) 

20 

21 id = Column(String, primary_key=True, unique=True) 

22 user_id = Column(String, index=True) 

23 type = Column(String, default='context', server_default='context', index=True) 

24 path = Column(Text, nullable=True) 

25 content = Column(Text) # free-form text learned from conversation 

26 meta = Column(JSON, nullable=True) 

27 updated_at = Column(BigInteger) # epoch seconds 

28 created_at = Column(BigInteger) # epoch seconds 

29 

30 

31class MemoryModel(BaseModel): 

32 """Pydantic mirror of the Memory table row.""" 

33 

34 id: str 

35 user_id: str 

36 type: Literal['user', 'context'] = 'context' 

37 path: str | None = None 

38 content: str 

39 meta: dict | None = None 

40 updated_at: int # timestamp in epoch 

41 created_at: int # timestamp in epoch 

42 model_config = ConfigDict(from_attributes=True) # allows ORM mapping 

43 

44 

45class MemoriesTable: 

46 @staticmethod 

47 def normalize_memory_type(memory_type: str | None = None) -> str: 

48 return 'user' if memory_type == 'user' else 'context' 

49 

50 async def insert_new_memory( 

51 self, 

52 user_id: str, 

53 content: str, 

54 memory_type: str | None = None, 

55 path: str | None = None, 

56 meta: dict | None = None, 

57 db: AsyncSession | None = None, 

58 ) -> MemoryModel | None: 

59 """Persist a new memory entry and return the created model.""" 

60 async with get_async_db_context(db) as db: 

61 now = int(time.time()) 

62 record = Memory( 

63 id=str(uuid.uuid4()), 

64 user_id=user_id, 

65 type=self.normalize_memory_type(memory_type), 

66 path=path, 

67 content=content, 

68 meta=meta, 

69 created_at=now, 

70 updated_at=now, 

71 ) 

72 db.add(record) 

73 await db.commit() 

74 return MemoryModel.model_validate(record) if record else None 

75 

76 async def update_memory_by_id_and_user_id( 

77 self, 

78 id: str, 

79 user_id: str, 

80 content: str | None, 

81 memory_type: str | None = None, 

82 path: str | None = None, 

83 update_path: bool = False, 

84 meta: dict | None = None, 

85 db: AsyncSession | None = None, 

86 ) -> MemoryModel | None: 

87 async with get_async_db_context(db) as db: 

88 try: 

89 memory = await db.get(Memory, id) 

90 if not memory or memory.user_id != user_id: 

91 return None 

92 

93 if content is not None: 

94 memory.content = content 

95 if memory_type is not None: 

96 memory.type = self.normalize_memory_type(memory_type) 

97 if update_path: 

98 memory.path = path 

99 if meta is not None: 

100 memory.meta = {**(memory.meta or {}), **meta} 

101 memory.updated_at = int(time.time()) 

102 

103 await db.commit() 

104 return MemoryModel.model_validate(memory) 

105 except Exception: 

106 return None 

107 

108 async def get_memories(self, db: AsyncSession | None = None) -> list[MemoryModel]: 

109 async with get_async_db_context(db) as db: 

110 try: 

111 result = await db.execute(select(Memory)) 

112 memories = result.scalars().all() 

113 return [MemoryModel.model_validate(memory) for memory in memories] 

114 except Exception: 

115 return None 

116 

117 async def get_memories_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[MemoryModel]: 

118 async with get_async_db_context(db) as db: 

119 try: 

120 result = await db.execute(select(Memory).filter_by(user_id=user_id)) 

121 memories = result.scalars().all() 

122 return [MemoryModel.model_validate(memory) for memory in memories] 

123 except Exception: 

124 return None 

125 

126 async def get_memory_by_id(self, id: str, db: AsyncSession | None = None) -> MemoryModel | None: 

127 async with get_async_db_context(db) as db: 

128 try: 

129 memory = await db.get(Memory, id) 

130 return MemoryModel.model_validate(memory) if memory else None 

131 except Exception: 

132 return None 

133 

134 async def delete_memory_by_id(self, id: str, db: AsyncSession | None = None) -> bool: 

135 async with get_async_db_context(db) as db: 

136 try: 

137 await db.execute(delete(Memory).filter_by(id=id)) 

138 await db.commit() 

139 

140 return True 

141 

142 except Exception: 

143 return False 

144 

145 async def delete_memories_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: 

146 async with get_async_db_context(db) as db: 

147 try: 

148 await db.execute(delete(Memory).filter_by(user_id=user_id)) 

149 await db.commit() 

150 

151 return True 

152 except Exception: 

153 return False 

154 

155 async def delete_memory_by_id_and_user_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: 

156 async with get_async_db_context(db) as db: 

157 try: 

158 memory = await db.get(Memory, id) 

159 if not memory or memory.user_id != user_id: 

160 return False 

161 

162 await db.delete(memory) 

163 await db.commit() 

164 return True 

165 except Exception: 

166 return False 

167 

168 async def apply_memory_operations( 

169 self, 

170 user_id: str, 

171 operations: list[dict], 

172 db: AsyncSession | None = None, 

173 ) -> list[dict]: 

174 now = int(time.time()) 

175 results: list[dict] = [] 

176 

177 async with get_async_db_context(db) as db: 

178 for operation in operations: 178 ↛ 263line 178 didn't jump to line 263 because the loop on line 178 didn't complete

179 action = operation.get('action') 

180 

181 if action == 'add': 

182 content = operation.get('content', '').strip() 

183 memory_type = self.normalize_memory_type(operation.get('type')) 

184 path = operation.get('path') 

185 result = await db.execute( 

186 select(Memory).filter_by(user_id=user_id, content=content, type=memory_type, path=path) 

187 ) 

188 existing = result.scalars().first() 

189 if existing: 

190 results.append( 

191 { 

192 'action': action, 

193 'status': 'skipped', 

194 'memory': MemoryModel.model_validate(existing), 

195 'reason': 'duplicate', 

196 } 

197 ) 

198 continue 

199 

200 memory = Memory( 

201 id=str(uuid.uuid4()), 

202 user_id=user_id, 

203 type=memory_type, 

204 path=path, 

205 content=content, 

206 meta=operation.get('meta'), 

207 created_at=now, 

208 updated_at=now, 

209 ) 

210 db.add(memory) 

211 await db.flush() 

212 results.append( 

213 {'action': action, 'status': 'created', 'memory': MemoryModel.model_validate(memory)} 

214 ) 

215 

216 elif action == 'replace': 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true

217 memory_id = operation.get('id') 

218 content = operation.get('content', '').strip() 

219 memory = await db.get(Memory, memory_id) 

220 if not memory or memory.user_id != user_id: 

221 raise ValueError(f'Memory not found: {memory_id}') 

222 

223 memory.content = content 

224 if operation.get('type') is not None: 

225 memory.type = self.normalize_memory_type(operation.get('type')) 

226 if 'path' in operation: 

227 memory.path = operation.get('path') 

228 if operation.get('meta') is not None: 

229 memory.meta = {**(memory.meta or {}), **operation.get('meta')} 

230 memory.updated_at = now 

231 await db.flush() 

232 results.append( 

233 {'action': action, 'status': 'updated', 'memory': MemoryModel.model_validate(memory)} 

234 ) 

235 

236 elif action == 'move': 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 memory_id = operation.get('id') 

238 memory = await db.get(Memory, memory_id) 

239 if not memory or memory.user_id != user_id: 

240 raise ValueError(f'Memory not found: {memory_id}') 

241 

242 memory.path = operation.get('path') 

243 if operation.get('meta') is not None: 

244 memory.meta = {**(memory.meta or {}), **operation.get('meta')} 

245 memory.updated_at = now 

246 await db.flush() 

247 results.append( 

248 {'action': action, 'status': 'updated', 'memory': MemoryModel.model_validate(memory)} 

249 ) 

250 

251 elif action == 'remove': 251 ↛ 261line 251 didn't jump to line 261 because the condition on line 251 was always true

252 memory_id = operation.get('id') 

253 memory = await db.get(Memory, memory_id) 

254 if not memory or memory.user_id != user_id: 

255 raise ValueError(f'Memory not found: {memory_id}') 

256 

257 await db.delete(memory) 

258 results.append({'action': action, 'status': 'deleted', 'id': memory_id}) 

259 

260 else: 

261 raise ValueError(f'Unsupported memory operation: {action}') 

262 

263 await db.commit() 

264 

265 return results 

266 

267 

268Memories = MemoriesTable() # user memory registry