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.
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/METADATA +8 -2
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/RECORD +92 -69
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/WHEEL +1 -1
- memos/__init__.py +1 -1
- memos/api/client.py +109 -0
- memos/api/config.py +35 -8
- memos/api/context/dependencies.py +15 -66
- memos/api/middleware/request_context.py +63 -0
- memos/api/product_api.py +5 -2
- memos/api/product_models.py +107 -16
- memos/api/routers/product_router.py +62 -19
- memos/api/start_api.py +13 -0
- memos/configs/graph_db.py +4 -0
- memos/configs/mem_scheduler.py +38 -3
- memos/configs/memory.py +13 -0
- memos/configs/reranker.py +18 -0
- memos/context/context.py +255 -0
- memos/embedders/factory.py +2 -0
- memos/graph_dbs/base.py +4 -2
- memos/graph_dbs/nebular.py +368 -223
- memos/graph_dbs/neo4j.py +49 -13
- memos/graph_dbs/neo4j_community.py +13 -3
- memos/llms/factory.py +2 -0
- memos/llms/openai.py +74 -2
- memos/llms/vllm.py +2 -0
- memos/log.py +128 -4
- memos/mem_cube/general.py +3 -1
- memos/mem_os/core.py +89 -23
- memos/mem_os/main.py +3 -6
- memos/mem_os/product.py +418 -154
- memos/mem_os/utils/reference_utils.py +20 -0
- memos/mem_reader/factory.py +2 -0
- memos/mem_reader/simple_struct.py +204 -82
- memos/mem_scheduler/analyzer/__init__.py +0 -0
- memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +569 -0
- memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
- memos/mem_scheduler/base_scheduler.py +126 -56
- memos/mem_scheduler/general_modules/dispatcher.py +2 -2
- memos/mem_scheduler/general_modules/misc.py +99 -1
- memos/mem_scheduler/general_modules/scheduler_logger.py +17 -11
- memos/mem_scheduler/general_scheduler.py +40 -88
- memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
- memos/mem_scheduler/memory_manage_modules/memory_filter.py +308 -0
- memos/mem_scheduler/{general_modules → memory_manage_modules}/retriever.py +34 -7
- memos/mem_scheduler/monitors/dispatcher_monitor.py +9 -8
- memos/mem_scheduler/monitors/general_monitor.py +119 -39
- memos/mem_scheduler/optimized_scheduler.py +124 -0
- memos/mem_scheduler/orm_modules/__init__.py +0 -0
- memos/mem_scheduler/orm_modules/base_model.py +635 -0
- memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
- memos/mem_scheduler/scheduler_factory.py +2 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +96 -29
- memos/mem_scheduler/utils/config_utils.py +100 -0
- memos/mem_scheduler/utils/db_utils.py +33 -0
- memos/mem_scheduler/utils/filter_utils.py +1 -1
- memos/mem_scheduler/webservice_modules/__init__.py +0 -0
- memos/mem_user/mysql_user_manager.py +4 -2
- memos/memories/activation/kv.py +2 -1
- memos/memories/textual/item.py +96 -17
- memos/memories/textual/naive.py +1 -1
- memos/memories/textual/tree.py +57 -3
- memos/memories/textual/tree_text_memory/organize/handler.py +4 -2
- memos/memories/textual/tree_text_memory/organize/manager.py +28 -14
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +1 -2
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +75 -23
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +10 -6
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -2
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +119 -21
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +172 -44
- memos/memories/textual/tree_text_memory/retrieve/utils.py +6 -4
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +5 -4
- memos/memos_tools/notification_utils.py +46 -0
- memos/memos_tools/singleton.py +174 -0
- memos/memos_tools/thread_safe_dict.py +22 -0
- memos/memos_tools/thread_safe_dict_segment.py +382 -0
- memos/parsers/factory.py +2 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +24 -0
- memos/reranker/concat.py +59 -0
- memos/reranker/cosine_local.py +96 -0
- memos/reranker/factory.py +48 -0
- memos/reranker/http_bge.py +312 -0
- memos/reranker/noop.py +16 -0
- memos/templates/mem_reader_prompts.py +289 -40
- memos/templates/mem_scheduler_prompts.py +242 -0
- memos/templates/mos_prompts.py +133 -60
- memos/types.py +4 -1
- memos/api/context/context.py +0 -147
- memos/mem_scheduler/mos_for_test_scheduler.py +0 -146
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/entry_points.txt +0 -0
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info/licenses}/LICENSE +0 -0
- /memos/mem_scheduler/{general_modules → webservice_modules}/rabbitmq_service.py +0 -0
- /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,
|
|
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],
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|