MemoryOS 1.0.0__py3-none-any.whl → 1.1.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 (94) hide show
  1. {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/METADATA +8 -2
  2. {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/RECORD +92 -69
  3. {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/WHEEL +1 -1
  4. memos/__init__.py +1 -1
  5. memos/api/client.py +109 -0
  6. memos/api/config.py +35 -8
  7. memos/api/context/dependencies.py +15 -66
  8. memos/api/middleware/request_context.py +63 -0
  9. memos/api/product_api.py +5 -2
  10. memos/api/product_models.py +107 -16
  11. memos/api/routers/product_router.py +62 -19
  12. memos/api/start_api.py +13 -0
  13. memos/configs/graph_db.py +4 -0
  14. memos/configs/mem_scheduler.py +38 -3
  15. memos/configs/memory.py +13 -0
  16. memos/configs/reranker.py +18 -0
  17. memos/context/context.py +255 -0
  18. memos/embedders/factory.py +2 -0
  19. memos/graph_dbs/base.py +4 -2
  20. memos/graph_dbs/nebular.py +368 -223
  21. memos/graph_dbs/neo4j.py +49 -13
  22. memos/graph_dbs/neo4j_community.py +13 -3
  23. memos/llms/factory.py +2 -0
  24. memos/llms/openai.py +74 -2
  25. memos/llms/vllm.py +2 -0
  26. memos/log.py +128 -4
  27. memos/mem_cube/general.py +3 -1
  28. memos/mem_os/core.py +89 -23
  29. memos/mem_os/main.py +3 -6
  30. memos/mem_os/product.py +418 -154
  31. memos/mem_os/utils/reference_utils.py +20 -0
  32. memos/mem_reader/factory.py +2 -0
  33. memos/mem_reader/simple_struct.py +204 -82
  34. memos/mem_scheduler/analyzer/__init__.py +0 -0
  35. memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +569 -0
  36. memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
  37. memos/mem_scheduler/base_scheduler.py +126 -56
  38. memos/mem_scheduler/general_modules/dispatcher.py +2 -2
  39. memos/mem_scheduler/general_modules/misc.py +99 -1
  40. memos/mem_scheduler/general_modules/scheduler_logger.py +17 -11
  41. memos/mem_scheduler/general_scheduler.py +40 -88
  42. memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
  43. memos/mem_scheduler/memory_manage_modules/memory_filter.py +308 -0
  44. memos/mem_scheduler/{general_modules → memory_manage_modules}/retriever.py +34 -7
  45. memos/mem_scheduler/monitors/dispatcher_monitor.py +9 -8
  46. memos/mem_scheduler/monitors/general_monitor.py +119 -39
  47. memos/mem_scheduler/optimized_scheduler.py +124 -0
  48. memos/mem_scheduler/orm_modules/__init__.py +0 -0
  49. memos/mem_scheduler/orm_modules/base_model.py +635 -0
  50. memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
  51. memos/mem_scheduler/scheduler_factory.py +2 -0
  52. memos/mem_scheduler/schemas/monitor_schemas.py +96 -29
  53. memos/mem_scheduler/utils/config_utils.py +100 -0
  54. memos/mem_scheduler/utils/db_utils.py +33 -0
  55. memos/mem_scheduler/utils/filter_utils.py +1 -1
  56. memos/mem_scheduler/webservice_modules/__init__.py +0 -0
  57. memos/mem_user/mysql_user_manager.py +4 -2
  58. memos/memories/activation/kv.py +2 -1
  59. memos/memories/textual/item.py +96 -17
  60. memos/memories/textual/naive.py +1 -1
  61. memos/memories/textual/tree.py +57 -3
  62. memos/memories/textual/tree_text_memory/organize/handler.py +4 -2
  63. memos/memories/textual/tree_text_memory/organize/manager.py +28 -14
  64. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +1 -2
  65. memos/memories/textual/tree_text_memory/organize/reorganizer.py +75 -23
  66. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +10 -6
  67. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -2
  68. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
  69. memos/memories/textual/tree_text_memory/retrieve/recall.py +119 -21
  70. memos/memories/textual/tree_text_memory/retrieve/searcher.py +172 -44
  71. memos/memories/textual/tree_text_memory/retrieve/utils.py +6 -4
  72. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +5 -4
  73. memos/memos_tools/notification_utils.py +46 -0
  74. memos/memos_tools/singleton.py +174 -0
  75. memos/memos_tools/thread_safe_dict.py +22 -0
  76. memos/memos_tools/thread_safe_dict_segment.py +382 -0
  77. memos/parsers/factory.py +2 -0
  78. memos/reranker/__init__.py +4 -0
  79. memos/reranker/base.py +24 -0
  80. memos/reranker/concat.py +59 -0
  81. memos/reranker/cosine_local.py +96 -0
  82. memos/reranker/factory.py +48 -0
  83. memos/reranker/http_bge.py +312 -0
  84. memos/reranker/noop.py +16 -0
  85. memos/templates/mem_reader_prompts.py +289 -40
  86. memos/templates/mem_scheduler_prompts.py +242 -0
  87. memos/templates/mos_prompts.py +133 -60
  88. memos/types.py +4 -1
  89. memos/api/context/context.py +0 -147
  90. memos/mem_scheduler/mos_for_test_scheduler.py +0 -146
  91. {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/entry_points.txt +0 -0
  92. {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info/licenses}/LICENSE +0 -0
  93. /memos/mem_scheduler/{general_modules → webservice_modules}/rabbitmq_service.py +0 -0
  94. /memos/mem_scheduler/{general_modules → webservice_modules}/redis_service.py +0 -0
memos/graph_dbs/neo4j.py CHANGED
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import time
2
3
 
3
4
  from datetime import datetime
@@ -174,6 +175,12 @@ class Neo4jGraphDB(BaseGraphDB):
174
175
  n.updated_at = datetime($updated_at),
175
176
  n += $metadata
176
177
  """
178
+
179
+ # serialization
180
+ if metadata["sources"]:
181
+ for idx in range(len(metadata["sources"])):
182
+ metadata["sources"][idx] = json.dumps(metadata["sources"][idx])
183
+
177
184
  with self.driver.session(database=self.db_name) as session:
178
185
  session.run(
179
186
  query,
@@ -323,12 +330,11 @@ class Neo4jGraphDB(BaseGraphDB):
323
330
  return result.single() is not None
324
331
 
325
332
  # Graph Query & Reasoning
326
- def get_node(self, id: str, include_embedding: bool = True) -> dict[str, Any] | None:
333
+ def get_node(self, id: str, **kwargs) -> dict[str, Any] | None:
327
334
  """
328
335
  Retrieve the metadata and memory of a node.
329
336
  Args:
330
337
  id: Node identifier.
331
- include_embedding (bool): Whether to include the large embedding field.
332
338
  Returns:
333
339
  Dictionary of node fields, or None if not found.
334
340
  """
@@ -345,12 +351,11 @@ class Neo4jGraphDB(BaseGraphDB):
345
351
  record = session.run(query, params).single()
346
352
  return self._parse_node(dict(record["n"])) if record else None
347
353
 
348
- def get_nodes(self, ids: list[str], include_embedding: bool = True) -> list[dict[str, Any]]:
354
+ def get_nodes(self, ids: list[str], **kwargs) -> list[dict[str, Any]]:
349
355
  """
350
356
  Retrieve the metadata and memory of a list of nodes.
351
357
  Args:
352
358
  ids: List of Node identifier.
353
- include_embedding (bool): Whether to include the large embedding field.
354
359
  Returns:
355
360
  list[dict]: Parsed node records containing 'id', 'memory', and 'metadata'.
356
361
 
@@ -367,7 +372,10 @@ class Neo4jGraphDB(BaseGraphDB):
367
372
 
368
373
  if not self.config.use_multi_db and self.config.user_name:
369
374
  where_user = " AND n.user_name = $user_name"
370
- params["user_name"] = self.config.user_name
375
+ if kwargs.get("cube_name"):
376
+ params["user_name"] = kwargs["cube_name"]
377
+ else:
378
+ params["user_name"] = self.config.user_name
371
379
 
372
380
  query = f"MATCH (n:Memory) WHERE n.id IN $ids{where_user} RETURN n"
373
381
 
@@ -605,6 +613,8 @@ class Neo4jGraphDB(BaseGraphDB):
605
613
  scope: str | None = None,
606
614
  status: str | None = None,
607
615
  threshold: float | None = None,
616
+ search_filter: dict | None = None,
617
+ **kwargs,
608
618
  ) -> list[dict]:
609
619
  """
610
620
  Retrieve node IDs based on vector similarity.
@@ -616,6 +626,8 @@ class Neo4jGraphDB(BaseGraphDB):
616
626
  status (str, optional): Node status filter (e.g., 'active', 'archived').
617
627
  If provided, restricts results to nodes with matching status.
618
628
  threshold (float, optional): Minimum similarity score threshold (0 ~ 1).
629
+ search_filter (dict, optional): Additional metadata filters for search results.
630
+ Keys should match node properties, values are the expected values.
619
631
 
620
632
  Returns:
621
633
  list[dict]: A list of dicts with 'id' and 'score', ordered by similarity.
@@ -625,6 +637,7 @@ class Neo4jGraphDB(BaseGraphDB):
625
637
  - If scope is provided, it restricts results to nodes with matching memory_type.
626
638
  - If 'status' is provided, only nodes with the matching status will be returned.
627
639
  - If threshold is provided, only results with score >= threshold will be returned.
640
+ - If search_filter is provided, additional WHERE clauses will be added for metadata filtering.
628
641
  - Typical use case: restrict to 'status = activated' to avoid
629
642
  matching archived or merged nodes.
630
643
  """
@@ -637,6 +650,12 @@ class Neo4jGraphDB(BaseGraphDB):
637
650
  if not self.config.use_multi_db and self.config.user_name:
638
651
  where_clauses.append("node.user_name = $user_name")
639
652
 
653
+ # Add search_filter conditions
654
+ if search_filter:
655
+ for key, _ in search_filter.items():
656
+ param_name = f"filter_{key}"
657
+ where_clauses.append(f"node.{key} = ${param_name}")
658
+
640
659
  where_clause = ""
641
660
  if where_clauses:
642
661
  where_clause = "WHERE " + " AND ".join(where_clauses)
@@ -648,13 +667,23 @@ class Neo4jGraphDB(BaseGraphDB):
648
667
  RETURN node.id AS id, score
649
668
  """
650
669
 
651
- parameters = {"embedding": vector, "k": top_k, "scope": scope}
670
+ parameters = {"embedding": vector, "k": top_k}
671
+
652
672
  if scope:
653
673
  parameters["scope"] = scope
654
674
  if status:
655
675
  parameters["status"] = status
656
676
  if not self.config.use_multi_db and self.config.user_name:
657
- parameters["user_name"] = self.config.user_name
677
+ if kwargs.get("cube_name"):
678
+ parameters["user_name"] = kwargs["cube_name"]
679
+ else:
680
+ parameters["user_name"] = self.config.user_name
681
+
682
+ # Add search_filter parameters
683
+ if search_filter:
684
+ for key, value in search_filter.items():
685
+ param_name = f"filter_{key}"
686
+ parameters[param_name] = value
658
687
 
659
688
  with self.driver.session(database=self.db_name) as session:
660
689
  result = session.run(query, parameters)
@@ -833,7 +862,7 @@ class Neo4jGraphDB(BaseGraphDB):
833
862
  logger.error(f"[ERROR] Failed to clear database '{self.db_name}': {e}")
834
863
  raise
835
864
 
836
- def export_graph(self, include_embedding: bool = True) -> dict[str, Any]:
865
+ def export_graph(self, **kwargs) -> dict[str, Any]:
837
866
  """
838
867
  Export all graph nodes and edges in a structured form.
839
868
 
@@ -914,13 +943,12 @@ class Neo4jGraphDB(BaseGraphDB):
914
943
  target_id=edge["target"],
915
944
  )
916
945
 
917
- def get_all_memory_items(self, scope: str, include_embedding: bool = True) -> list[dict]:
946
+ def get_all_memory_items(self, scope: str, **kwargs) -> list[dict]:
918
947
  """
919
948
  Retrieve all memory items of a specific memory_type.
920
949
 
921
950
  Args:
922
951
  scope (str): Must be one of 'WorkingMemory', 'LongTermMemory', or 'UserMemory'.
923
- include_embedding (bool): Whether to include the large embedding field.
924
952
  Returns:
925
953
 
926
954
  Returns:
@@ -946,9 +974,7 @@ class Neo4jGraphDB(BaseGraphDB):
946
974
  results = session.run(query, params)
947
975
  return [self._parse_node(dict(record["n"])) for record in results]
948
976
 
949
- def get_structure_optimization_candidates(
950
- self, scope: str, include_embedding: bool = True
951
- ) -> list[dict]:
977
+ def get_structure_optimization_candidates(self, scope: str, **kwargs) -> list[dict]:
952
978
  """
953
979
  Find nodes that are likely candidates for structure optimization:
954
980
  - Isolated nodes, nodes with empty background, or nodes with exactly one child.
@@ -1109,4 +1135,14 @@ class Neo4jGraphDB(BaseGraphDB):
1109
1135
  node[time_field] = node[time_field].isoformat()
1110
1136
  node.pop("user_name", None)
1111
1137
 
1138
+ # serialization
1139
+ if node["sources"]:
1140
+ for idx in range(len(node["sources"])):
1141
+ if not (
1142
+ isinstance(node["sources"][idx], str)
1143
+ and node["sources"][idx][0] == "{"
1144
+ and node["sources"][idx][0] == "}"
1145
+ ):
1146
+ break
1147
+ node["sources"][idx] = json.loads(node["sources"][idx])
1112
1148
  return {"id": node.pop("id"), "memory": node.pop("memory", ""), "metadata": node}
@@ -129,6 +129,8 @@ class Neo4jCommunityGraphDB(Neo4jGraphDB):
129
129
  scope: str | None = None,
130
130
  status: str | None = None,
131
131
  threshold: float | None = None,
132
+ search_filter: dict | None = None,
133
+ **kwargs,
132
134
  ) -> list[dict]:
133
135
  """
134
136
  Retrieve node IDs based on vector similarity using external vector DB.
@@ -139,6 +141,7 @@ class Neo4jCommunityGraphDB(Neo4jGraphDB):
139
141
  scope (str, optional): Memory type filter (e.g., 'WorkingMemory', 'LongTermMemory').
140
142
  status (str, optional): Node status filter (e.g., 'activated', 'archived').
141
143
  threshold (float, optional): Minimum similarity score threshold (0 ~ 1).
144
+ search_filter (dict, optional): Additional metadata filters to apply.
142
145
 
143
146
  Returns:
144
147
  list[dict]: A list of dicts with 'id' and 'score', ordered by similarity.
@@ -148,6 +151,7 @@ class Neo4jCommunityGraphDB(Neo4jGraphDB):
148
151
  - If 'scope' is provided, it restricts results to nodes with matching memory_type.
149
152
  - If 'status' is provided, it further filters nodes by status.
150
153
  - If 'threshold' is provided, only results with score >= threshold will be returned.
154
+ - If 'search_filter' is provided, it applies additional metadata-based filtering.
151
155
  - The returned IDs can be used to fetch full node data from Neo4j if needed.
152
156
  """
153
157
  # Build VecDB filter
@@ -157,7 +161,14 @@ class Neo4jCommunityGraphDB(Neo4jGraphDB):
157
161
  if status:
158
162
  vec_filter["status"] = status
159
163
  vec_filter["vector_sync"] = "success"
160
- vec_filter["user_name"] = self.config.user_name
164
+ if kwargs.get("cube_name"):
165
+ vec_filter["user_name"] = kwargs["cube_name"]
166
+ else:
167
+ vec_filter["user_name"] = self.config.user_name
168
+
169
+ # Add search_filter conditions
170
+ if search_filter:
171
+ vec_filter.update(search_filter)
161
172
 
162
173
  # Perform vector search
163
174
  results = self.vec_db.search(query_vector=vector, top_k=top_k, filter=vec_filter)
@@ -169,13 +180,12 @@ class Neo4jCommunityGraphDB(Neo4jGraphDB):
169
180
  # Return consistent format
170
181
  return [{"id": r.id, "score": r.score} for r in results]
171
182
 
172
- def get_all_memory_items(self, scope: str) -> list[dict]:
183
+ def get_all_memory_items(self, scope: str, **kwargs) -> list[dict]:
173
184
  """
174
185
  Retrieve all memory items of a specific memory_type.
175
186
 
176
187
  Args:
177
188
  scope (str): Must be one of 'WorkingMemory', 'LongTermMemory', or 'UserMemory'.
178
-
179
189
  Returns:
180
190
  list[dict]: Full list of memory items under this scope.
181
191
  """
memos/llms/factory.py CHANGED
@@ -9,6 +9,7 @@ from memos.llms.ollama import OllamaLLM
9
9
  from memos.llms.openai import AzureLLM, OpenAILLM
10
10
  from memos.llms.qwen import QwenLLM
11
11
  from memos.llms.vllm import VLLMLLM
12
+ from memos.memos_tools.singleton import singleton_factory
12
13
 
13
14
 
14
15
  class LLMFactory(BaseLLM):
@@ -26,6 +27,7 @@ class LLMFactory(BaseLLM):
26
27
  }
27
28
 
28
29
  @classmethod
30
+ @singleton_factory()
29
31
  def from_config(cls, config_factory: LLMConfigFactory) -> BaseLLM:
30
32
  backend = config_factory.backend
31
33
  if backend not in cls.backend_to_class:
memos/llms/openai.py CHANGED
@@ -1,4 +1,8 @@
1
+ import hashlib
2
+ import json
3
+
1
4
  from collections.abc import Generator
5
+ from typing import ClassVar
2
6
 
3
7
  import openai
4
8
 
@@ -13,11 +17,44 @@ logger = get_logger(__name__)
13
17
 
14
18
 
15
19
  class OpenAILLM(BaseLLM):
16
- """OpenAI LLM class."""
20
+ """OpenAI LLM class with singleton pattern."""
21
+
22
+ _instances: ClassVar[dict] = {} # Class variable to store instances
23
+
24
+ def __new__(cls, config: OpenAILLMConfig) -> "OpenAILLM":
25
+ config_hash = cls._get_config_hash(config)
26
+
27
+ if config_hash not in cls._instances:
28
+ logger.info(f"Creating new OpenAI LLM instance for config hash: {config_hash}")
29
+ instance = super().__new__(cls)
30
+ cls._instances[config_hash] = instance
31
+ else:
32
+ logger.info(f"Reusing existing OpenAI LLM instance for config hash: {config_hash}")
33
+
34
+ return cls._instances[config_hash]
17
35
 
18
36
  def __init__(self, config: OpenAILLMConfig):
37
+ # Avoid duplicate initialization
38
+ if hasattr(self, "_initialized"):
39
+ return
40
+
19
41
  self.config = config
20
42
  self.client = openai.Client(api_key=config.api_key, base_url=config.api_base)
43
+ self._initialized = True
44
+ logger.info("OpenAI LLM instance initialized")
45
+
46
+ @classmethod
47
+ def _get_config_hash(cls, config: OpenAILLMConfig) -> str:
48
+ """Generate hash value of configuration"""
49
+ config_dict = config.model_dump()
50
+ config_str = json.dumps(config_dict, sort_keys=True)
51
+ return hashlib.md5(config_str.encode()).hexdigest()
52
+
53
+ @classmethod
54
+ def clear_cache(cls):
55
+ """Clear all cached instances"""
56
+ cls._instances.clear()
57
+ logger.info("OpenAI LLM instance cache cleared")
21
58
 
22
59
  def generate(self, messages: MessageList) -> str:
23
60
  """Generate a response from OpenAI LLM."""
@@ -71,15 +108,50 @@ class OpenAILLM(BaseLLM):
71
108
 
72
109
 
73
110
  class AzureLLM(BaseLLM):
74
- """Azure OpenAI LLM class."""
111
+ """Azure OpenAI LLM class with singleton pattern."""
112
+
113
+ _instances: ClassVar[dict] = {} # Class variable to store instances
114
+
115
+ def __new__(cls, config: AzureLLMConfig):
116
+ # Generate hash value of config as cache key
117
+ config_hash = cls._get_config_hash(config)
118
+
119
+ if config_hash not in cls._instances:
120
+ logger.info(f"Creating new Azure LLM instance for config hash: {config_hash}")
121
+ instance = super().__new__(cls)
122
+ cls._instances[config_hash] = instance
123
+ else:
124
+ logger.info(f"Reusing existing Azure LLM instance for config hash: {config_hash}")
125
+
126
+ return cls._instances[config_hash]
75
127
 
76
128
  def __init__(self, config: AzureLLMConfig):
129
+ # Avoid duplicate initialization
130
+ if hasattr(self, "_initialized"):
131
+ return
132
+
77
133
  self.config = config
78
134
  self.client = openai.AzureOpenAI(
79
135
  azure_endpoint=config.base_url,
80
136
  api_version=config.api_version,
81
137
  api_key=config.api_key,
82
138
  )
139
+ self._initialized = True
140
+ logger.info("Azure LLM instance initialized")
141
+
142
+ @classmethod
143
+ def _get_config_hash(cls, config: AzureLLMConfig) -> str:
144
+ """Generate hash value of configuration"""
145
+ # Convert config to dict and sort to ensure consistency
146
+ config_dict = config.model_dump()
147
+ config_str = json.dumps(config_dict, sort_keys=True)
148
+ return hashlib.md5(config_str.encode()).hexdigest()
149
+
150
+ @classmethod
151
+ def clear_cache(cls):
152
+ """Clear all cached instances"""
153
+ cls._instances.clear()
154
+ logger.info("Azure LLM instance cache cleared")
83
155
 
84
156
  def generate(self, messages: MessageList) -> str:
85
157
  """Generate a response from Azure OpenAI LLM."""
memos/llms/vllm.py CHANGED
@@ -105,6 +105,7 @@ class VLLMLLM(BaseLLM):
105
105
  "temperature": float(getattr(self.config, "temperature", 0.8)),
106
106
  "max_tokens": int(getattr(self.config, "max_tokens", 1024)),
107
107
  "top_p": float(getattr(self.config, "top_p", 0.9)),
108
+ "extra_body": {"chat_template_kwargs": {"enable_thinking": False}},
108
109
  }
109
110
 
110
111
  response = self.client.chat.completions.create(**completion_kwargs)
@@ -142,6 +143,7 @@ class VLLMLLM(BaseLLM):
142
143
  "max_tokens": int(getattr(self.config, "max_tokens", 1024)),
143
144
  "top_p": float(getattr(self.config, "top_p", 0.9)),
144
145
  "stream": True, # Enable streaming
146
+ "extra_body": {"chat_template_kwargs": {"enable_thinking": False}},
145
147
  }
146
148
 
147
149
  stream = self.client.chat.completions.create(**completion_kwargs)
memos/log.py CHANGED
@@ -1,12 +1,20 @@
1
+ import atexit
1
2
  import logging
3
+ import os
4
+ import threading
5
+ import time
2
6
 
7
+ from concurrent.futures import ThreadPoolExecutor
3
8
  from logging.config import dictConfig
4
9
  from pathlib import Path
5
10
  from sys import stdout
6
11
 
12
+ import requests
13
+
7
14
  from dotenv import load_dotenv
8
15
 
9
16
  from memos import settings
17
+ from memos.context.context import get_current_api_path, get_current_trace_id
10
18
 
11
19
 
12
20
  # Load environment variables
@@ -26,19 +34,129 @@ def _setup_logfile() -> Path:
26
34
  return logfile
27
35
 
28
36
 
37
+ class TraceIDFilter(logging.Filter):
38
+ """add trace_id to the log record"""
39
+
40
+ def filter(self, record):
41
+ try:
42
+ trace_id = get_current_trace_id()
43
+ record.trace_id = trace_id if trace_id else "trace-id"
44
+ except Exception:
45
+ record.trace_id = "trace-id"
46
+ return True
47
+
48
+
49
+ class CustomLoggerRequestHandler(logging.Handler):
50
+ _instance = None
51
+ _lock = threading.Lock()
52
+
53
+ def __new__(cls):
54
+ if cls._instance is None:
55
+ with cls._lock:
56
+ if cls._instance is None:
57
+ cls._instance = super().__new__(cls)
58
+ cls._instance._initialized = False
59
+ cls._instance._executor = None
60
+ cls._instance._session = None
61
+ cls._instance._is_shutting_down = None
62
+ return cls._instance
63
+
64
+ def __init__(self):
65
+ """Initialize handler with minimal setup"""
66
+ if not self._initialized:
67
+ super().__init__()
68
+ workers = int(os.getenv("CUSTOM_LOGGER_WORKERS", "2"))
69
+ self._executor = ThreadPoolExecutor(
70
+ max_workers=workers, thread_name_prefix="log_sender"
71
+ )
72
+ self._is_shutting_down = threading.Event()
73
+ self._session = requests.Session()
74
+ self._initialized = True
75
+ atexit.register(self._cleanup)
76
+
77
+ def emit(self, record):
78
+ """Process log records of INFO or ERROR level (non-blocking)"""
79
+ if os.getenv("CUSTOM_LOGGER_URL") is None or self._is_shutting_down.is_set():
80
+ return
81
+
82
+ # Only process INFO and ERROR level logs
83
+ if record.levelno < logging.INFO: # Skip DEBUG and lower
84
+ return
85
+
86
+ try:
87
+ trace_id = get_current_trace_id() or "trace-id"
88
+ api_path = get_current_api_path()
89
+ if api_path is not None:
90
+ self._executor.submit(self._send_log_sync, record.getMessage(), trace_id, api_path)
91
+ except Exception as e:
92
+ if not self._is_shutting_down.is_set():
93
+ print(f"Error sending log: {e}")
94
+
95
+ def _send_log_sync(self, message, trace_id, api_path):
96
+ """Send log message synchronously in a separate thread"""
97
+ try:
98
+ logger_url = os.getenv("CUSTOM_LOGGER_URL")
99
+ token = os.getenv("CUSTOM_LOGGER_TOKEN")
100
+
101
+ headers = {"Content-Type": "application/json"}
102
+ post_content = {
103
+ "message": message,
104
+ "trace_id": trace_id,
105
+ "action": api_path,
106
+ "current_time": round(time.time(), 3),
107
+ }
108
+
109
+ # Add auth token if exists
110
+ if token:
111
+ headers["Authorization"] = f"Bearer {token}"
112
+
113
+ # Add traceId to headers for consistency
114
+ headers["traceId"] = trace_id
115
+
116
+ # Add custom attributes from env
117
+ for key, value in os.environ.items():
118
+ if key.startswith("CUSTOM_LOGGER_ATTRIBUTE_"):
119
+ attribute_key = key[len("CUSTOM_LOGGER_ATTRIBUTE_") :].lower()
120
+ post_content[attribute_key] = value
121
+
122
+ self._session.post(logger_url, headers=headers, json=post_content, timeout=5)
123
+ except Exception:
124
+ # Silently ignore errors to avoid affecting main application
125
+ pass
126
+
127
+ def _cleanup(self):
128
+ """Clean up resources during program exit"""
129
+ if not self._initialized:
130
+ return
131
+
132
+ self._is_shutting_down.set()
133
+ try:
134
+ self._executor.shutdown(wait=False)
135
+ self._session.close()
136
+ except Exception as e:
137
+ print(f"Error during cleanup: {e}")
138
+
139
+ def close(self):
140
+ """Override close to prevent premature shutdown"""
141
+
142
+
29
143
  LOGGING_CONFIG = {
30
144
  "version": 1,
31
145
  "disable_existing_loggers": False,
32
146
  "formatters": {
33
147
  "standard": {
34
- "format": "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s - %(message)s"
148
+ "format": "%(asctime)s [%(trace_id)s] - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s - %(message)s"
35
149
  },
36
150
  "no_datetime": {
37
- "format": "%(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s - %(message)s"
151
+ "format": "[%(trace_id)s] - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s - %(message)s"
152
+ },
153
+ "simplified": {
154
+ "format": "%(asctime)s | %(trace_id)s | %(levelname)s | %(filename)s:%(lineno)d: %(funcName)s | %(message)s"
38
155
  },
39
156
  },
40
157
  "filters": {
41
- "package_tree_filter": {"()": "logging.Filter", "name": settings.LOG_FILTER_TREE_PREFIX}
158
+ "package_tree_filter": {"()": "logging.Filter", "name": settings.LOG_FILTER_TREE_PREFIX},
159
+ "trace_id_filter": {"()": "memos.log.TraceIDFilter"},
42
160
  },
43
161
  "handlers": {
44
162
  "console": {
@@ -46,7 +164,7 @@ LOGGING_CONFIG = {
46
164
  "class": "logging.StreamHandler",
47
165
  "stream": stdout,
48
166
  "formatter": "no_datetime",
49
- "filters": ["package_tree_filter"],
167
+ "filters": ["package_tree_filter", "trace_id_filter"],
50
168
  },
51
169
  "file": {
52
170
  "level": "DEBUG",
@@ -55,6 +173,12 @@ LOGGING_CONFIG = {
55
173
  "maxBytes": 1024**2 * 10,
56
174
  "backupCount": 10,
57
175
  "formatter": "standard",
176
+ "filters": ["trace_id_filter"],
177
+ },
178
+ "custom_logger": {
179
+ "level": "INFO",
180
+ "class": "memos.log.CustomLoggerRequestHandler",
181
+ "formatter": "simplified",
58
182
  },
59
183
  },
60
184
  "root": { # Root logger handles all logs
memos/mem_cube/general.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import time
2
3
 
3
4
  from typing import Literal
4
5
 
@@ -23,11 +24,13 @@ class GeneralMemCube(BaseMemCube):
23
24
  def __init__(self, config: GeneralMemCubeConfig):
24
25
  """Initialize the MemCube with a configuration."""
25
26
  self.config = config
27
+ time_start = time.time()
26
28
  self._text_mem: BaseTextMemory | None = (
27
29
  MemoryFactory.from_config(config.text_mem)
28
30
  if config.text_mem.backend != "uninitialized"
29
31
  else None
30
32
  )
33
+ logger.info(f"init_text_mem in {time.time() - time_start} seconds")
31
34
  self._act_mem: BaseActMemory | None = (
32
35
  MemoryFactory.from_config(config.act_mem)
33
36
  if config.act_mem.backend != "uninitialized"
@@ -137,7 +140,6 @@ class GeneralMemCube(BaseMemCube):
137
140
  if default_config is not None:
138
141
  config = merge_config_with_default(config, default_config)
139
142
  logger.info(f"Applied default config to cube {config.cube_id}")
140
-
141
143
  mem_cube = GeneralMemCube(config)
142
144
  mem_cube.load(dir, memory_types)
143
145
  return mem_cube