MemoryOS 0.1.13__py3-none-any.whl → 0.2.1__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 (84) hide show
  1. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/METADATA +78 -49
  2. memoryos-0.2.1.dist-info/RECORD +152 -0
  3. memoryos-0.2.1.dist-info/entry_points.txt +3 -0
  4. memos/__init__.py +1 -1
  5. memos/api/config.py +471 -0
  6. memos/api/exceptions.py +28 -0
  7. memos/api/mcp_serve.py +502 -0
  8. memos/api/product_api.py +35 -0
  9. memos/api/product_models.py +159 -0
  10. memos/api/routers/__init__.py +1 -0
  11. memos/api/routers/product_router.py +358 -0
  12. memos/chunkers/sentence_chunker.py +8 -2
  13. memos/cli.py +113 -0
  14. memos/configs/embedder.py +27 -0
  15. memos/configs/graph_db.py +83 -2
  16. memos/configs/llm.py +48 -0
  17. memos/configs/mem_cube.py +1 -1
  18. memos/configs/mem_reader.py +4 -0
  19. memos/configs/mem_scheduler.py +91 -5
  20. memos/configs/memory.py +10 -4
  21. memos/dependency.py +52 -0
  22. memos/embedders/ark.py +92 -0
  23. memos/embedders/factory.py +4 -0
  24. memos/embedders/sentence_transformer.py +8 -2
  25. memos/embedders/universal_api.py +32 -0
  26. memos/graph_dbs/base.py +2 -2
  27. memos/graph_dbs/factory.py +2 -0
  28. memos/graph_dbs/item.py +46 -0
  29. memos/graph_dbs/neo4j.py +377 -101
  30. memos/graph_dbs/neo4j_community.py +300 -0
  31. memos/llms/base.py +9 -0
  32. memos/llms/deepseek.py +54 -0
  33. memos/llms/factory.py +10 -1
  34. memos/llms/hf.py +170 -13
  35. memos/llms/hf_singleton.py +114 -0
  36. memos/llms/ollama.py +4 -0
  37. memos/llms/openai.py +68 -1
  38. memos/llms/qwen.py +63 -0
  39. memos/llms/vllm.py +153 -0
  40. memos/mem_cube/general.py +77 -16
  41. memos/mem_cube/utils.py +102 -0
  42. memos/mem_os/core.py +131 -41
  43. memos/mem_os/main.py +93 -11
  44. memos/mem_os/product.py +1098 -35
  45. memos/mem_os/utils/default_config.py +352 -0
  46. memos/mem_os/utils/format_utils.py +1154 -0
  47. memos/mem_reader/simple_struct.py +13 -8
  48. memos/mem_scheduler/base_scheduler.py +467 -36
  49. memos/mem_scheduler/general_scheduler.py +125 -244
  50. memos/mem_scheduler/modules/base.py +9 -0
  51. memos/mem_scheduler/modules/dispatcher.py +68 -2
  52. memos/mem_scheduler/modules/misc.py +39 -0
  53. memos/mem_scheduler/modules/monitor.py +228 -49
  54. memos/mem_scheduler/modules/rabbitmq_service.py +317 -0
  55. memos/mem_scheduler/modules/redis_service.py +32 -22
  56. memos/mem_scheduler/modules/retriever.py +250 -23
  57. memos/mem_scheduler/modules/schemas.py +189 -7
  58. memos/mem_scheduler/mos_for_test_scheduler.py +143 -0
  59. memos/mem_scheduler/utils.py +51 -2
  60. memos/mem_user/persistent_user_manager.py +260 -0
  61. memos/memories/activation/item.py +25 -0
  62. memos/memories/activation/kv.py +10 -3
  63. memos/memories/activation/vllmkv.py +219 -0
  64. memos/memories/factory.py +2 -0
  65. memos/memories/textual/general.py +7 -5
  66. memos/memories/textual/item.py +3 -1
  67. memos/memories/textual/tree.py +14 -6
  68. memos/memories/textual/tree_text_memory/organize/conflict.py +198 -0
  69. memos/memories/textual/tree_text_memory/organize/manager.py +72 -23
  70. memos/memories/textual/tree_text_memory/organize/redundancy.py +193 -0
  71. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +233 -0
  72. memos/memories/textual/tree_text_memory/organize/reorganizer.py +606 -0
  73. memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
  74. memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
  75. memos/memories/textual/tree_text_memory/retrieve/searcher.py +6 -5
  76. memos/parsers/markitdown.py +8 -2
  77. memos/templates/mem_reader_prompts.py +105 -36
  78. memos/templates/mem_scheduler_prompts.py +96 -47
  79. memos/templates/tree_reorganize_prompts.py +223 -0
  80. memos/vec_dbs/base.py +12 -0
  81. memos/vec_dbs/qdrant.py +46 -20
  82. memoryos-0.1.13.dist-info/RECORD +0 -122
  83. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/LICENSE +0 -0
  84. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/WHEEL +0 -0
@@ -2,11 +2,9 @@ import asyncio
2
2
  import threading
3
3
 
4
4
  from collections.abc import Callable
5
+ from typing import Any
5
6
 
6
- import redis
7
-
8
- from redis import Redis
9
-
7
+ from memos.dependency import require_python_package
10
8
  from memos.log import get_logger
11
9
  from memos.mem_scheduler.modules.base import BaseSchedulerModule
12
10
 
@@ -15,6 +13,11 @@ logger = get_logger(__name__)
15
13
 
16
14
 
17
15
  class RedisSchedulerModule(BaseSchedulerModule):
16
+ @require_python_package(
17
+ import_name="redis",
18
+ install_command="pip install redis",
19
+ install_link="https://redis.readthedocs.io/en/stable/",
20
+ )
18
21
  def __init__(self):
19
22
  """
20
23
  intent_detector: Object used for intent recognition (such as the above IntentDetector)
@@ -35,23 +38,25 @@ class RedisSchedulerModule(BaseSchedulerModule):
35
38
  self._redis_listener_loop: asyncio.AbstractEventLoop | None = None
36
39
 
37
40
  @property
38
- def redis(self) -> Redis:
41
+ def redis(self) -> Any:
39
42
  return self._redis_conn
40
43
 
41
44
  @redis.setter
42
- def redis(self, value: Redis) -> None:
45
+ def redis(self, value: Any) -> None:
43
46
  self._redis_conn = value
44
47
 
45
48
  def initialize_redis(
46
49
  self, redis_host: str = "localhost", redis_port: int = 6379, redis_db: int = 0
47
50
  ):
51
+ import redis
52
+
48
53
  self.redis_host = redis_host
49
54
  self.redis_port = redis_port
50
55
  self.redis_db = redis_db
51
56
 
52
57
  try:
53
58
  logger.debug(f"Connecting to Redis at {redis_host}:{redis_port}/{redis_db}")
54
- self._redis_conn = Redis(
59
+ self._redis_conn = redis.Redis(
55
60
  host=self.redis_host, port=self.redis_port, db=self.redis_db, decode_responses=True
56
61
  )
57
62
  # test conn
@@ -63,21 +68,21 @@ class RedisSchedulerModule(BaseSchedulerModule):
63
68
  self._redis_conn.xtrim("user:queries:stream", self.query_list_capacity)
64
69
  return self._redis_conn
65
70
 
66
- async def add_message_stream(self, message: dict):
71
+ async def redis_add_message_stream(self, message: dict):
67
72
  logger.debug(f"add_message_stream: {message}")
68
73
  return self._redis_conn.xadd("user:queries:stream", message)
69
74
 
70
- async def consume_message_stream(self, message: dict):
75
+ async def redis_consume_message_stream(self, message: dict):
71
76
  logger.debug(f"consume_message_stream: {message}")
72
77
 
73
- def _run_listener_async(self, handler: Callable):
78
+ def _redis_run_listener_async(self, handler: Callable):
74
79
  """Run the async listener in a separate thread"""
75
80
  self._redis_listener_loop = asyncio.new_event_loop()
76
81
  asyncio.set_event_loop(self._redis_listener_loop)
77
82
 
78
83
  async def listener_wrapper():
79
84
  try:
80
- await self._listen_query_stream(handler)
85
+ await self.__redis_listen_query_stream(handler)
81
86
  except Exception as e:
82
87
  logger.error(f"Listener thread error: {e}")
83
88
  finally:
@@ -85,8 +90,12 @@ class RedisSchedulerModule(BaseSchedulerModule):
85
90
 
86
91
  self._redis_listener_loop.run_until_complete(listener_wrapper())
87
92
 
88
- async def _listen_query_stream(self, handler=None, last_id: str = "$", block_time: int = 2000):
93
+ async def __redis_listen_query_stream(
94
+ self, handler=None, last_id: str = "$", block_time: int = 2000
95
+ ):
89
96
  """Internal async stream listener"""
97
+ import redis
98
+
90
99
  self._redis_listener_running = True
91
100
  while self._redis_listener_running:
92
101
  try:
@@ -99,6 +108,7 @@ class RedisSchedulerModule(BaseSchedulerModule):
99
108
  for _, stream_messages in messages:
100
109
  for message_id, message_data in stream_messages:
101
110
  try:
111
+ print(f"deal with message_data {message_data}")
102
112
  await handler(message_data)
103
113
  last_id = message_id
104
114
  except Exception as e:
@@ -112,17 +122,17 @@ class RedisSchedulerModule(BaseSchedulerModule):
112
122
  logger.error(f"Unexpected error: {e}")
113
123
  await asyncio.sleep(1)
114
124
 
115
- def start_listening(self, handler: Callable | None = None):
125
+ def redis_start_listening(self, handler: Callable | None = None):
116
126
  """Start the Redis stream listener in a background thread"""
117
127
  if self._redis_listener_thread and self._redis_listener_thread.is_alive():
118
128
  logger.warning("Listener is already running")
119
129
  return
120
130
 
121
131
  if handler is None:
122
- handler = self.consume_message_stream
132
+ handler = self.redis_consume_message_stream
123
133
 
124
134
  self._redis_listener_thread = threading.Thread(
125
- target=self._run_listener_async,
135
+ target=self._redis_run_listener_async,
126
136
  args=(handler,),
127
137
  daemon=True,
128
138
  name="RedisListenerThread",
@@ -130,13 +140,7 @@ class RedisSchedulerModule(BaseSchedulerModule):
130
140
  self._redis_listener_thread.start()
131
141
  logger.info("Started Redis stream listener thread")
132
142
 
133
- def close(self):
134
- """Close Redis connection"""
135
- if self._redis_conn is not None:
136
- self._redis_conn.close()
137
- self._redis_conn = None
138
-
139
- def stop_listening(self):
143
+ def redis_stop_listening(self):
140
144
  """Stop the listener thread gracefully"""
141
145
  self._redis_listener_running = False
142
146
  if self._redis_listener_thread and self._redis_listener_thread.is_alive():
@@ -144,3 +148,9 @@ class RedisSchedulerModule(BaseSchedulerModule):
144
148
  if self._redis_listener_thread.is_alive():
145
149
  logger.warning("Listener thread did not stop gracefully")
146
150
  logger.info("Redis stream listener stopped")
151
+
152
+ def redis_close(self):
153
+ """Close Redis connection"""
154
+ if self._redis_conn is not None:
155
+ self._redis_conn.close()
156
+ self._redis_conn = None
@@ -1,41 +1,268 @@
1
+ import logging
2
+
3
+ from memos.configs.mem_scheduler import BaseSchedulerConfig
4
+ from memos.dependency import require_python_package
5
+ from memos.llms.base import BaseLLM
1
6
  from memos.log import get_logger
7
+ from memos.mem_cube.general import GeneralMemCube
2
8
  from memos.mem_scheduler.modules.base import BaseSchedulerModule
9
+ from memos.mem_scheduler.modules.schemas import (
10
+ TreeTextMemory_SEARCH_METHOD,
11
+ )
12
+ from memos.mem_scheduler.utils import (
13
+ extract_json_dict,
14
+ is_all_chinese,
15
+ is_all_english,
16
+ transform_name_to_key,
17
+ )
18
+ from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
3
19
 
4
20
 
5
21
  logger = get_logger(__name__)
6
22
 
7
23
 
8
24
  class SchedulerRetriever(BaseSchedulerModule):
9
- def __init__(self, chat_llm, context_window_size=5):
25
+ def __init__(self, process_llm: BaseLLM, config: BaseSchedulerConfig):
26
+ super().__init__()
27
+
28
+ self.config: BaseSchedulerConfig = config
29
+ self.process_llm = process_llm
30
+
31
+ # hyper-parameters
32
+ self.filter_similarity_threshold = 0.75
33
+ self.filter_min_length_threshold = 6
34
+
35
+ # log function callbacks
36
+ self.log_working_memory_replacement = None
37
+
38
+ def search(
39
+ self, query: str, mem_cube: GeneralMemCube, top_k: int, method=TreeTextMemory_SEARCH_METHOD
40
+ ):
41
+ """Search in text memory with the given query.
42
+
43
+ Args:
44
+ query: The search query string
45
+ top_k: Number of top results to return
46
+ method: Search method to use
47
+
48
+ Returns:
49
+ Search results or None if not implemented
50
+ """
51
+ text_mem_base = mem_cube.text_mem
52
+ try:
53
+ if method == TreeTextMemory_SEARCH_METHOD:
54
+ assert isinstance(text_mem_base, TreeTextMemory)
55
+ results_long_term = text_mem_base.search(
56
+ query=query, top_k=top_k, memory_type="LongTermMemory"
57
+ )
58
+ results_user = text_mem_base.search(
59
+ query=query, top_k=top_k, memory_type="UserMemory"
60
+ )
61
+ results = results_long_term + results_user
62
+ else:
63
+ raise NotImplementedError(str(type(text_mem_base)))
64
+ except Exception as e:
65
+ logger.error(f"Fail to search. The exeption is {e}.", exc_info=True)
66
+ results = []
67
+ return results
68
+
69
+ @require_python_package(
70
+ import_name="sklearn",
71
+ install_command="pip install scikit-learn",
72
+ install_link="https://scikit-learn.org/stable/install.html",
73
+ )
74
+ def filter_similar_memories(
75
+ self, text_memories: list[str], similarity_threshold: float = 0.75
76
+ ) -> list[str]:
10
77
  """
11
- monitor: Object used to acquire monitoring information
12
- mem_cube: Object/interface for querying the underlying database
13
- context_window_size: Size of the context window for conversation history
78
+ Filters out low-quality or duplicate memories based on text similarity.
79
+
80
+ Args:
81
+ text_memories: List of text memories to filter
82
+ similarity_threshold: Threshold for considering memories duplicates (0.0-1.0)
83
+ Higher values mean stricter filtering
84
+
85
+ Returns:
86
+ List of filtered memories with duplicates removed
14
87
  """
15
- super().__init__()
88
+ from sklearn.feature_extraction.text import TfidfVectorizer
89
+ from sklearn.metrics.pairwise import cosine_similarity
90
+
91
+ if not text_memories:
92
+ logging.warning("Received empty memories list - nothing to filter")
93
+ return []
94
+
95
+ for idx in range(len(text_memories)):
96
+ if not isinstance(text_memories[idx], str):
97
+ logger.error(
98
+ f"{text_memories[idx]} in memories is not a string,"
99
+ f" and now has been transformed to be a string."
100
+ )
101
+ text_memories[idx] = str(text_memories[idx])
102
+
103
+ try:
104
+ # Step 1: Vectorize texts using TF-IDF
105
+ vectorizer = TfidfVectorizer()
106
+ tfidf_matrix = vectorizer.fit_transform(text_memories)
107
+
108
+ # Step 2: Calculate pairwise similarity matrix
109
+ similarity_matrix = cosine_similarity(tfidf_matrix)
110
+
111
+ # Step 3: Identify duplicates
112
+ to_keep = []
113
+ removal_reasons = {}
16
114
 
17
- self.monitors = {}
18
- self.context_window_size = context_window_size
115
+ for current_idx in range(len(text_memories)):
116
+ is_duplicate = False
19
117
 
20
- self._chat_llm = chat_llm
21
- self._current_mem_cube = None
118
+ # Compare with already kept memories
119
+ for kept_idx in to_keep:
120
+ similarity_score = similarity_matrix[current_idx, kept_idx]
22
121
 
23
- @property
24
- def memory_texts(self) -> list[str]:
25
- """The memory cube associated with this MemChat."""
26
- return self._memory_text_list
122
+ if similarity_score > similarity_threshold:
123
+ is_duplicate = True
124
+ # Generate removal reason with sample text
125
+ removal_reasons[current_idx] = (
126
+ f"Memory too similar (score: {similarity_score:.2f}) to kept memory #{kept_idx}. "
127
+ f"Kept: '{text_memories[kept_idx][:100]}...' | "
128
+ f"Removed: '{text_memories[current_idx][:100]}...'"
129
+ )
130
+ logger.info(removal_reasons)
131
+ break
27
132
 
28
- @memory_texts.setter
29
- def memory_texts(self, value: list[str]) -> None:
30
- """The memory cube associated with this MemChat."""
31
- self._memory_text_list = value
133
+ if not is_duplicate:
134
+ to_keep.append(current_idx)
32
135
 
33
- def fetch_context(self):
136
+ # Return filtered memories
137
+ return [text_memories[i] for i in sorted(to_keep)]
138
+
139
+ except Exception as e:
140
+ logging.error(f"Error filtering memories: {e!s}")
141
+ return text_memories # Return original list if error occurs
142
+
143
+ def filter_too_short_memories(
144
+ self, text_memories: list[str], min_length_threshold: int = 20
145
+ ) -> list[str]:
34
146
  """
35
- Extract the context window from the current conversation
36
- conversation_history: a list (in chronological order)
147
+ Filters out text memories that fall below the minimum length requirement.
148
+ Handles both English (word count) and Chinese (character count) differently.
149
+
150
+ Args:
151
+ text_memories: List of text memories to be filtered
152
+ min_length_threshold: Minimum length required to keep a memory.
153
+ For English: word count, for Chinese: character count.
154
+
155
+ Returns:
156
+ List of filtered memories meeting the length requirement
37
157
  """
38
- return self._memory_text_list[-self.context_window_size :]
158
+ if not text_memories:
159
+ logging.debug("Empty memories list received in short memory filter")
160
+ return []
161
+
162
+ filtered_memories = []
163
+ removed_count = 0
164
+
165
+ for memory in text_memories:
166
+ stripped_memory = memory.strip()
167
+ if not stripped_memory: # Skip empty/whitespace memories
168
+ removed_count += 1
169
+ continue
170
+
171
+ # Determine measurement method based on language
172
+ if is_all_english(stripped_memory):
173
+ length = len(stripped_memory.split()) # Word count for English
174
+ elif is_all_chinese(stripped_memory):
175
+ length = len(stripped_memory) # Character count for Chinese
176
+ else:
177
+ logger.debug(
178
+ f"Mixed-language memory, using character count: {stripped_memory[:50]}..."
179
+ )
180
+ length = len(stripped_memory) # Default to character count
181
+
182
+ if length >= min_length_threshold:
183
+ filtered_memories.append(memory)
184
+ else:
185
+ removed_count += 1
186
+
187
+ if removed_count > 0:
188
+ logger.info(
189
+ f"Filtered out {removed_count} short memories "
190
+ f"(below {min_length_threshold} units). "
191
+ f"Total remaining: {len(filtered_memories)}"
192
+ )
193
+
194
+ return filtered_memories
195
+
196
+ def replace_working_memory(
197
+ self,
198
+ queries: list[str],
199
+ user_id: str,
200
+ mem_cube_id: str,
201
+ mem_cube: GeneralMemCube,
202
+ original_memory: list[TextualMemoryItem],
203
+ new_memory: list[TextualMemoryItem],
204
+ top_k: int = 10,
205
+ ) -> None | list[TextualMemoryItem]:
206
+ """Replace working memory with new memories after reranking."""
207
+ memories_with_new_order = None
208
+ text_mem_base = mem_cube.text_mem
209
+ if isinstance(text_mem_base, TreeTextMemory):
210
+ text_mem_base: TreeTextMemory = text_mem_base
211
+ combined_memory = original_memory + new_memory
212
+ memory_map = {
213
+ transform_name_to_key(name=mem_obj.memory): mem_obj for mem_obj in combined_memory
214
+ }
215
+ combined_text_memory = [transform_name_to_key(name=m.memory) for m in combined_memory]
216
+
217
+ # apply filters
218
+ filtered_combined_text_memory = self.filter_similar_memories(
219
+ text_memories=combined_text_memory,
220
+ similarity_threshold=self.filter_similarity_threshold,
221
+ )
222
+
223
+ filtered_combined_text_memory = self.filter_too_short_memories(
224
+ text_memories=filtered_combined_text_memory,
225
+ min_length_threshold=self.filter_min_length_threshold,
226
+ )
227
+
228
+ unique_memory = list(dict.fromkeys(filtered_combined_text_memory))
229
+
230
+ try:
231
+ prompt = self.build_prompt(
232
+ "memory_reranking",
233
+ queries=queries,
234
+ current_order=unique_memory,
235
+ staging_buffer=[],
236
+ )
237
+ response = self.process_llm.generate([{"role": "user", "content": prompt}])
238
+ response = extract_json_dict(response)
239
+ text_memories_with_new_order = response.get("new_order", [])[:top_k]
240
+ except Exception as e:
241
+ logger.error(f"Fail to rerank with LLM, Exeption: {e}.", exc_info=True)
242
+ text_memories_with_new_order = unique_memory[:top_k]
243
+
244
+ memories_with_new_order = []
245
+ for text in text_memories_with_new_order:
246
+ normalized_text = transform_name_to_key(name=text)
247
+ if text in memory_map:
248
+ memories_with_new_order.append(memory_map[normalized_text])
249
+ else:
250
+ logger.warning(
251
+ f"Memory text not found in memory map. text: {text}; keys of memory_map: {memory_map.keys()}"
252
+ )
253
+
254
+ text_mem_base.replace_working_memory(memories_with_new_order)
255
+ logger.info(
256
+ f"The working memory has been replaced with {len(memories_with_new_order)} new memories."
257
+ )
258
+ self.log_working_memory_replacement(
259
+ original_memory=original_memory,
260
+ new_memory=memories_with_new_order,
261
+ user_id=user_id,
262
+ mem_cube_id=mem_cube_id,
263
+ mem_cube=mem_cube,
264
+ )
265
+ else:
266
+ logger.error("memory_base is not supported")
39
267
 
40
- def retrieve(self, query: str, memory_texts: list[str], top_k: int = 5) -> list[str]:
41
- return None
268
+ return memories_with_new_order