MemoryOS 0.1.12__py3-none-any.whl → 0.2.0__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.

Potentially problematic release.


This version of MemoryOS might be problematic. Click here for more details.

Files changed (32) hide show
  1. {memoryos-0.1.12.dist-info → memoryos-0.2.0.dist-info}/METADATA +51 -31
  2. {memoryos-0.1.12.dist-info → memoryos-0.2.0.dist-info}/RECORD +32 -21
  3. memos/__init__.py +1 -1
  4. memos/configs/internet_retriever.py +81 -0
  5. memos/configs/llm.py +1 -0
  6. memos/configs/mem_os.py +4 -0
  7. memos/configs/mem_reader.py +4 -0
  8. memos/configs/memory.py +11 -1
  9. memos/graph_dbs/item.py +46 -0
  10. memos/graph_dbs/neo4j.py +72 -5
  11. memos/llms/openai.py +1 -0
  12. memos/mem_os/main.py +491 -0
  13. memos/mem_reader/simple_struct.py +11 -6
  14. memos/mem_user/user_manager.py +10 -0
  15. memos/memories/textual/item.py +3 -1
  16. memos/memories/textual/tree.py +39 -3
  17. memos/memories/textual/tree_text_memory/organize/conflict.py +196 -0
  18. memos/memories/textual/tree_text_memory/organize/manager.py +49 -8
  19. memos/memories/textual/tree_text_memory/organize/redundancy.py +212 -0
  20. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +235 -0
  21. memos/memories/textual/tree_text_memory/organize/reorganizer.py +584 -0
  22. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +263 -0
  23. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +89 -0
  24. memos/memories/textual/tree_text_memory/retrieve/reasoner.py +1 -4
  25. memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -4
  26. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +3 -3
  27. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +335 -0
  28. memos/templates/mem_reader_prompts.py +42 -15
  29. memos/templates/mos_prompts.py +63 -0
  30. memos/templates/tree_reorganize_prompts.py +168 -0
  31. {memoryos-0.1.12.dist-info → memoryos-0.2.0.dist-info}/LICENSE +0 -0
  32. {memoryos-0.1.12.dist-info → memoryos-0.2.0.dist-info}/WHEEL +0 -0
memos/mem_os/main.py CHANGED
@@ -1,5 +1,21 @@
1
+ import concurrent.futures
2
+ import json
3
+
4
+ from typing import Any
5
+
1
6
  from memos.configs.mem_os import MOSConfig
7
+ from memos.llms.factory import LLMFactory
8
+ from memos.log import get_logger
2
9
  from memos.mem_os.core import MOSCore
10
+ from memos.memories.textual.base import BaseTextMemory
11
+ from memos.templates.mos_prompts import (
12
+ COT_DECOMPOSE_PROMPT,
13
+ PRO_MODE_WELCOME_MESSAGE,
14
+ SYNTHESIS_PROMPT,
15
+ )
16
+
17
+
18
+ logger = get_logger(__name__)
3
19
 
4
20
 
5
21
  class MOS(MOSCore):
@@ -9,4 +25,479 @@ class MOS(MOSCore):
9
25
  """
10
26
 
11
27
  def __init__(self, config: MOSConfig):
28
+ self.enable_cot = config.PRO_MODE
29
+ if config.PRO_MODE:
30
+ print(PRO_MODE_WELCOME_MESSAGE)
31
+ logger.info(PRO_MODE_WELCOME_MESSAGE)
12
32
  super().__init__(config)
33
+
34
+ def chat(self, query: str, user_id: str | None = None) -> str:
35
+ """
36
+ Enhanced chat method with optional CoT (Chain of Thought) enhancement.
37
+
38
+ Args:
39
+ query (str): The user's query.
40
+ user_id (str, optional): User ID for context.
41
+
42
+ Returns:
43
+ str: The response from the MOS.
44
+ """
45
+ # Check if CoT enhancement is enabled (either explicitly or via PRO mode)
46
+
47
+ if not self.enable_cot:
48
+ # Use the original chat method from core
49
+ return super().chat(query, user_id)
50
+
51
+ # Enhanced chat with CoT decomposition
52
+ return self._chat_with_cot_enhancement(query, user_id)
53
+
54
+ def _chat_with_cot_enhancement(self, query: str, user_id: str | None = None) -> str:
55
+ """
56
+ Chat with CoT enhancement for complex query decomposition.
57
+ This method includes all the same validation and processing logic as the core chat method.
58
+
59
+ Args:
60
+ query (str): The user's query.
61
+ user_id (str, optional): User ID for context.
62
+
63
+ Returns:
64
+ str: The enhanced response.
65
+ """
66
+ # Step 1: Perform all the same validation and setup as core chat method
67
+ target_user_id = user_id if user_id is not None else self.user_id
68
+ accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
69
+ user_cube_ids = [cube.cube_id for cube in accessible_cubes]
70
+
71
+ # Register chat history if needed
72
+ if target_user_id not in self.chat_history_manager:
73
+ self._register_chat_history(target_user_id)
74
+
75
+ chat_history = self.chat_history_manager[target_user_id]
76
+
77
+ try:
78
+ # Step 2: Decompose the query using CoT
79
+ logger.info(f"🔍 [CoT] Decomposing query: {query}")
80
+ decomposition_result = self.cot_decompose(
81
+ query, self.config.chat_model, target_user_id, self.chat_llm
82
+ )
83
+
84
+ # Check if the query is complex and needs decomposition
85
+ if not decomposition_result.get("is_complex", False):
86
+ logger.info("🔍 [CoT] Query is not complex, using standard chat")
87
+ return super().chat(query, user_id)
88
+
89
+ sub_questions = decomposition_result.get("sub_questions", [])
90
+ logger.info(f"🔍 [CoT] Decomposed into {len(sub_questions)} sub-questions")
91
+
92
+ # Step 3: Get search engine for sub-questions (with proper validation)
93
+ search_engine = self._get_search_engine_for_cot_with_validation(user_cube_ids)
94
+ if not search_engine:
95
+ logger.warning("🔍 [CoT] No search engine available, using standard chat")
96
+ return super().chat(query, user_id)
97
+
98
+ # Step 4: Get answers for sub-questions
99
+ logger.info("🔍 [CoT] Getting answers for sub-questions...")
100
+ sub_questions, sub_answers = self.get_sub_answers(
101
+ sub_questions=sub_questions,
102
+ search_engine=search_engine,
103
+ llm_config=self.config.chat_model,
104
+ user_id=target_user_id,
105
+ top_k=getattr(self.config, "cot_top_k", 3),
106
+ llm=self.chat_llm,
107
+ )
108
+
109
+ # Step 5: Generate enhanced response using sub-answers
110
+ logger.info("🔍 [CoT] Generating enhanced response...")
111
+ enhanced_response = self._generate_enhanced_response_with_context(
112
+ original_query=query,
113
+ sub_questions=sub_questions,
114
+ sub_answers=sub_answers,
115
+ chat_history=chat_history,
116
+ user_id=target_user_id,
117
+ search_engine=search_engine,
118
+ )
119
+
120
+ # Step 6: Update chat history (same as core method)
121
+ chat_history.chat_history.append({"role": "user", "content": query})
122
+ chat_history.chat_history.append({"role": "assistant", "content": enhanced_response})
123
+ self.chat_history_manager[target_user_id] = chat_history
124
+
125
+ # Step 7: Submit message to scheduler (same as core method)
126
+ if len(accessible_cubes) == 1:
127
+ mem_cube_id = accessible_cubes[0].cube_id
128
+ mem_cube = self.mem_cubes[mem_cube_id]
129
+ if self.enable_mem_scheduler and self.mem_scheduler is not None:
130
+ from datetime import datetime
131
+
132
+ from memos.mem_scheduler.modules.schemas import (
133
+ ANSWER_LABEL,
134
+ ScheduleMessageItem,
135
+ )
136
+
137
+ message_item = ScheduleMessageItem(
138
+ user_id=target_user_id,
139
+ mem_cube_id=mem_cube_id,
140
+ mem_cube=mem_cube,
141
+ label=ANSWER_LABEL,
142
+ content=enhanced_response,
143
+ timestamp=datetime.now(),
144
+ )
145
+ self.mem_scheduler.submit_messages(messages=[message_item])
146
+
147
+ return enhanced_response
148
+
149
+ except Exception as e:
150
+ logger.error(f"🔍 [CoT] Error in CoT enhancement: {e}")
151
+ logger.info("🔍 [CoT] Falling back to standard chat")
152
+ return super().chat(query, user_id)
153
+
154
+ def _get_search_engine_for_cot_with_validation(
155
+ self, user_cube_ids: list[str]
156
+ ) -> BaseTextMemory | None:
157
+ """
158
+ Get the best available search engine for CoT operations with proper validation.
159
+
160
+ Args:
161
+ user_cube_ids (list[str]): List of cube IDs the user has access to.
162
+
163
+ Returns:
164
+ BaseTextMemory or None: The search engine to use for CoT.
165
+ """
166
+ if not self.mem_cubes:
167
+ return None
168
+
169
+ # Get the first available text memory from user's accessible cubes
170
+ for mem_cube_id, mem_cube in self.mem_cubes.items():
171
+ if mem_cube_id not in user_cube_ids:
172
+ continue
173
+ if mem_cube.text_mem:
174
+ return mem_cube.text_mem
175
+
176
+ return None
177
+
178
+ def _generate_enhanced_response_with_context(
179
+ self,
180
+ original_query: str,
181
+ sub_questions: list[str],
182
+ sub_answers: list[str],
183
+ chat_history: Any,
184
+ user_id: str | None = None,
185
+ search_engine: BaseTextMemory | None = None,
186
+ ) -> str:
187
+ """
188
+ Generate an enhanced response using sub-questions and their answers, with chat context.
189
+
190
+ Args:
191
+ original_query (str): The original user query.
192
+ sub_questions (list[str]): List of sub-questions.
193
+ sub_answers (list[str]): List of answers to sub-questions.
194
+ chat_history: The user's chat history.
195
+ user_id (str, optional): User ID for context.
196
+
197
+ Returns:
198
+ str: The enhanced response.
199
+ """
200
+ # Build the synthesis prompt
201
+ qa_text = ""
202
+ for i, (question, answer) in enumerate(zip(sub_questions, sub_answers, strict=False), 1):
203
+ qa_text += f"Q{i}: {question}\nA{i}: {answer}\n\n"
204
+
205
+ # Build messages with chat history context (similar to core method)
206
+ if (search_engine is not None) and self.config.enable_textual_memory:
207
+ if self.enable_cot:
208
+ search_memories = search_engine.search(
209
+ original_query, top_k=self.config.top_k, mode="fine"
210
+ )
211
+ else:
212
+ search_memories = search_engine.search(
213
+ original_query, top_k=self.config.top_k, mode="fast"
214
+ )
215
+ system_prompt = self._build_system_prompt(
216
+ search_memories
217
+ ) # Use the same system prompt builder
218
+ else:
219
+ system_prompt = self._build_system_prompt()
220
+ current_messages = [
221
+ {"role": "system", "content": system_prompt + SYNTHESIS_PROMPT.format(qa_text=qa_text)},
222
+ *chat_history.chat_history,
223
+ {
224
+ "role": "user",
225
+ "content": original_query,
226
+ },
227
+ ]
228
+
229
+ # Handle activation memory if enabled (same as core method)
230
+ past_key_values = None
231
+ if self.config.enable_activation_memory:
232
+ assert self.config.chat_model.backend == "huggingface", (
233
+ "Activation memory only used for huggingface backend."
234
+ )
235
+ # Get accessible cubes for the user
236
+ target_user_id = user_id if user_id is not None else self.user_id
237
+ accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
238
+ user_cube_ids = [cube.cube_id for cube in accessible_cubes]
239
+
240
+ for mem_cube_id, mem_cube in self.mem_cubes.items():
241
+ if mem_cube_id not in user_cube_ids:
242
+ continue
243
+ if mem_cube.act_mem:
244
+ kv_cache = next(iter(mem_cube.act_mem.get_all()), None)
245
+ past_key_values = (
246
+ kv_cache.memory if (kv_cache and hasattr(kv_cache, "memory")) else None
247
+ )
248
+ break
249
+
250
+ try:
251
+ # Generate the enhanced response using the chat LLM with same parameters as core
252
+ if past_key_values is not None:
253
+ enhanced_response = self.chat_llm.generate(
254
+ current_messages, past_key_values=past_key_values
255
+ )
256
+ else:
257
+ enhanced_response = self.chat_llm.generate(current_messages)
258
+
259
+ logger.info("🔍 [CoT] Generated enhanced response")
260
+ return enhanced_response
261
+ except Exception as e:
262
+ logger.error(f"🔍 [CoT] Error generating enhanced response: {e}")
263
+ # Fallback to standard chat
264
+ return super().chat(original_query, user_id)
265
+
266
+ @classmethod
267
+ def cot_decompose(
268
+ cls, query: str, llm_config: Any, user_id: str | None = None, llm: LLMFactory | None = None
269
+ ) -> list[str] | dict[str, Any]:
270
+ """
271
+ Decompose a complex query into sub-questions using Chain of Thought reasoning.
272
+
273
+ Args:
274
+ query (str): The complex query to decompose
275
+ llm_config: LLM configuration for decomposition
276
+ user_id (str, optional): User ID for context
277
+
278
+ Returns:
279
+ Union[List[str], Dict[str, Any]]: List of decomposed sub-questions or dict with complexity analysis
280
+ """
281
+ # Create a temporary LLM instance for decomposition
282
+ if llm is None:
283
+ llm = LLMFactory.from_config(llm_config)
284
+
285
+ # System prompt for CoT decomposition with complexity analysis
286
+ system_prompt = COT_DECOMPOSE_PROMPT.format(query=query)
287
+
288
+ messages = [{"role": "system", "content": system_prompt}]
289
+
290
+ try:
291
+ response = llm.generate(messages)
292
+ # Try to parse JSON response
293
+ result = json.loads(response)
294
+ return result
295
+ except json.JSONDecodeError as e:
296
+ logger.warning(f"Failed to parse JSON response from LLM: {e}")
297
+ logger.warning(f"Raw response: {response}")
298
+
299
+ # Try to extract JSON-like content from the response
300
+ try:
301
+ # Look for JSON-like content between curly braces
302
+ import re
303
+
304
+ json_match = re.search(r"\{.*\}", response, re.DOTALL)
305
+ if json_match:
306
+ json_str = json_match.group(0)
307
+ result = json.loads(json_str)
308
+ return result
309
+ except Exception:
310
+ pass
311
+
312
+ # If all parsing attempts fail, return default
313
+ return {"is_complex": False, "sub_questions": []}
314
+ except Exception as e:
315
+ logger.error(f"Unexpected error in cot_decompose: {e}")
316
+ return {"is_complex": False, "sub_questions": []}
317
+
318
+ @classmethod
319
+ def get_sub_answers(
320
+ cls,
321
+ sub_questions: list[str] | dict[str, Any],
322
+ search_results: dict[str, Any] | None = None,
323
+ search_engine: BaseTextMemory | None = None,
324
+ llm_config: LLMFactory | None = None,
325
+ user_id: str | None = None,
326
+ top_k: int = 5,
327
+ llm: LLMFactory | None = None,
328
+ ) -> tuple[list[str], list[str]]:
329
+ """
330
+ Get answers for sub-questions using either search results or a search engine.
331
+
332
+ Args:
333
+ sub_questions (Union[List[str], Dict[str, Any]]): List of sub-questions from cot_decompose or dict with analysis
334
+ search_results (Dict[str, Any], optional): Search results containing relevant information
335
+ search_engine (BaseTextMemory, optional): Text memory engine for searching
336
+ llm_config (Any, optional): LLM configuration for processing (required if search_engine is provided)
337
+ user_id (str, optional): User ID for context
338
+ top_k (int): Number of top results to retrieve from search engine
339
+
340
+ Returns:
341
+ Tuple[List[str], List[str]]: (sub_questions, sub_answers)
342
+ """
343
+ # Extract sub-questions from decomposition result if needed
344
+ if isinstance(sub_questions, dict):
345
+ if not sub_questions.get("is_complex", False):
346
+ return [], []
347
+ sub_questions = sub_questions.get("sub_questions", [])
348
+
349
+ if not sub_questions:
350
+ return [], []
351
+
352
+ # Validate inputs
353
+ if search_results is None and search_engine is None:
354
+ raise ValueError("Either search_results or search_engine must be provided")
355
+ if llm is None:
356
+ llm = LLMFactory.from_config(llm_config)
357
+
358
+ # Step 1: Get search results if search_engine is provided
359
+ if search_engine is not None:
360
+ search_results = cls._search_with_engine(sub_questions, search_engine, top_k)
361
+
362
+ # Step 2: Generate answers for each sub-question using LLM in parallel
363
+ def generate_answer_for_question(question_index: int, sub_question: str) -> tuple[int, str]:
364
+ """Generate answer for a single sub-question."""
365
+ # Extract relevant information from search results
366
+ relevant_info = []
367
+ if search_results and search_results.get("text_mem"):
368
+ for cube_result in search_results["text_mem"]:
369
+ for memory in cube_result.get("memories", []):
370
+ relevant_info.append(memory.memory)
371
+
372
+ # Build system prompt with memories (similar to MOSCore._build_system_prompt)
373
+ base_prompt = (
374
+ "You are a knowledgeable and helpful AI assistant. "
375
+ "You have access to relevant information that helps you provide accurate answers. "
376
+ "Use the provided information to answer the question comprehensively. "
377
+ "If the information is not sufficient, acknowledge the limitations."
378
+ )
379
+
380
+ # Add memory context if available
381
+ if relevant_info:
382
+ memory_context = "\n\n## Relevant Information:\n"
383
+ for j, info in enumerate(relevant_info[:top_k], 1): # Take top 3 most relevant
384
+ memory_context += f"{j}. {info}\n"
385
+ system_prompt = base_prompt + memory_context
386
+ else:
387
+ system_prompt = (
388
+ base_prompt
389
+ + "\n\n## Relevant Information:\nNo specific information found in memory."
390
+ )
391
+
392
+ # Create messages for LLM
393
+ messages = [
394
+ {"role": "system", "content": system_prompt},
395
+ {"role": "user", "content": sub_question},
396
+ ]
397
+
398
+ try:
399
+ # Generate answer using LLM
400
+ response = llm.generate(messages)
401
+ return question_index, response
402
+ except Exception as e:
403
+ logger.error(f"Failed to generate answer for sub-question '{sub_question}': {e}")
404
+ return question_index, f"Unable to generate answer for: {sub_question}"
405
+
406
+ # Generate answers in parallel while maintaining order
407
+ sub_answers = [None] * len(sub_questions)
408
+ with concurrent.futures.ThreadPoolExecutor(
409
+ max_workers=min(len(sub_questions), 10)
410
+ ) as executor:
411
+ # Submit all answer generation tasks
412
+ future_to_index = {
413
+ executor.submit(generate_answer_for_question, i, question): i
414
+ for i, question in enumerate(sub_questions)
415
+ }
416
+
417
+ # Collect results as they complete, but store them in the correct position
418
+ for future in concurrent.futures.as_completed(future_to_index):
419
+ try:
420
+ question_index, answer = future.result()
421
+ sub_answers[question_index] = answer
422
+ except Exception as e:
423
+ question_index = future_to_index[future]
424
+ logger.error(
425
+ f"Exception occurred while generating answer for question at index {question_index}: {e}"
426
+ )
427
+ sub_answers[question_index] = (
428
+ f"Error generating answer for question {question_index + 1}"
429
+ )
430
+
431
+ return sub_questions, sub_answers
432
+
433
+ @classmethod
434
+ def _search_with_engine(
435
+ cls, sub_questions: list[str], search_engine: BaseTextMemory, top_k: int
436
+ ) -> dict[str, Any]:
437
+ """
438
+ Search for sub-questions using the provided search engine in parallel.
439
+
440
+ Args:
441
+ sub_questions (List[str]): List of sub-questions to search for
442
+ search_engine (BaseTextMemory): Text memory engine for searching
443
+ top_k (int): Number of top results to retrieve
444
+
445
+ Returns:
446
+ Dict[str, Any]: Search results in the expected format
447
+ """
448
+
449
+ def search_single_question(question: str) -> list[Any]:
450
+ """Search for a single question using the search engine."""
451
+ try:
452
+ # Handle different search method signatures
453
+ if hasattr(search_engine, "search"):
454
+ # Try different parameter combinations based on the engine type
455
+ try:
456
+ # For tree_text memory
457
+ return search_engine.search(question, top_k, mode="fast")
458
+ except TypeError:
459
+ try:
460
+ # For general_text memory
461
+ return search_engine.search(question, top_k)
462
+ except TypeError:
463
+ # For naive_text memory
464
+ return search_engine.search(question, top_k)
465
+ else:
466
+ return []
467
+ except Exception as e:
468
+ logger.error(f"Search failed for question '{question}': {e}")
469
+ return []
470
+
471
+ # Search in parallel while maintaining order
472
+ all_memories = []
473
+ with concurrent.futures.ThreadPoolExecutor(
474
+ max_workers=min(len(sub_questions), 10)
475
+ ) as executor:
476
+ # Submit all search tasks and keep track of their order
477
+ future_to_index = {
478
+ executor.submit(search_single_question, question): i
479
+ for i, question in enumerate(sub_questions)
480
+ }
481
+
482
+ # Initialize results list with None values to maintain order
483
+ results = [None] * len(sub_questions)
484
+
485
+ # Collect results as they complete, but store them in the correct position
486
+ for future in concurrent.futures.as_completed(future_to_index):
487
+ index = future_to_index[future]
488
+ try:
489
+ memories = future.result()
490
+ results[index] = memories
491
+ except Exception as e:
492
+ logger.error(
493
+ f"Exception occurred while searching for question at index {index}: {e}"
494
+ )
495
+ results[index] = []
496
+
497
+ # Combine all results in the correct order
498
+ for result in results:
499
+ if result is not None:
500
+ all_memories.extend(result)
501
+
502
+ # Format results in the expected structure
503
+ return {"text_mem": [{"cube_id": "search_engine", "memories": all_memories}]}
@@ -1,7 +1,7 @@
1
1
  import concurrent.futures
2
2
  import copy
3
3
  import json
4
-
4
+ import re
5
5
  from abc import ABC
6
6
  from typing import Any
7
7
 
@@ -17,6 +17,7 @@ from memos.parsers.factory import ParserFactory
17
17
  from memos.templates.mem_reader_prompts import (
18
18
  SIMPLE_STRUCT_DOC_READER_PROMPT,
19
19
  SIMPLE_STRUCT_MEM_READER_PROMPT,
20
+ SIMPLE_STRUCT_MEM_READER_EXAMPLE,
20
21
  )
21
22
 
22
23
 
@@ -39,11 +40,11 @@ class SimpleStructMemReader(BaseMemReader, ABC):
39
40
  self.chunker = ChunkerFactory.from_config(config.chunker)
40
41
 
41
42
  def _process_chat_data(self, scene_data_info, info):
42
- prompt = (
43
- SIMPLE_STRUCT_MEM_READER_PROMPT.replace("${user_a}", "user")
44
- .replace("${user_b}", "assistant")
45
- .replace("${conversation}", "\n".join(scene_data_info))
43
+ prompt = SIMPLE_STRUCT_MEM_READER_PROMPT.replace(
44
+ "${conversation}", "\n".join(scene_data_info)
46
45
  )
46
+ if self.config.remove_prompt_example:
47
+ prompt = prompt.replace(SIMPLE_STRUCT_MEM_READER_EXAMPLE, "")
47
48
 
48
49
  messages = [{"role": "user", "content": prompt}]
49
50
 
@@ -228,7 +229,11 @@ class SimpleStructMemReader(BaseMemReader, ABC):
228
229
 
229
230
  def parse_json_result(self, response_text):
230
231
  try:
231
- response_text = response_text.replace("```", "").replace("json", "")
232
+ json_start = response_text.find("{")
233
+ response_text = response_text[json_start:]
234
+ response_text = response_text.replace("```", "").strip()
235
+ if response_text[-1] != "}":
236
+ response_text += "}"
232
237
  response_json = json.loads(response_text)
233
238
  return response_json
234
239
  except json.JSONDecodeError as e:
@@ -476,3 +476,13 @@ class UserManager:
476
476
  return False
477
477
  finally:
478
478
  session.close()
479
+
480
+ def close(self) -> None:
481
+ """Close the database engine and dispose of all connections.
482
+
483
+ This method should be called when the UserManager is no longer needed
484
+ to ensure proper cleanup of database connections.
485
+ """
486
+ if hasattr(self, "engine"):
487
+ self.engine.dispose()
488
+ logger.info("UserManager database connections closed")
@@ -27,7 +27,9 @@ class TextualMemoryMetadata(BaseModel):
27
27
  default="activated",
28
28
  description="The status of the memory, e.g., 'activated', 'archived', 'deleted'.",
29
29
  )
30
- type: Literal["procedure", "fact", "event", "opinion", "topic"] | None = Field(default=None)
30
+ type: Literal["procedure", "fact", "event", "opinion", "topic", "reasoning"] | None = Field(
31
+ default=None
32
+ )
31
33
  memory_time: str | None = Field(
32
34
  default=None,
33
35
  description='The time the memory occurred or refers to. Must be in standard `YYYY-MM-DD` format. Relative expressions such as "yesterday" or "tomorrow" are not allowed.',
@@ -15,6 +15,9 @@ from memos.log import get_logger
15
15
  from memos.memories.textual.base import BaseTextMemory
16
16
  from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
17
17
  from memos.memories.textual.tree_text_memory.organize.manager import MemoryManager
18
+ from memos.memories.textual.tree_text_memory.retrieve.internet_retriever_factory import (
19
+ InternetRetrieverFactory,
20
+ )
18
21
  from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher
19
22
  from memos.types import MessageList
20
23
 
@@ -32,7 +35,23 @@ class TreeTextMemory(BaseTextMemory):
32
35
  self.dispatcher_llm: OpenAILLM | OllamaLLM = LLMFactory.from_config(config.dispatcher_llm)
33
36
  self.embedder: OllamaEmbedder = EmbedderFactory.from_config(config.embedder)
34
37
  self.graph_store: Neo4jGraphDB = GraphStoreFactory.from_config(config.graph_db)
35
- self.memory_manager: MemoryManager = MemoryManager(self.graph_store, self.embedder)
38
+ self.is_reorganize = config.reorganize
39
+
40
+ self.memory_manager: MemoryManager = MemoryManager(
41
+ self.graph_store, self.embedder, self.extractor_llm, is_reorganize=self.is_reorganize
42
+ )
43
+
44
+ # Create internet retriever if configured
45
+ self.internet_retriever = None
46
+ if config.internet_retriever is not None:
47
+ self.internet_retriever = InternetRetrieverFactory.from_config(
48
+ config.internet_retriever, self.embedder
49
+ )
50
+ logger.info(
51
+ f"Internet retriever initialized with backend: {config.internet_retriever.backend}"
52
+ )
53
+ else:
54
+ logger.info("No internet retriever configured")
36
55
 
37
56
  def add(self, memories: list[TextualMemoryItem | dict[str, Any]]) -> None:
38
57
  """Add memories.
@@ -66,7 +85,13 @@ class TreeTextMemory(BaseTextMemory):
66
85
  return self.memory_manager.get_current_memory_size()
67
86
 
68
87
  def search(
69
- self, query: str, top_k: int, info=None, mode: str = "fast", memory_type: str = "All"
88
+ self,
89
+ query: str,
90
+ top_k: int,
91
+ info=None,
92
+ mode: str = "fast",
93
+ memory_type: str = "All",
94
+ manual_close_internet: bool = False,
70
95
  ) -> list[TextualMemoryItem]:
71
96
  """Search for memories based on a query.
72
97
  User query -> TaskGoalParser -> MemoryPathResolver ->
@@ -80,10 +105,21 @@ class TreeTextMemory(BaseTextMemory):
80
105
  - 'fine': Uses a more detailed search process, invoking large models for higher precision, but slower performance.
81
106
  memory_type (str): Type restriction for search.
82
107
  ['All', 'WorkingMemory', 'LongTermMemory', 'UserMemory']
108
+ manual_close_internet (bool): If True, the internet retriever will be closed by this search, it high priority than config.
83
109
  Returns:
84
110
  list[TextualMemoryItem]: List of matching memories.
85
111
  """
86
- searcher = Searcher(self.dispatcher_llm, self.graph_store, self.embedder)
112
+ if (self.internet_retriever is not None) and manual_close_internet:
113
+ logger.warning(
114
+ "Internet retriever is init by config , but this search set manual_close_internet is True and will close it"
115
+ )
116
+ self.internet_retriever = None
117
+ searcher = Searcher(
118
+ self.dispatcher_llm,
119
+ self.graph_store,
120
+ self.embedder,
121
+ internet_retriever=self.internet_retriever,
122
+ )
87
123
  return searcher.search(query, top_k, info, mode, memory_type)
88
124
 
89
125
  def get_relevant_subgraph(