MemoryOS 2.0.3__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.
- memoryos-2.0.3.dist-info/METADATA +418 -0
- memoryos-2.0.3.dist-info/RECORD +315 -0
- memoryos-2.0.3.dist-info/WHEEL +4 -0
- memoryos-2.0.3.dist-info/entry_points.txt +3 -0
- memoryos-2.0.3.dist-info/licenses/LICENSE +201 -0
- memos/__init__.py +20 -0
- memos/api/client.py +571 -0
- memos/api/config.py +1018 -0
- memos/api/context/dependencies.py +50 -0
- memos/api/exceptions.py +53 -0
- memos/api/handlers/__init__.py +62 -0
- memos/api/handlers/add_handler.py +158 -0
- memos/api/handlers/base_handler.py +194 -0
- memos/api/handlers/chat_handler.py +1401 -0
- memos/api/handlers/component_init.py +388 -0
- memos/api/handlers/config_builders.py +190 -0
- memos/api/handlers/feedback_handler.py +93 -0
- memos/api/handlers/formatters_handler.py +237 -0
- memos/api/handlers/memory_handler.py +316 -0
- memos/api/handlers/scheduler_handler.py +497 -0
- memos/api/handlers/search_handler.py +222 -0
- memos/api/handlers/suggestion_handler.py +117 -0
- memos/api/mcp_serve.py +614 -0
- memos/api/middleware/request_context.py +101 -0
- memos/api/product_api.py +38 -0
- memos/api/product_models.py +1206 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +477 -0
- memos/api/routers/server_router.py +394 -0
- memos/api/server_api.py +44 -0
- memos/api/start_api.py +433 -0
- memos/chunkers/__init__.py +4 -0
- memos/chunkers/base.py +24 -0
- memos/chunkers/charactertext_chunker.py +41 -0
- memos/chunkers/factory.py +24 -0
- memos/chunkers/markdown_chunker.py +62 -0
- memos/chunkers/sentence_chunker.py +54 -0
- memos/chunkers/simple_chunker.py +50 -0
- memos/cli.py +113 -0
- memos/configs/__init__.py +0 -0
- memos/configs/base.py +82 -0
- memos/configs/chunker.py +59 -0
- memos/configs/embedder.py +88 -0
- memos/configs/graph_db.py +236 -0
- memos/configs/internet_retriever.py +100 -0
- memos/configs/llm.py +151 -0
- memos/configs/mem_agent.py +54 -0
- memos/configs/mem_chat.py +81 -0
- memos/configs/mem_cube.py +105 -0
- memos/configs/mem_os.py +83 -0
- memos/configs/mem_reader.py +91 -0
- memos/configs/mem_scheduler.py +385 -0
- memos/configs/mem_user.py +70 -0
- memos/configs/memory.py +324 -0
- memos/configs/parser.py +38 -0
- memos/configs/reranker.py +18 -0
- memos/configs/utils.py +8 -0
- memos/configs/vec_db.py +80 -0
- memos/context/context.py +355 -0
- memos/dependency.py +52 -0
- memos/deprecation.py +262 -0
- memos/embedders/__init__.py +0 -0
- memos/embedders/ark.py +95 -0
- memos/embedders/base.py +106 -0
- memos/embedders/factory.py +29 -0
- memos/embedders/ollama.py +77 -0
- memos/embedders/sentence_transformer.py +49 -0
- memos/embedders/universal_api.py +51 -0
- memos/exceptions.py +30 -0
- memos/graph_dbs/__init__.py +0 -0
- memos/graph_dbs/base.py +274 -0
- memos/graph_dbs/factory.py +27 -0
- memos/graph_dbs/item.py +46 -0
- memos/graph_dbs/nebular.py +1794 -0
- memos/graph_dbs/neo4j.py +1942 -0
- memos/graph_dbs/neo4j_community.py +1058 -0
- memos/graph_dbs/polardb.py +5446 -0
- memos/hello_world.py +97 -0
- memos/llms/__init__.py +0 -0
- memos/llms/base.py +25 -0
- memos/llms/deepseek.py +13 -0
- memos/llms/factory.py +38 -0
- memos/llms/hf.py +443 -0
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +135 -0
- memos/llms/openai.py +222 -0
- memos/llms/openai_new.py +198 -0
- memos/llms/qwen.py +13 -0
- memos/llms/utils.py +14 -0
- memos/llms/vllm.py +218 -0
- memos/log.py +237 -0
- memos/mem_agent/base.py +19 -0
- memos/mem_agent/deepsearch_agent.py +391 -0
- memos/mem_agent/factory.py +36 -0
- memos/mem_chat/__init__.py +0 -0
- memos/mem_chat/base.py +30 -0
- memos/mem_chat/factory.py +21 -0
- memos/mem_chat/simple.py +200 -0
- memos/mem_cube/__init__.py +0 -0
- memos/mem_cube/base.py +30 -0
- memos/mem_cube/general.py +240 -0
- memos/mem_cube/navie.py +172 -0
- memos/mem_cube/utils.py +169 -0
- memos/mem_feedback/base.py +15 -0
- memos/mem_feedback/feedback.py +1192 -0
- memos/mem_feedback/simple_feedback.py +40 -0
- memos/mem_feedback/utils.py +230 -0
- memos/mem_os/client.py +5 -0
- memos/mem_os/core.py +1203 -0
- memos/mem_os/main.py +582 -0
- memos/mem_os/product.py +1608 -0
- memos/mem_os/product_server.py +455 -0
- memos/mem_os/utils/default_config.py +359 -0
- memos/mem_os/utils/format_utils.py +1403 -0
- memos/mem_os/utils/reference_utils.py +162 -0
- memos/mem_reader/__init__.py +0 -0
- memos/mem_reader/base.py +47 -0
- memos/mem_reader/factory.py +53 -0
- memos/mem_reader/memory.py +298 -0
- memos/mem_reader/multi_modal_struct.py +965 -0
- memos/mem_reader/read_multi_modal/__init__.py +43 -0
- memos/mem_reader/read_multi_modal/assistant_parser.py +311 -0
- memos/mem_reader/read_multi_modal/base.py +273 -0
- memos/mem_reader/read_multi_modal/file_content_parser.py +826 -0
- memos/mem_reader/read_multi_modal/image_parser.py +359 -0
- memos/mem_reader/read_multi_modal/multi_modal_parser.py +252 -0
- memos/mem_reader/read_multi_modal/string_parser.py +139 -0
- memos/mem_reader/read_multi_modal/system_parser.py +327 -0
- memos/mem_reader/read_multi_modal/text_content_parser.py +131 -0
- memos/mem_reader/read_multi_modal/tool_parser.py +210 -0
- memos/mem_reader/read_multi_modal/user_parser.py +218 -0
- memos/mem_reader/read_multi_modal/utils.py +358 -0
- memos/mem_reader/simple_struct.py +912 -0
- memos/mem_reader/strategy_struct.py +163 -0
- memos/mem_reader/utils.py +157 -0
- memos/mem_scheduler/__init__.py +0 -0
- memos/mem_scheduler/analyzer/__init__.py +0 -0
- memos/mem_scheduler/analyzer/api_analyzer.py +714 -0
- memos/mem_scheduler/analyzer/eval_analyzer.py +219 -0
- memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +571 -0
- memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
- memos/mem_scheduler/base_scheduler.py +1319 -0
- memos/mem_scheduler/general_modules/__init__.py +0 -0
- memos/mem_scheduler/general_modules/api_misc.py +137 -0
- memos/mem_scheduler/general_modules/base.py +80 -0
- memos/mem_scheduler/general_modules/init_components_for_scheduler.py +425 -0
- memos/mem_scheduler/general_modules/misc.py +313 -0
- memos/mem_scheduler/general_modules/scheduler_logger.py +389 -0
- memos/mem_scheduler/general_modules/task_threads.py +315 -0
- memos/mem_scheduler/general_scheduler.py +1495 -0
- memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
- memos/mem_scheduler/memory_manage_modules/memory_filter.py +306 -0
- memos/mem_scheduler/memory_manage_modules/retriever.py +547 -0
- memos/mem_scheduler/monitors/__init__.py +0 -0
- memos/mem_scheduler/monitors/dispatcher_monitor.py +366 -0
- memos/mem_scheduler/monitors/general_monitor.py +394 -0
- memos/mem_scheduler/monitors/task_schedule_monitor.py +254 -0
- memos/mem_scheduler/optimized_scheduler.py +410 -0
- memos/mem_scheduler/orm_modules/__init__.py +0 -0
- memos/mem_scheduler/orm_modules/api_redis_model.py +518 -0
- memos/mem_scheduler/orm_modules/base_model.py +729 -0
- memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
- memos/mem_scheduler/orm_modules/redis_model.py +699 -0
- memos/mem_scheduler/scheduler_factory.py +23 -0
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/analyzer_schemas.py +52 -0
- memos/mem_scheduler/schemas/api_schemas.py +233 -0
- memos/mem_scheduler/schemas/general_schemas.py +55 -0
- memos/mem_scheduler/schemas/message_schemas.py +173 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +406 -0
- memos/mem_scheduler/schemas/task_schemas.py +132 -0
- memos/mem_scheduler/task_schedule_modules/__init__.py +0 -0
- memos/mem_scheduler/task_schedule_modules/dispatcher.py +740 -0
- memos/mem_scheduler/task_schedule_modules/local_queue.py +247 -0
- memos/mem_scheduler/task_schedule_modules/orchestrator.py +74 -0
- memos/mem_scheduler/task_schedule_modules/redis_queue.py +1385 -0
- memos/mem_scheduler/task_schedule_modules/task_queue.py +162 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/api_utils.py +77 -0
- memos/mem_scheduler/utils/config_utils.py +100 -0
- memos/mem_scheduler/utils/db_utils.py +50 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/metrics.py +125 -0
- memos/mem_scheduler/utils/misc_utils.py +290 -0
- memos/mem_scheduler/utils/monitor_event_utils.py +67 -0
- memos/mem_scheduler/utils/status_tracker.py +229 -0
- memos/mem_scheduler/webservice_modules/__init__.py +0 -0
- memos/mem_scheduler/webservice_modules/rabbitmq_service.py +485 -0
- memos/mem_scheduler/webservice_modules/redis_service.py +380 -0
- memos/mem_user/factory.py +94 -0
- memos/mem_user/mysql_persistent_user_manager.py +271 -0
- memos/mem_user/mysql_user_manager.py +502 -0
- memos/mem_user/persistent_factory.py +98 -0
- memos/mem_user/persistent_user_manager.py +260 -0
- memos/mem_user/redis_persistent_user_manager.py +225 -0
- memos/mem_user/user_manager.py +488 -0
- memos/memories/__init__.py +0 -0
- memos/memories/activation/__init__.py +0 -0
- memos/memories/activation/base.py +42 -0
- memos/memories/activation/item.py +56 -0
- memos/memories/activation/kv.py +292 -0
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/base.py +19 -0
- memos/memories/factory.py +42 -0
- memos/memories/parametric/__init__.py +0 -0
- memos/memories/parametric/base.py +19 -0
- memos/memories/parametric/item.py +11 -0
- memos/memories/parametric/lora.py +41 -0
- memos/memories/textual/__init__.py +0 -0
- memos/memories/textual/base.py +92 -0
- memos/memories/textual/general.py +236 -0
- memos/memories/textual/item.py +304 -0
- memos/memories/textual/naive.py +187 -0
- memos/memories/textual/prefer_text_memory/__init__.py +0 -0
- memos/memories/textual/prefer_text_memory/adder.py +504 -0
- memos/memories/textual/prefer_text_memory/config.py +106 -0
- memos/memories/textual/prefer_text_memory/extractor.py +221 -0
- memos/memories/textual/prefer_text_memory/factory.py +85 -0
- memos/memories/textual/prefer_text_memory/retrievers.py +177 -0
- memos/memories/textual/prefer_text_memory/spliter.py +132 -0
- memos/memories/textual/prefer_text_memory/utils.py +93 -0
- memos/memories/textual/preference.py +344 -0
- memos/memories/textual/simple_preference.py +161 -0
- memos/memories/textual/simple_tree.py +69 -0
- memos/memories/textual/tree.py +459 -0
- memos/memories/textual/tree_text_memory/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/handler.py +184 -0
- memos/memories/textual/tree_text_memory/organize/manager.py +518 -0
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +238 -0
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +622 -0
- memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/retrieve/advanced_searcher.py +364 -0
- memos/memories/textual/tree_text_memory/retrieve/bm25_util.py +186 -0
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +419 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +270 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +102 -0
- memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +497 -0
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +16 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py +472 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +848 -0
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +135 -0
- memos/memories/textual/tree_text_memory/retrieve/utils.py +54 -0
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +387 -0
- memos/memos_tools/dinding_report_bot.py +453 -0
- memos/memos_tools/lockfree_dict.py +120 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +142 -0
- memos/memos_tools/singleton.py +174 -0
- memos/memos_tools/thread_safe_dict.py +310 -0
- memos/memos_tools/thread_safe_dict_segment.py +382 -0
- memos/multi_mem_cube/__init__.py +0 -0
- memos/multi_mem_cube/composite_cube.py +86 -0
- memos/multi_mem_cube/single_cube.py +874 -0
- memos/multi_mem_cube/views.py +54 -0
- memos/parsers/__init__.py +0 -0
- memos/parsers/base.py +15 -0
- memos/parsers/factory.py +21 -0
- memos/parsers/markitdown.py +28 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +25 -0
- memos/reranker/concat.py +103 -0
- memos/reranker/cosine_local.py +102 -0
- memos/reranker/factory.py +72 -0
- memos/reranker/http_bge.py +324 -0
- memos/reranker/http_bge_strategy.py +327 -0
- memos/reranker/noop.py +19 -0
- memos/reranker/strategies/__init__.py +4 -0
- memos/reranker/strategies/base.py +61 -0
- memos/reranker/strategies/concat_background.py +94 -0
- memos/reranker/strategies/concat_docsource.py +110 -0
- memos/reranker/strategies/dialogue_common.py +109 -0
- memos/reranker/strategies/factory.py +31 -0
- memos/reranker/strategies/single_turn.py +107 -0
- memos/reranker/strategies/singleturn_outmem.py +98 -0
- memos/settings.py +10 -0
- memos/templates/__init__.py +0 -0
- memos/templates/advanced_search_prompts.py +211 -0
- memos/templates/cloud_service_prompt.py +107 -0
- memos/templates/instruction_completion.py +66 -0
- memos/templates/mem_agent_prompts.py +85 -0
- memos/templates/mem_feedback_prompts.py +822 -0
- memos/templates/mem_reader_prompts.py +1096 -0
- memos/templates/mem_reader_strategy_prompts.py +238 -0
- memos/templates/mem_scheduler_prompts.py +626 -0
- memos/templates/mem_search_prompts.py +93 -0
- memos/templates/mos_prompts.py +403 -0
- memos/templates/prefer_complete_prompt.py +735 -0
- memos/templates/tool_mem_prompts.py +139 -0
- memos/templates/tree_reorganize_prompts.py +230 -0
- memos/types/__init__.py +34 -0
- memos/types/general_types.py +151 -0
- memos/types/openai_chat_completion_types/__init__.py +15 -0
- memos/types/openai_chat_completion_types/chat_completion_assistant_message_param.py +56 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +27 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_input_audio_param.py +23 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_param.py +43 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_refusal_param.py +16 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_text_param.py +16 -0
- memos/types/openai_chat_completion_types/chat_completion_message_custom_tool_call_param.py +27 -0
- memos/types/openai_chat_completion_types/chat_completion_message_function_tool_call_param.py +32 -0
- memos/types/openai_chat_completion_types/chat_completion_message_param.py +18 -0
- memos/types/openai_chat_completion_types/chat_completion_message_tool_call_union_param.py +15 -0
- memos/types/openai_chat_completion_types/chat_completion_system_message_param.py +36 -0
- memos/types/openai_chat_completion_types/chat_completion_tool_message_param.py +30 -0
- memos/types/openai_chat_completion_types/chat_completion_user_message_param.py +34 -0
- memos/utils.py +123 -0
- memos/vec_dbs/__init__.py +0 -0
- memos/vec_dbs/base.py +117 -0
- memos/vec_dbs/factory.py +23 -0
- memos/vec_dbs/item.py +50 -0
- memos/vec_dbs/milvus.py +654 -0
- memos/vec_dbs/qdrant.py +355 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import traceback
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from concurrent.futures import as_completed
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from memos.context.context import ContextThreadPoolExecutor
|
|
9
|
+
from memos.embedders.factory import OllamaEmbedder
|
|
10
|
+
from memos.graph_dbs.neo4j import Neo4jGraphDB
|
|
11
|
+
from memos.llms.factory import AzureLLM, OllamaLLM, OpenAILLM
|
|
12
|
+
from memos.log import get_logger
|
|
13
|
+
from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
|
|
14
|
+
from memos.memories.textual.tree_text_memory.organize.reorganizer import (
|
|
15
|
+
GraphStructureReorganizer,
|
|
16
|
+
QueueMessage,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_working_binding_ids(mem_items: list[TextualMemoryItem]) -> set[str]:
|
|
24
|
+
"""
|
|
25
|
+
Scan enhanced memory items for background hints like
|
|
26
|
+
"[working_binding:<uuid>]" and collect those working memory IDs.
|
|
27
|
+
|
|
28
|
+
We store the working<->long binding inside metadata.background when
|
|
29
|
+
initially adding memories in async mode, so we can later clean up
|
|
30
|
+
the temporary WorkingMemory nodes after mem_reader produces the
|
|
31
|
+
final LongTermMemory/UserMemory.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mem_items: list of TextualMemoryItem we just added (enhanced memories)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A set of working memory IDs (as strings) that should be deleted.
|
|
38
|
+
"""
|
|
39
|
+
bindings: set[str] = set()
|
|
40
|
+
pattern = re.compile(r"\[working_binding:([0-9a-fA-F-]{36})\]")
|
|
41
|
+
for item in mem_items:
|
|
42
|
+
try:
|
|
43
|
+
bg = getattr(item.metadata, "background", "") or ""
|
|
44
|
+
except Exception:
|
|
45
|
+
bg = ""
|
|
46
|
+
if not isinstance(bg, str):
|
|
47
|
+
continue
|
|
48
|
+
match = pattern.search(bg)
|
|
49
|
+
if match:
|
|
50
|
+
bindings.add(match.group(1))
|
|
51
|
+
return bindings
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MemoryManager:
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
graph_store: Neo4jGraphDB,
|
|
58
|
+
embedder: OllamaEmbedder,
|
|
59
|
+
llm: OpenAILLM | OllamaLLM | AzureLLM,
|
|
60
|
+
memory_size: dict | None = None,
|
|
61
|
+
threshold: float | None = 0.80,
|
|
62
|
+
merged_threshold: float | None = 0.92,
|
|
63
|
+
is_reorganize: bool = False,
|
|
64
|
+
):
|
|
65
|
+
self.graph_store = graph_store
|
|
66
|
+
self.embedder = embedder
|
|
67
|
+
self.memory_size = memory_size
|
|
68
|
+
self.current_memory_size = {
|
|
69
|
+
"WorkingMemory": 0,
|
|
70
|
+
"LongTermMemory": 0,
|
|
71
|
+
"UserMemory": 0,
|
|
72
|
+
}
|
|
73
|
+
if not memory_size:
|
|
74
|
+
self.memory_size = {
|
|
75
|
+
"WorkingMemory": 20,
|
|
76
|
+
"LongTermMemory": 1500,
|
|
77
|
+
"UserMemory": 480,
|
|
78
|
+
}
|
|
79
|
+
logger.info(f"MemorySize is {self.memory_size}")
|
|
80
|
+
self._threshold = threshold
|
|
81
|
+
self.is_reorganize = is_reorganize
|
|
82
|
+
self.reorganizer = GraphStructureReorganizer(
|
|
83
|
+
graph_store, llm, embedder, is_reorganize=is_reorganize
|
|
84
|
+
)
|
|
85
|
+
self._merged_threshold = merged_threshold
|
|
86
|
+
|
|
87
|
+
def add(
|
|
88
|
+
self,
|
|
89
|
+
memories: list[TextualMemoryItem],
|
|
90
|
+
user_name: str | None = None,
|
|
91
|
+
mode: str = "sync",
|
|
92
|
+
use_batch: bool = True,
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
"""
|
|
95
|
+
Add new memories to different memory types.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
memories: List of memory items to add.
|
|
99
|
+
user_name: Optional user name for the memories.
|
|
100
|
+
mode: "sync" to cleanup and refresh after adding, "async" to skip.
|
|
101
|
+
use_batch: If True, use batch database operations (more efficient for large batches).
|
|
102
|
+
If False, use parallel single-node operations (original behavior).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of added memory IDs.
|
|
106
|
+
"""
|
|
107
|
+
added_ids: list[str] = []
|
|
108
|
+
if use_batch:
|
|
109
|
+
added_ids = self._add_memories_batch(memories, user_name)
|
|
110
|
+
else:
|
|
111
|
+
added_ids = self._add_memories_parallel(memories, user_name)
|
|
112
|
+
|
|
113
|
+
if mode == "sync":
|
|
114
|
+
self._cleanup_working_memory(user_name)
|
|
115
|
+
self._refresh_memory_size(user_name=user_name)
|
|
116
|
+
|
|
117
|
+
return added_ids
|
|
118
|
+
|
|
119
|
+
def _add_memories_parallel(
|
|
120
|
+
self, memories: list[TextualMemoryItem], user_name: str | None = None
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""
|
|
123
|
+
Add memories using parallel single-node operations (original behavior).
|
|
124
|
+
"""
|
|
125
|
+
added_ids: list[str] = []
|
|
126
|
+
with ContextThreadPoolExecutor(max_workers=10) as executor:
|
|
127
|
+
futures = {executor.submit(self._process_memory, m, user_name): m for m in memories}
|
|
128
|
+
for future in as_completed(futures, timeout=500):
|
|
129
|
+
try:
|
|
130
|
+
ids = future.result()
|
|
131
|
+
added_ids.extend(ids)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.exception("Memory processing error: ", exc_info=e)
|
|
134
|
+
logger.info(f"[MemoryManager: _add_memories_parallel] Added {len(added_ids)} memories")
|
|
135
|
+
return added_ids
|
|
136
|
+
|
|
137
|
+
def _add_memories_batch(
|
|
138
|
+
self, memories: list[TextualMemoryItem], user_name: str | None = None, batch_size: int = 5
|
|
139
|
+
) -> list[str]:
|
|
140
|
+
"""
|
|
141
|
+
Add memories using batch database operations (more efficient for large batches).
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
memories: List of memory items to add.
|
|
145
|
+
user_name: Optional user name for the memories.
|
|
146
|
+
batch_size: Number of nodes to insert per batch.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of added graph memory node IDs.
|
|
150
|
+
"""
|
|
151
|
+
if not memories:
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
added_ids: list[str] = []
|
|
155
|
+
working_nodes: list[dict] = []
|
|
156
|
+
graph_nodes: list[dict] = []
|
|
157
|
+
graph_node_ids: list[str] = []
|
|
158
|
+
|
|
159
|
+
for memory in memories:
|
|
160
|
+
working_id = str(uuid.uuid4())
|
|
161
|
+
|
|
162
|
+
if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"):
|
|
163
|
+
working_metadata = memory.metadata.model_copy(
|
|
164
|
+
update={"memory_type": "WorkingMemory"}
|
|
165
|
+
).model_dump(exclude_none=True)
|
|
166
|
+
working_metadata["updated_at"] = datetime.now().isoformat()
|
|
167
|
+
working_nodes.append(
|
|
168
|
+
{
|
|
169
|
+
"id": working_id,
|
|
170
|
+
"memory": memory.memory,
|
|
171
|
+
"metadata": working_metadata,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
if memory.metadata.memory_type in (
|
|
175
|
+
"LongTermMemory",
|
|
176
|
+
"UserMemory",
|
|
177
|
+
"ToolSchemaMemory",
|
|
178
|
+
"ToolTrajectoryMemory",
|
|
179
|
+
):
|
|
180
|
+
graph_node_id = str(uuid.uuid4())
|
|
181
|
+
metadata_dict = memory.metadata.model_dump(exclude_none=True)
|
|
182
|
+
metadata_dict["updated_at"] = datetime.now().isoformat()
|
|
183
|
+
|
|
184
|
+
# Add working_binding for fast mode
|
|
185
|
+
tags = metadata_dict.get("tags") or []
|
|
186
|
+
if "mode:fast" in tags:
|
|
187
|
+
prev_bg = metadata_dict.get("background", "") or ""
|
|
188
|
+
binding_line = f"[working_binding:{working_id}] direct built from raw inputs"
|
|
189
|
+
metadata_dict["background"] = (
|
|
190
|
+
f"{prev_bg} || {binding_line}" if prev_bg else binding_line
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
graph_nodes.append(
|
|
194
|
+
{
|
|
195
|
+
"id": graph_node_id,
|
|
196
|
+
"memory": memory.memory,
|
|
197
|
+
"metadata": metadata_dict,
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
graph_node_ids.append(graph_node_id)
|
|
201
|
+
added_ids.append(graph_node_id)
|
|
202
|
+
|
|
203
|
+
def _submit_batches(nodes: list[dict], node_kind: str) -> None:
|
|
204
|
+
if not nodes:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
max_workers = min(8, max(1, len(nodes) // max(1, batch_size)))
|
|
208
|
+
with ContextThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
209
|
+
futures: list[tuple[int, int, object]] = []
|
|
210
|
+
for batch_index, i in enumerate(range(0, len(nodes), batch_size), start=1):
|
|
211
|
+
batch = nodes[i : i + batch_size]
|
|
212
|
+
fut = executor.submit(
|
|
213
|
+
self.graph_store.add_nodes_batch, batch, user_name=user_name
|
|
214
|
+
)
|
|
215
|
+
futures.append((batch_index, len(batch), fut))
|
|
216
|
+
|
|
217
|
+
for idx, size, fut in futures:
|
|
218
|
+
try:
|
|
219
|
+
fut.result()
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.exception(
|
|
222
|
+
f"Batch add {node_kind} nodes error (batch {idx}, size {size}): ",
|
|
223
|
+
exc_info=e,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
_submit_batches(working_nodes, "WorkingMemory")
|
|
227
|
+
_submit_batches(graph_nodes, "graph memory")
|
|
228
|
+
|
|
229
|
+
if graph_node_ids and self.is_reorganize:
|
|
230
|
+
self.reorganizer.add_message(QueueMessage(op="add", after_node=graph_node_ids))
|
|
231
|
+
|
|
232
|
+
return added_ids
|
|
233
|
+
|
|
234
|
+
def _cleanup_working_memory(self, user_name: str | None = None) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Remove oldest WorkingMemory nodes to keep within size limit.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
self.graph_store.remove_oldest_memory(
|
|
240
|
+
memory_type="WorkingMemory",
|
|
241
|
+
keep_latest=self.memory_size["WorkingMemory"],
|
|
242
|
+
user_name=user_name,
|
|
243
|
+
)
|
|
244
|
+
except Exception:
|
|
245
|
+
logger.warning(f"Remove WorkingMemory error: {traceback.format_exc()}")
|
|
246
|
+
|
|
247
|
+
def replace_working_memory(
|
|
248
|
+
self, memories: list[TextualMemoryItem], user_name: str | None = None
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Replace WorkingMemory
|
|
252
|
+
"""
|
|
253
|
+
working_memory_top_k = memories[: self.memory_size["WorkingMemory"]]
|
|
254
|
+
with ContextThreadPoolExecutor(max_workers=8) as executor:
|
|
255
|
+
futures = [
|
|
256
|
+
executor.submit(
|
|
257
|
+
self._add_memory_to_db, memory, "WorkingMemory", user_name=user_name
|
|
258
|
+
)
|
|
259
|
+
for memory in working_memory_top_k
|
|
260
|
+
]
|
|
261
|
+
for future in as_completed(futures, timeout=60):
|
|
262
|
+
try:
|
|
263
|
+
future.result()
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.exception("Memory processing error: ", exc_info=e)
|
|
266
|
+
|
|
267
|
+
self.graph_store.remove_oldest_memory(
|
|
268
|
+
memory_type="WorkingMemory",
|
|
269
|
+
keep_latest=self.memory_size["WorkingMemory"],
|
|
270
|
+
user_name=user_name,
|
|
271
|
+
)
|
|
272
|
+
self._refresh_memory_size(user_name=user_name)
|
|
273
|
+
|
|
274
|
+
def get_current_memory_size(self, user_name: str | None = None) -> dict[str, int]:
|
|
275
|
+
"""
|
|
276
|
+
Return the cached memory type counts.
|
|
277
|
+
"""
|
|
278
|
+
self._refresh_memory_size(user_name=user_name)
|
|
279
|
+
return self.current_memory_size
|
|
280
|
+
|
|
281
|
+
def _refresh_memory_size(self, user_name: str | None = None) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Query the latest counts from the graph store and update internal state.
|
|
284
|
+
"""
|
|
285
|
+
results = self.graph_store.get_grouped_counts(
|
|
286
|
+
group_fields=["memory_type"], user_name=user_name
|
|
287
|
+
)
|
|
288
|
+
self.current_memory_size = {
|
|
289
|
+
record["memory_type"]: int(record["count"]) for record in results
|
|
290
|
+
}
|
|
291
|
+
logger.info(f"[MemoryManager] Refreshed memory sizes: {self.current_memory_size}")
|
|
292
|
+
|
|
293
|
+
def _process_memory(self, memory: TextualMemoryItem, user_name: str | None = None):
|
|
294
|
+
"""
|
|
295
|
+
Process and add memory to different memory types.
|
|
296
|
+
|
|
297
|
+
Behavior:
|
|
298
|
+
1. Always create a WorkingMemory node from `memory` and get its node id.
|
|
299
|
+
2. If `memory.metadata.memory_type` is "LongTermMemory" or "UserMemory",
|
|
300
|
+
also create a corresponding long/user node.
|
|
301
|
+
- In async mode, that long/user node's metadata will include
|
|
302
|
+
`working_binding` in `background` which records the WorkingMemory
|
|
303
|
+
node id created in step 1.
|
|
304
|
+
3. Return ONLY the ids of the long/user nodes (NOT the working node id),
|
|
305
|
+
which preserves the previous external contract of `add()`.
|
|
306
|
+
"""
|
|
307
|
+
ids: list[str] = []
|
|
308
|
+
futures = []
|
|
309
|
+
|
|
310
|
+
working_id = str(uuid.uuid4())
|
|
311
|
+
|
|
312
|
+
with ContextThreadPoolExecutor(max_workers=2, thread_name_prefix="mem") as ex:
|
|
313
|
+
if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"):
|
|
314
|
+
f_working = ex.submit(
|
|
315
|
+
self._add_memory_to_db, memory, "WorkingMemory", user_name, working_id
|
|
316
|
+
)
|
|
317
|
+
futures.append(("working", f_working))
|
|
318
|
+
|
|
319
|
+
if memory.metadata.memory_type in (
|
|
320
|
+
"LongTermMemory",
|
|
321
|
+
"UserMemory",
|
|
322
|
+
"ToolSchemaMemory",
|
|
323
|
+
"ToolTrajectoryMemory",
|
|
324
|
+
):
|
|
325
|
+
f_graph = ex.submit(
|
|
326
|
+
self._add_to_graph_memory,
|
|
327
|
+
memory=memory,
|
|
328
|
+
memory_type=memory.metadata.memory_type,
|
|
329
|
+
user_name=user_name,
|
|
330
|
+
working_binding=working_id,
|
|
331
|
+
)
|
|
332
|
+
futures.append(("long", f_graph))
|
|
333
|
+
|
|
334
|
+
for kind, fut in futures:
|
|
335
|
+
try:
|
|
336
|
+
res = fut.result()
|
|
337
|
+
if kind != "working" and isinstance(res, str) and res:
|
|
338
|
+
ids.append(res)
|
|
339
|
+
except Exception:
|
|
340
|
+
logger.warning("Parallel memory processing failed:\n%s", traceback.format_exc())
|
|
341
|
+
|
|
342
|
+
return ids
|
|
343
|
+
|
|
344
|
+
def _add_memory_to_db(
|
|
345
|
+
self,
|
|
346
|
+
memory: TextualMemoryItem,
|
|
347
|
+
memory_type: str,
|
|
348
|
+
user_name: str | None = None,
|
|
349
|
+
forced_id: str | None = None,
|
|
350
|
+
) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Add a single memory item to the graph store, with FIFO logic for WorkingMemory.
|
|
353
|
+
If forced_id is provided, use that as the node id.
|
|
354
|
+
"""
|
|
355
|
+
metadata = memory.metadata.model_copy(update={"memory_type": memory_type}).model_dump(
|
|
356
|
+
exclude_none=True
|
|
357
|
+
)
|
|
358
|
+
metadata["updated_at"] = datetime.now().isoformat()
|
|
359
|
+
node_id = forced_id or str(uuid.uuid4())
|
|
360
|
+
working_memory = TextualMemoryItem(id=node_id, memory=memory.memory, metadata=metadata)
|
|
361
|
+
# Insert node into graph
|
|
362
|
+
self.graph_store.add_node(working_memory.id, working_memory.memory, metadata, user_name)
|
|
363
|
+
return node_id
|
|
364
|
+
|
|
365
|
+
def _add_to_graph_memory(
|
|
366
|
+
self,
|
|
367
|
+
memory: TextualMemoryItem,
|
|
368
|
+
memory_type: str,
|
|
369
|
+
user_name: str | None = None,
|
|
370
|
+
working_binding: str | None = None,
|
|
371
|
+
):
|
|
372
|
+
"""
|
|
373
|
+
Generalized method to add memory to a graph-based memory type (e.g., LongTermMemory, UserMemory).
|
|
374
|
+
"""
|
|
375
|
+
node_id = str(uuid.uuid4())
|
|
376
|
+
# Step 2: Add new node to graph
|
|
377
|
+
metadata_dict = memory.metadata.model_dump(exclude_none=True)
|
|
378
|
+
tags = metadata_dict.get("tags") or []
|
|
379
|
+
if working_binding and ("mode:fast" in tags):
|
|
380
|
+
prev_bg = metadata_dict.get("background", "") or ""
|
|
381
|
+
binding_line = f"[working_binding:{working_binding}] direct built from raw inputs"
|
|
382
|
+
if prev_bg:
|
|
383
|
+
metadata_dict["background"] = prev_bg + " || " + binding_line
|
|
384
|
+
else:
|
|
385
|
+
metadata_dict["background"] = binding_line
|
|
386
|
+
self.graph_store.add_node(
|
|
387
|
+
node_id,
|
|
388
|
+
memory.memory,
|
|
389
|
+
metadata_dict,
|
|
390
|
+
user_name=user_name,
|
|
391
|
+
)
|
|
392
|
+
self.reorganizer.add_message(
|
|
393
|
+
QueueMessage(
|
|
394
|
+
op="add",
|
|
395
|
+
after_node=[node_id],
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
return node_id
|
|
399
|
+
|
|
400
|
+
def _inherit_edges(self, from_id: str, to_id: str) -> None:
|
|
401
|
+
"""
|
|
402
|
+
Migrate all non-lineage edges from `from_id` to `to_id`,
|
|
403
|
+
and remove them from `from_id` after copying.
|
|
404
|
+
"""
|
|
405
|
+
edges = self.graph_store.get_edges(from_id, type="ANY", direction="ANY")
|
|
406
|
+
|
|
407
|
+
for edge in edges:
|
|
408
|
+
if edge["type"] == "MERGED_TO":
|
|
409
|
+
continue # Keep lineage edges
|
|
410
|
+
|
|
411
|
+
new_from = to_id if edge["from"] == from_id else edge["from"]
|
|
412
|
+
new_to = to_id if edge["to"] == from_id else edge["to"]
|
|
413
|
+
|
|
414
|
+
if new_from == new_to:
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
# Add edge to merged node if it doesn't already exist
|
|
418
|
+
if not self.graph_store.edge_exists(new_from, new_to, edge["type"], direction="ANY"):
|
|
419
|
+
self.graph_store.add_edge(new_from, new_to, edge["type"])
|
|
420
|
+
|
|
421
|
+
# Remove original edge if it involved the archived node
|
|
422
|
+
self.graph_store.delete_edge(edge["from"], edge["to"], edge["type"])
|
|
423
|
+
|
|
424
|
+
def _ensure_structure_path(
|
|
425
|
+
self, memory_type: str, metadata: TreeNodeTextualMemoryMetadata
|
|
426
|
+
) -> str:
|
|
427
|
+
"""
|
|
428
|
+
Ensure structural path exists (ROOT → ... → final node), return last node ID.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
path: like ["hobby", "photography"]
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Final node ID of the structure path.
|
|
435
|
+
"""
|
|
436
|
+
# Step 1: Try to find an existing memory node with content == tag
|
|
437
|
+
existing = self.graph_store.get_by_metadata(
|
|
438
|
+
[
|
|
439
|
+
{"field": "memory", "op": "=", "value": metadata.key},
|
|
440
|
+
{"field": "memory_type", "op": "=", "value": memory_type},
|
|
441
|
+
]
|
|
442
|
+
)
|
|
443
|
+
if existing:
|
|
444
|
+
node_id = existing[0] # Use the first match
|
|
445
|
+
else:
|
|
446
|
+
# Step 2: If not found, create a new structure node
|
|
447
|
+
new_node = TextualMemoryItem(
|
|
448
|
+
memory=metadata.key,
|
|
449
|
+
metadata=TreeNodeTextualMemoryMetadata(
|
|
450
|
+
user_id=metadata.user_id,
|
|
451
|
+
session_id=metadata.session_id,
|
|
452
|
+
memory_type=memory_type,
|
|
453
|
+
status="activated",
|
|
454
|
+
tags=[],
|
|
455
|
+
key=metadata.key,
|
|
456
|
+
embedding=self.embedder.embed([metadata.key])[0],
|
|
457
|
+
usage=[],
|
|
458
|
+
sources=[],
|
|
459
|
+
confidence=0.99,
|
|
460
|
+
background="",
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
self.graph_store.add_node(
|
|
464
|
+
id=new_node.id,
|
|
465
|
+
memory=new_node.memory,
|
|
466
|
+
metadata=new_node.metadata.model_dump(exclude_none=True),
|
|
467
|
+
)
|
|
468
|
+
self.reorganizer.add_message(
|
|
469
|
+
QueueMessage(
|
|
470
|
+
op="add",
|
|
471
|
+
after_node=[new_node.id],
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
node_id = new_node.id
|
|
476
|
+
|
|
477
|
+
# Step 3: Return this structure node ID as the parent_id
|
|
478
|
+
return node_id
|
|
479
|
+
|
|
480
|
+
def remove_and_refresh_memory(self, user_name: str | None = None):
|
|
481
|
+
self._cleanup_memories_if_needed(user_name=user_name)
|
|
482
|
+
self._refresh_memory_size(user_name=user_name)
|
|
483
|
+
|
|
484
|
+
def _cleanup_memories_if_needed(self, user_name: str | None = None) -> None:
|
|
485
|
+
"""
|
|
486
|
+
Only clean up memories if we're close to or over the limit.
|
|
487
|
+
This reduces unnecessary database operations.
|
|
488
|
+
"""
|
|
489
|
+
cleanup_threshold = 0.8 # Clean up when 80% full
|
|
490
|
+
|
|
491
|
+
logger.info(f"self.memory_size: {self.memory_size}")
|
|
492
|
+
for memory_type, limit in self.memory_size.items():
|
|
493
|
+
current_count = self.current_memory_size.get(memory_type, 0)
|
|
494
|
+
threshold = int(int(limit) * cleanup_threshold)
|
|
495
|
+
|
|
496
|
+
# Only clean up if we're at or above the threshold
|
|
497
|
+
if current_count >= threshold:
|
|
498
|
+
try:
|
|
499
|
+
self.graph_store.remove_oldest_memory(
|
|
500
|
+
memory_type=memory_type, keep_latest=limit, user_name=user_name
|
|
501
|
+
)
|
|
502
|
+
logger.debug(f"Cleaned up {memory_type}: {current_count} -> {limit}")
|
|
503
|
+
except Exception:
|
|
504
|
+
logger.warning(f"Remove {memory_type} error: {traceback.format_exc()}")
|
|
505
|
+
|
|
506
|
+
def wait_reorganizer(self):
|
|
507
|
+
"""
|
|
508
|
+
Wait for the reorganizer to finish processing all messages.
|
|
509
|
+
"""
|
|
510
|
+
logger.debug("Waiting for reorganizer to finish processing messages...")
|
|
511
|
+
self.reorganizer.wait_until_current_task_done()
|
|
512
|
+
|
|
513
|
+
def close(self):
|
|
514
|
+
self.wait_reorganizer()
|
|
515
|
+
self.reorganizer.stop()
|
|
516
|
+
|
|
517
|
+
def __del__(self):
|
|
518
|
+
self.close()
|