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,965 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from memos import log
|
|
9
|
+
from memos.configs.mem_reader import MultiModalStructMemReaderConfig
|
|
10
|
+
from memos.context.context import ContextThreadPoolExecutor
|
|
11
|
+
from memos.mem_reader.read_multi_modal import MultiModalParser, detect_lang
|
|
12
|
+
from memos.mem_reader.read_multi_modal.base import _derive_key
|
|
13
|
+
from memos.mem_reader.simple_struct import PROMPT_DICT, SimpleStructMemReader
|
|
14
|
+
from memos.mem_reader.utils import parse_json_result
|
|
15
|
+
from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
|
|
16
|
+
from memos.templates.mem_reader_prompts import MEMORY_MERGE_PROMPT_EN, MEMORY_MERGE_PROMPT_ZH
|
|
17
|
+
from memos.templates.tool_mem_prompts import TOOL_TRAJECTORY_PROMPT_EN, TOOL_TRAJECTORY_PROMPT_ZH
|
|
18
|
+
from memos.types import MessagesType
|
|
19
|
+
from memos.utils import timed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = log.get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MultiModalStructMemReader(SimpleStructMemReader):
|
|
26
|
+
"""Multimodal implementation of MemReader that inherits from
|
|
27
|
+
SimpleStructMemReader."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: MultiModalStructMemReaderConfig):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the MultiModalStructMemReader with configuration.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
config: Configuration object for the reader
|
|
35
|
+
"""
|
|
36
|
+
from memos.configs.mem_reader import SimpleStructMemReaderConfig
|
|
37
|
+
|
|
38
|
+
# Extract direct_markdown_hostnames before converting to SimpleStructMemReaderConfig
|
|
39
|
+
direct_markdown_hostnames = getattr(config, "direct_markdown_hostnames", None)
|
|
40
|
+
|
|
41
|
+
# Create config_dict excluding direct_markdown_hostnames for SimpleStructMemReaderConfig
|
|
42
|
+
config_dict = config.model_dump(exclude_none=True)
|
|
43
|
+
config_dict.pop("direct_markdown_hostnames", None)
|
|
44
|
+
|
|
45
|
+
simple_config = SimpleStructMemReaderConfig(**config_dict)
|
|
46
|
+
super().__init__(simple_config)
|
|
47
|
+
|
|
48
|
+
# Initialize MultiModalParser for routing to different parsers
|
|
49
|
+
self.multi_modal_parser = MultiModalParser(
|
|
50
|
+
embedder=self.embedder,
|
|
51
|
+
llm=self.llm,
|
|
52
|
+
parser=None,
|
|
53
|
+
direct_markdown_hostnames=direct_markdown_hostnames,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _split_large_memory_item(
|
|
57
|
+
self, item: TextualMemoryItem, max_tokens: int
|
|
58
|
+
) -> list[TextualMemoryItem]:
|
|
59
|
+
"""
|
|
60
|
+
Split a single memory item that exceeds max_tokens into multiple chunks.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
item: TextualMemoryItem to split
|
|
64
|
+
max_tokens: Maximum tokens per chunk
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of TextualMemoryItem chunks
|
|
68
|
+
"""
|
|
69
|
+
item_text = item.memory or ""
|
|
70
|
+
if not item_text:
|
|
71
|
+
return [item]
|
|
72
|
+
|
|
73
|
+
item_tokens = self._count_tokens(item_text)
|
|
74
|
+
if item_tokens <= max_tokens:
|
|
75
|
+
return [item]
|
|
76
|
+
|
|
77
|
+
# Use chunker to split the text
|
|
78
|
+
try:
|
|
79
|
+
chunks = self.chunker.chunk(item_text)
|
|
80
|
+
split_items = []
|
|
81
|
+
|
|
82
|
+
for chunk in chunks:
|
|
83
|
+
# Chunk objects have a 'text' attribute
|
|
84
|
+
chunk_text = chunk.text
|
|
85
|
+
if not chunk_text or not chunk_text.strip():
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Create a new memory item for each chunk, preserving original metadata
|
|
89
|
+
split_item = self._make_memory_item(
|
|
90
|
+
value=chunk_text,
|
|
91
|
+
info={
|
|
92
|
+
"user_id": item.metadata.user_id,
|
|
93
|
+
"session_id": item.metadata.session_id,
|
|
94
|
+
**(item.metadata.info or {}),
|
|
95
|
+
},
|
|
96
|
+
memory_type=item.metadata.memory_type,
|
|
97
|
+
tags=item.metadata.tags or [],
|
|
98
|
+
key=item.metadata.key,
|
|
99
|
+
sources=item.metadata.sources or [],
|
|
100
|
+
background=item.metadata.background or "",
|
|
101
|
+
)
|
|
102
|
+
split_items.append(split_item)
|
|
103
|
+
|
|
104
|
+
return split_items if split_items else [item]
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(
|
|
107
|
+
f"[MultiModalStruct] Failed to split large memory item: {e}. Returning original item."
|
|
108
|
+
)
|
|
109
|
+
return [item]
|
|
110
|
+
|
|
111
|
+
def _concat_multi_modal_memories(
|
|
112
|
+
self, all_memory_items: list[TextualMemoryItem], max_tokens=None, overlap=200
|
|
113
|
+
) -> list[TextualMemoryItem]:
|
|
114
|
+
"""
|
|
115
|
+
Aggregates memory items using sliding window logic similar to
|
|
116
|
+
`_iter_chat_windows` in simple_struct:
|
|
117
|
+
1. Groups items into windows based on token count (max_tokens)
|
|
118
|
+
2. Each window has overlap tokens for context continuity
|
|
119
|
+
3. Aggregates items within each window into a single memory item
|
|
120
|
+
4. Determines memory_type based on roles in each window
|
|
121
|
+
5. Splits single large memory items that exceed max_tokens
|
|
122
|
+
"""
|
|
123
|
+
if not all_memory_items:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
max_tokens = max_tokens or self.chat_window_max_tokens
|
|
127
|
+
|
|
128
|
+
# Split large memory items before processing
|
|
129
|
+
processed_items = []
|
|
130
|
+
for item in all_memory_items:
|
|
131
|
+
item_text = item.memory or ""
|
|
132
|
+
item_tokens = self._count_tokens(item_text)
|
|
133
|
+
if item_tokens > max_tokens:
|
|
134
|
+
# Split the large item into multiple chunks
|
|
135
|
+
split_items = self._split_large_memory_item(item, max_tokens)
|
|
136
|
+
processed_items.extend(split_items)
|
|
137
|
+
else:
|
|
138
|
+
processed_items.append(item)
|
|
139
|
+
|
|
140
|
+
# If only one item after processing, return as-is
|
|
141
|
+
if len(processed_items) == 1:
|
|
142
|
+
return processed_items
|
|
143
|
+
|
|
144
|
+
windows = []
|
|
145
|
+
buf_items = []
|
|
146
|
+
cur_text = ""
|
|
147
|
+
|
|
148
|
+
# Extract info from first item (all items should have same user_id, session_id)
|
|
149
|
+
first_item = processed_items[0]
|
|
150
|
+
info = {
|
|
151
|
+
"user_id": first_item.metadata.user_id,
|
|
152
|
+
"session_id": first_item.metadata.session_id,
|
|
153
|
+
**(first_item.metadata.info or {}),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for _idx, item in enumerate(processed_items):
|
|
157
|
+
item_text = item.memory or ""
|
|
158
|
+
# Ensure line ends with newline (same format as simple_struct)
|
|
159
|
+
line = item_text if item_text.endswith("\n") else f"{item_text}\n"
|
|
160
|
+
|
|
161
|
+
# Check if adding this item would exceed max_tokens (same logic as _iter_chat_windows)
|
|
162
|
+
# Note: After splitting large items, each item should be <= max_tokens,
|
|
163
|
+
# but we still check to handle edge cases
|
|
164
|
+
if self._count_tokens(cur_text + line) > max_tokens and cur_text:
|
|
165
|
+
# Yield current window
|
|
166
|
+
window = self._build_window_from_items(buf_items, info)
|
|
167
|
+
if window:
|
|
168
|
+
windows.append(window)
|
|
169
|
+
|
|
170
|
+
# Keep overlap: remove items until remaining tokens <= overlap
|
|
171
|
+
# (same logic as _iter_chat_windows)
|
|
172
|
+
while (
|
|
173
|
+
buf_items
|
|
174
|
+
and self._count_tokens("".join([it.memory or "" for it in buf_items])) > overlap
|
|
175
|
+
):
|
|
176
|
+
buf_items.pop(0)
|
|
177
|
+
# Recalculate cur_text from remaining items
|
|
178
|
+
cur_text = "".join([it.memory or "" for it in buf_items])
|
|
179
|
+
|
|
180
|
+
# Add item to current window
|
|
181
|
+
buf_items.append(item)
|
|
182
|
+
# Recalculate cur_text from all items in buffer (same as _iter_chat_windows)
|
|
183
|
+
cur_text = "".join([it.memory or "" for it in buf_items])
|
|
184
|
+
|
|
185
|
+
# Yield final window if any items remain
|
|
186
|
+
if buf_items:
|
|
187
|
+
window = self._build_window_from_items(buf_items, info)
|
|
188
|
+
if window:
|
|
189
|
+
windows.append(window)
|
|
190
|
+
|
|
191
|
+
# Batch compute embeddings for all windows
|
|
192
|
+
if windows:
|
|
193
|
+
# Collect all valid windows that need embedding
|
|
194
|
+
valid_windows = [w for w in windows if w and w.memory]
|
|
195
|
+
|
|
196
|
+
if valid_windows:
|
|
197
|
+
# Collect all texts that need embedding
|
|
198
|
+
texts_to_embed = [w.memory for w in valid_windows]
|
|
199
|
+
|
|
200
|
+
# Batch compute all embeddings at once
|
|
201
|
+
try:
|
|
202
|
+
embeddings = self.embedder.embed(texts_to_embed)
|
|
203
|
+
# Fill embeddings back into memory items
|
|
204
|
+
for window, embedding in zip(valid_windows, embeddings, strict=True):
|
|
205
|
+
window.metadata.embedding = embedding
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"[MultiModalStruct] Error batch computing embeddings: {e}")
|
|
208
|
+
# Fallback: compute embeddings individually
|
|
209
|
+
for window in valid_windows:
|
|
210
|
+
if window.memory:
|
|
211
|
+
try:
|
|
212
|
+
window.metadata.embedding = self.embedder.embed([window.memory])[0]
|
|
213
|
+
except Exception as e2:
|
|
214
|
+
logger.error(
|
|
215
|
+
f"[MultiModalStruct] Error computing embedding for item: {e2}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return windows
|
|
219
|
+
|
|
220
|
+
def _build_window_from_items(
|
|
221
|
+
self, items: list[TextualMemoryItem], info: dict[str, Any]
|
|
222
|
+
) -> TextualMemoryItem | None:
|
|
223
|
+
"""
|
|
224
|
+
Build a single memory item from a window of items (similar to _build_fast_node).
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
items: List of TextualMemoryItem objects in the window
|
|
228
|
+
info: Dictionary containing user_id and session_id
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Aggregated TextualMemoryItem or None if no valid content
|
|
232
|
+
"""
|
|
233
|
+
if not items:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
# Collect all memory texts and sources
|
|
237
|
+
memory_texts = []
|
|
238
|
+
all_sources = []
|
|
239
|
+
roles = set()
|
|
240
|
+
aggregated_file_ids: list[str] = []
|
|
241
|
+
|
|
242
|
+
for item in items:
|
|
243
|
+
if item.memory:
|
|
244
|
+
memory_texts.append(item.memory)
|
|
245
|
+
|
|
246
|
+
# Collect sources and extract roles
|
|
247
|
+
item_sources = item.metadata.sources or []
|
|
248
|
+
if not isinstance(item_sources, list):
|
|
249
|
+
item_sources = [item_sources]
|
|
250
|
+
|
|
251
|
+
for source in item_sources:
|
|
252
|
+
# Add source to all_sources
|
|
253
|
+
all_sources.append(source)
|
|
254
|
+
|
|
255
|
+
# Extract role from source
|
|
256
|
+
if hasattr(source, "role") and source.role:
|
|
257
|
+
roles.add(source.role)
|
|
258
|
+
elif isinstance(source, dict) and source.get("role"):
|
|
259
|
+
roles.add(source.get("role"))
|
|
260
|
+
|
|
261
|
+
# Aggregate file_ids from metadata
|
|
262
|
+
metadata = getattr(item, "metadata", None)
|
|
263
|
+
if metadata is not None:
|
|
264
|
+
item_file_ids = getattr(metadata, "file_ids", None)
|
|
265
|
+
if isinstance(item_file_ids, list):
|
|
266
|
+
for fid in item_file_ids:
|
|
267
|
+
if fid and fid not in aggregated_file_ids:
|
|
268
|
+
aggregated_file_ids.append(fid)
|
|
269
|
+
|
|
270
|
+
# Determine memory_type based on roles (same logic as simple_struct)
|
|
271
|
+
# UserMemory if only user role, else LongTermMemory
|
|
272
|
+
memory_type = "UserMemory" if roles == {"user"} else "LongTermMemory"
|
|
273
|
+
|
|
274
|
+
# Merge all memory texts (preserve the format from parser)
|
|
275
|
+
merged_text = "".join(memory_texts) if memory_texts else ""
|
|
276
|
+
|
|
277
|
+
if not merged_text.strip():
|
|
278
|
+
# If no text content, return None
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
# Create aggregated memory item without embedding (will be computed in batch later)
|
|
282
|
+
extra_kwargs: dict[str, Any] = {}
|
|
283
|
+
if aggregated_file_ids:
|
|
284
|
+
extra_kwargs["file_ids"] = aggregated_file_ids
|
|
285
|
+
|
|
286
|
+
# Extract info fields
|
|
287
|
+
info_ = info.copy()
|
|
288
|
+
user_id = info_.pop("user_id", "")
|
|
289
|
+
session_id = info_.pop("session_id", "")
|
|
290
|
+
|
|
291
|
+
# Create memory item without embedding (set to None, will be filled in batch)
|
|
292
|
+
aggregated_item = TextualMemoryItem(
|
|
293
|
+
memory=merged_text,
|
|
294
|
+
metadata=TreeNodeTextualMemoryMetadata(
|
|
295
|
+
user_id=user_id,
|
|
296
|
+
session_id=session_id,
|
|
297
|
+
memory_type=memory_type,
|
|
298
|
+
status="activated",
|
|
299
|
+
tags=["mode:fast"],
|
|
300
|
+
key=_derive_key(merged_text),
|
|
301
|
+
embedding=None, # Will be computed in batch
|
|
302
|
+
usage=[],
|
|
303
|
+
sources=all_sources,
|
|
304
|
+
background="",
|
|
305
|
+
confidence=0.99,
|
|
306
|
+
type="fact",
|
|
307
|
+
info=info_,
|
|
308
|
+
**extra_kwargs,
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return aggregated_item
|
|
313
|
+
|
|
314
|
+
def _get_llm_response(
|
|
315
|
+
self,
|
|
316
|
+
mem_str: str,
|
|
317
|
+
custom_tags: list[str] | None = None,
|
|
318
|
+
sources: list | None = None,
|
|
319
|
+
prompt_type: str = "chat",
|
|
320
|
+
) -> dict:
|
|
321
|
+
"""
|
|
322
|
+
Override parent method to improve language detection by using actual text content
|
|
323
|
+
from sources instead of JSON-structured memory string.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
mem_str: Memory string (may contain JSON structures)
|
|
327
|
+
custom_tags: Optional custom tags
|
|
328
|
+
sources: Optional list of SourceMessage objects to extract text content from
|
|
329
|
+
prompt_type: Type of prompt to use ("chat" or "doc")
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
LLM response dictionary
|
|
333
|
+
"""
|
|
334
|
+
# Determine language: prioritize lang from sources (set in fast mode),
|
|
335
|
+
# fallback to detecting from mem_str if sources don't have lang
|
|
336
|
+
lang = None
|
|
337
|
+
|
|
338
|
+
# First, try to get lang from sources (fast mode already set this)
|
|
339
|
+
if sources:
|
|
340
|
+
for source in sources:
|
|
341
|
+
if hasattr(source, "lang") and source.lang:
|
|
342
|
+
lang = source.lang
|
|
343
|
+
break
|
|
344
|
+
elif isinstance(source, dict) and source.get("lang"):
|
|
345
|
+
lang = source.get("lang")
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
# Fallback: detect language from mem_str if no lang from sources
|
|
349
|
+
if lang is None:
|
|
350
|
+
lang = detect_lang(mem_str)
|
|
351
|
+
|
|
352
|
+
# Select prompt template based on prompt_type
|
|
353
|
+
if prompt_type == "doc":
|
|
354
|
+
template = PROMPT_DICT["doc"][lang]
|
|
355
|
+
examples = "" # doc prompts don't have examples
|
|
356
|
+
prompt = template.replace("{chunk_text}", mem_str)
|
|
357
|
+
elif prompt_type == "general_string":
|
|
358
|
+
template = PROMPT_DICT["general_string"][lang]
|
|
359
|
+
examples = ""
|
|
360
|
+
prompt = template.replace("{chunk_text}", mem_str)
|
|
361
|
+
else:
|
|
362
|
+
template = PROMPT_DICT["chat"][lang]
|
|
363
|
+
examples = PROMPT_DICT["chat"][f"{lang}_example"]
|
|
364
|
+
prompt = template.replace("${conversation}", mem_str)
|
|
365
|
+
|
|
366
|
+
custom_tags_prompt = (
|
|
367
|
+
PROMPT_DICT["custom_tags"][lang].replace("{custom_tags}", str(custom_tags))
|
|
368
|
+
if custom_tags
|
|
369
|
+
else ""
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Replace custom_tags_prompt placeholder (different for doc vs chat)
|
|
373
|
+
if prompt_type in ["doc", "general_string"]:
|
|
374
|
+
prompt = prompt.replace("{custom_tags_prompt}", custom_tags_prompt)
|
|
375
|
+
else:
|
|
376
|
+
prompt = prompt.replace("${custom_tags_prompt}", custom_tags_prompt)
|
|
377
|
+
|
|
378
|
+
if self.config.remove_prompt_example and examples:
|
|
379
|
+
prompt = prompt.replace(examples, "")
|
|
380
|
+
messages = [{"role": "user", "content": prompt}]
|
|
381
|
+
try:
|
|
382
|
+
response_text = self.llm.generate(messages)
|
|
383
|
+
response_json = parse_json_result(response_text)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error(f"[LLM] Exception during chat generation: {e}")
|
|
386
|
+
response_json = {
|
|
387
|
+
"memory list": [
|
|
388
|
+
{
|
|
389
|
+
"key": mem_str[:10],
|
|
390
|
+
"memory_type": "UserMemory",
|
|
391
|
+
"value": mem_str,
|
|
392
|
+
"tags": [],
|
|
393
|
+
}
|
|
394
|
+
],
|
|
395
|
+
"summary": mem_str,
|
|
396
|
+
}
|
|
397
|
+
logger.info(f"[MultiModalFine] Task {messages}, Result {response_json}")
|
|
398
|
+
return response_json
|
|
399
|
+
|
|
400
|
+
def _determine_prompt_type(self, sources: list) -> str:
|
|
401
|
+
"""
|
|
402
|
+
Determine prompt type based on sources.
|
|
403
|
+
"""
|
|
404
|
+
if not sources:
|
|
405
|
+
return "chat"
|
|
406
|
+
prompt_type = "general_string"
|
|
407
|
+
for source in sources:
|
|
408
|
+
source_role = None
|
|
409
|
+
if hasattr(source, "role"):
|
|
410
|
+
source_role = source.role
|
|
411
|
+
elif isinstance(source, dict):
|
|
412
|
+
source_role = source.get("role")
|
|
413
|
+
if source_role in {"user", "assistant", "system", "tool"}:
|
|
414
|
+
prompt_type = "chat"
|
|
415
|
+
|
|
416
|
+
return prompt_type
|
|
417
|
+
|
|
418
|
+
def _get_maybe_merged_memory(
|
|
419
|
+
self,
|
|
420
|
+
extracted_memory_dict: dict,
|
|
421
|
+
mem_text: str,
|
|
422
|
+
sources: list,
|
|
423
|
+
**kwargs,
|
|
424
|
+
) -> dict:
|
|
425
|
+
"""
|
|
426
|
+
Check if extracted memory should be merged with similar existing memories.
|
|
427
|
+
If merge is needed, return merged memory dict with merged_from field.
|
|
428
|
+
Otherwise, return original memory dict.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
extracted_memory_dict: The extracted memory dict from LLM response
|
|
432
|
+
mem_text: The memory text content
|
|
433
|
+
sources: Source messages for language detection
|
|
434
|
+
**kwargs: Additional parameters (merge_similarity_threshold, etc.)
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Memory dict (possibly merged) with merged_from field if merged
|
|
438
|
+
"""
|
|
439
|
+
# If no graph_db or user_name, return original
|
|
440
|
+
if not self.graph_db or "user_name" not in kwargs:
|
|
441
|
+
return extracted_memory_dict
|
|
442
|
+
user_name = kwargs.get("user_name")
|
|
443
|
+
|
|
444
|
+
# Detect language
|
|
445
|
+
lang = "en"
|
|
446
|
+
if sources:
|
|
447
|
+
for source in sources:
|
|
448
|
+
if hasattr(source, "lang") and source.lang:
|
|
449
|
+
lang = source.lang
|
|
450
|
+
break
|
|
451
|
+
elif isinstance(source, dict) and source.get("lang"):
|
|
452
|
+
lang = source.get("lang")
|
|
453
|
+
break
|
|
454
|
+
if lang is None:
|
|
455
|
+
lang = detect_lang(mem_text)
|
|
456
|
+
|
|
457
|
+
# Search for similar memories
|
|
458
|
+
merge_threshold = kwargs.get("merge_similarity_threshold", 0.3)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
search_results = self.graph_db.search_by_embedding(
|
|
462
|
+
vector=self.embedder.embed(mem_text)[0],
|
|
463
|
+
top_k=20,
|
|
464
|
+
status="activated",
|
|
465
|
+
threshold=merge_threshold,
|
|
466
|
+
user_name=user_name,
|
|
467
|
+
filter={
|
|
468
|
+
"or": [
|
|
469
|
+
{"memory_type": "LongTermMemory"},
|
|
470
|
+
{"memory_type": "UserMemory"},
|
|
471
|
+
{"memory_type": "WorkingMemory"},
|
|
472
|
+
]
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if not search_results:
|
|
477
|
+
return extracted_memory_dict
|
|
478
|
+
|
|
479
|
+
# Get full memory details
|
|
480
|
+
similar_memory_ids = [r["id"] for r in search_results if r.get("id")]
|
|
481
|
+
similar_memories_list = [
|
|
482
|
+
self.graph_db.get_node(mem_id, include_embedding=False, user_name=user_name)
|
|
483
|
+
for mem_id in similar_memory_ids
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
# Filter out None and mode:fast memories
|
|
487
|
+
filtered_similar = []
|
|
488
|
+
for mem in similar_memories_list:
|
|
489
|
+
if not mem:
|
|
490
|
+
continue
|
|
491
|
+
mem_metadata = mem.get("metadata", {})
|
|
492
|
+
tags = mem_metadata.get("tags", [])
|
|
493
|
+
if isinstance(tags, list) and "mode:fast" in tags:
|
|
494
|
+
continue
|
|
495
|
+
filtered_similar.append(
|
|
496
|
+
{
|
|
497
|
+
"id": mem.get("id"),
|
|
498
|
+
"memory": mem.get("memory", ""),
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
logger.info(
|
|
502
|
+
f"Valid similar memories for {mem_text} is "
|
|
503
|
+
f"{len(filtered_similar)}: {filtered_similar}"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if not filtered_similar:
|
|
507
|
+
return extracted_memory_dict
|
|
508
|
+
|
|
509
|
+
# Create a temporary TextualMemoryItem for merge check
|
|
510
|
+
temp_memory_item = TextualMemoryItem(
|
|
511
|
+
memory=mem_text,
|
|
512
|
+
metadata=TreeNodeTextualMemoryMetadata(
|
|
513
|
+
user_id="",
|
|
514
|
+
session_id="",
|
|
515
|
+
memory_type=extracted_memory_dict.get("memory_type", "LongTermMemory"),
|
|
516
|
+
status="activated",
|
|
517
|
+
tags=extracted_memory_dict.get("tags", []),
|
|
518
|
+
key=extracted_memory_dict.get("key", ""),
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Try to merge with LLM
|
|
523
|
+
merge_result = self._merge_memories_with_llm(
|
|
524
|
+
temp_memory_item, filtered_similar, lang=lang
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
if merge_result:
|
|
528
|
+
# Return merged memory dict
|
|
529
|
+
merged_dict = extracted_memory_dict.copy()
|
|
530
|
+
merged_content = merge_result.get("value", mem_text)
|
|
531
|
+
merged_dict["value"] = merged_content
|
|
532
|
+
merged_from_ids = merge_result.get("merged_from", [])
|
|
533
|
+
merged_dict["merged_from"] = merged_from_ids
|
|
534
|
+
return merged_dict
|
|
535
|
+
else:
|
|
536
|
+
return extracted_memory_dict
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
logger.error(f"[MultiModalFine] Error in get_maybe_merged_memory: {e}")
|
|
540
|
+
# On error, return original
|
|
541
|
+
return extracted_memory_dict
|
|
542
|
+
|
|
543
|
+
def _merge_memories_with_llm(
|
|
544
|
+
self,
|
|
545
|
+
new_memory: TextualMemoryItem,
|
|
546
|
+
similar_memories: list[dict],
|
|
547
|
+
lang: str = "en",
|
|
548
|
+
) -> dict | None:
|
|
549
|
+
"""
|
|
550
|
+
Use LLM to merge new memory with similar existing memories.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
new_memory: The newly extracted memory item
|
|
554
|
+
similar_memories: List of similar memories from graph_db (with id and memory fields)
|
|
555
|
+
lang: Language code ("en" or "zh")
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Merged memory dict with merged_from field, or None if no merge needed
|
|
559
|
+
"""
|
|
560
|
+
if not similar_memories:
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
# Build merge prompt using template
|
|
564
|
+
similar_memories_text = "\n".join(
|
|
565
|
+
[f"[{mem['id']}]: {mem['memory']}" for mem in similar_memories]
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
merge_prompt_template = MEMORY_MERGE_PROMPT_ZH if lang == "zh" else MEMORY_MERGE_PROMPT_EN
|
|
569
|
+
merge_prompt = merge_prompt_template.format(
|
|
570
|
+
new_memory=new_memory.memory,
|
|
571
|
+
similar_memories=similar_memories_text,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
response_text = self.llm.generate([{"role": "user", "content": merge_prompt}])
|
|
576
|
+
merge_result = parse_json_result(response_text)
|
|
577
|
+
|
|
578
|
+
if merge_result.get("should_merge", False):
|
|
579
|
+
return {
|
|
580
|
+
"value": merge_result.get("value", new_memory.memory),
|
|
581
|
+
"merged_from": merge_result.get(
|
|
582
|
+
"merged_from", [mem["id"] for mem in similar_memories]
|
|
583
|
+
),
|
|
584
|
+
}
|
|
585
|
+
except Exception as e:
|
|
586
|
+
logger.error(f"[MultiModalFine] Error in merge LLM call: {e}")
|
|
587
|
+
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
def _process_string_fine(
|
|
591
|
+
self,
|
|
592
|
+
fast_memory_items: list[TextualMemoryItem],
|
|
593
|
+
info: dict[str, Any],
|
|
594
|
+
custom_tags: list[str] | None = None,
|
|
595
|
+
**kwargs,
|
|
596
|
+
) -> list[TextualMemoryItem]:
|
|
597
|
+
"""
|
|
598
|
+
Process fast mode memory items through LLM to generate fine mode memories.
|
|
599
|
+
"""
|
|
600
|
+
if not fast_memory_items:
|
|
601
|
+
return []
|
|
602
|
+
|
|
603
|
+
def _process_one_item(fast_item: TextualMemoryItem) -> list[TextualMemoryItem]:
|
|
604
|
+
"""Process a single fast memory item and return a list of fine items."""
|
|
605
|
+
fine_items: list[TextualMemoryItem] = []
|
|
606
|
+
|
|
607
|
+
# Extract memory text (string content)
|
|
608
|
+
mem_str = fast_item.memory or ""
|
|
609
|
+
if not mem_str.strip():
|
|
610
|
+
return fine_items
|
|
611
|
+
|
|
612
|
+
sources = fast_item.metadata.sources or []
|
|
613
|
+
if not isinstance(sources, list):
|
|
614
|
+
sources = [sources]
|
|
615
|
+
|
|
616
|
+
# Extract file_ids from fast item metadata for propagation
|
|
617
|
+
metadata = getattr(fast_item, "metadata", None)
|
|
618
|
+
file_ids = getattr(metadata, "file_ids", None) if metadata is not None else None
|
|
619
|
+
file_ids = [fid for fid in file_ids if fid] if isinstance(file_ids, list) else []
|
|
620
|
+
|
|
621
|
+
# Build per-item info copy and kwargs for _make_memory_item
|
|
622
|
+
info_per_item = info.copy()
|
|
623
|
+
if file_ids and "file_id" not in info_per_item:
|
|
624
|
+
info_per_item["file_id"] = file_ids[0]
|
|
625
|
+
extra_kwargs: dict[str, Any] = {}
|
|
626
|
+
if file_ids:
|
|
627
|
+
extra_kwargs["file_ids"] = file_ids
|
|
628
|
+
|
|
629
|
+
# Determine prompt type based on sources
|
|
630
|
+
prompt_type = self._determine_prompt_type(sources)
|
|
631
|
+
|
|
632
|
+
# ========== Stage 1: Normal extraction (without reference) ==========
|
|
633
|
+
try:
|
|
634
|
+
resp = self._get_llm_response(mem_str, custom_tags, sources, prompt_type)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
logger.error(f"[MultiModalFine] Error calling LLM: {e}")
|
|
637
|
+
return fine_items
|
|
638
|
+
|
|
639
|
+
if resp.get("memory list", []):
|
|
640
|
+
for m in resp.get("memory list", []):
|
|
641
|
+
try:
|
|
642
|
+
# Check and merge with similar memories if needed
|
|
643
|
+
m_maybe_merged = self._get_maybe_merged_memory(
|
|
644
|
+
extracted_memory_dict=m,
|
|
645
|
+
mem_text=m.get("value", ""),
|
|
646
|
+
sources=sources,
|
|
647
|
+
original_query=mem_str,
|
|
648
|
+
**kwargs,
|
|
649
|
+
)
|
|
650
|
+
# Normalize memory_type (same as simple_struct)
|
|
651
|
+
memory_type = (
|
|
652
|
+
m_maybe_merged.get("memory_type", "LongTermMemory")
|
|
653
|
+
.replace("长期记忆", "LongTermMemory")
|
|
654
|
+
.replace("用户记忆", "UserMemory")
|
|
655
|
+
)
|
|
656
|
+
node = self._make_memory_item(
|
|
657
|
+
value=m_maybe_merged.get("value", ""),
|
|
658
|
+
info=info_per_item,
|
|
659
|
+
memory_type=memory_type,
|
|
660
|
+
tags=m_maybe_merged.get("tags", []),
|
|
661
|
+
key=m_maybe_merged.get("key", ""),
|
|
662
|
+
sources=sources, # Preserve sources from fast item
|
|
663
|
+
background=resp.get("summary", ""),
|
|
664
|
+
**extra_kwargs,
|
|
665
|
+
)
|
|
666
|
+
# Add merged_from to info if present
|
|
667
|
+
if "merged_from" in m_maybe_merged:
|
|
668
|
+
node.metadata.info = node.metadata.info or {}
|
|
669
|
+
node.metadata.info["merged_from"] = m_maybe_merged["merged_from"]
|
|
670
|
+
fine_items.append(node)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
logger.error(f"[MultiModalFine] parse error: {e}")
|
|
673
|
+
elif resp.get("value") and resp.get("key"):
|
|
674
|
+
try:
|
|
675
|
+
# Check and merge with similar memories if needed
|
|
676
|
+
resp_maybe_merged = self._get_maybe_merged_memory(
|
|
677
|
+
extracted_memory_dict=resp,
|
|
678
|
+
mem_text=resp.get("value", "").strip(),
|
|
679
|
+
sources=sources,
|
|
680
|
+
original_query=mem_str,
|
|
681
|
+
**kwargs,
|
|
682
|
+
)
|
|
683
|
+
node = self._make_memory_item(
|
|
684
|
+
value=resp_maybe_merged.get("value", "").strip(),
|
|
685
|
+
info=info_per_item,
|
|
686
|
+
memory_type="LongTermMemory",
|
|
687
|
+
tags=resp_maybe_merged.get("tags", []),
|
|
688
|
+
key=resp_maybe_merged.get("key", None),
|
|
689
|
+
sources=sources, # Preserve sources from fast item
|
|
690
|
+
background=resp.get("summary", ""),
|
|
691
|
+
**extra_kwargs,
|
|
692
|
+
)
|
|
693
|
+
# Add merged_from to info if present
|
|
694
|
+
if "merged_from" in resp_maybe_merged:
|
|
695
|
+
node.metadata.info = node.metadata.info or {}
|
|
696
|
+
node.metadata.info["merged_from"] = resp_maybe_merged["merged_from"]
|
|
697
|
+
fine_items.append(node)
|
|
698
|
+
except Exception as e:
|
|
699
|
+
logger.error(f"[MultiModalFine] parse error: {e}")
|
|
700
|
+
|
|
701
|
+
return fine_items
|
|
702
|
+
|
|
703
|
+
fine_memory_items: list[TextualMemoryItem] = []
|
|
704
|
+
|
|
705
|
+
with ContextThreadPoolExecutor(max_workers=30) as executor:
|
|
706
|
+
futures = [executor.submit(_process_one_item, item) for item in fast_memory_items]
|
|
707
|
+
|
|
708
|
+
for future in concurrent.futures.as_completed(futures):
|
|
709
|
+
try:
|
|
710
|
+
result = future.result()
|
|
711
|
+
if result:
|
|
712
|
+
fine_memory_items.extend(result)
|
|
713
|
+
except Exception as e:
|
|
714
|
+
logger.error(f"[MultiModalFine] worker error: {e}")
|
|
715
|
+
|
|
716
|
+
return fine_memory_items
|
|
717
|
+
|
|
718
|
+
def _get_llm_tool_trajectory_response(self, mem_str: str) -> dict:
|
|
719
|
+
"""
|
|
720
|
+
Generete tool trajectory experience item by llm.
|
|
721
|
+
"""
|
|
722
|
+
try:
|
|
723
|
+
lang = detect_lang(mem_str)
|
|
724
|
+
template = TOOL_TRAJECTORY_PROMPT_ZH if lang == "zh" else TOOL_TRAJECTORY_PROMPT_EN
|
|
725
|
+
prompt = template.replace("{messages}", mem_str)
|
|
726
|
+
rsp = self.llm.generate([{"role": "user", "content": prompt}])
|
|
727
|
+
rsp = rsp.replace("```json", "").replace("```", "")
|
|
728
|
+
return json.loads(rsp)
|
|
729
|
+
except Exception as e:
|
|
730
|
+
logger.error(f"[MultiModalFine] Error calling LLM for tool trajectory: {e}")
|
|
731
|
+
return []
|
|
732
|
+
|
|
733
|
+
def _process_tool_trajectory_fine(
|
|
734
|
+
self, fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], **kwargs
|
|
735
|
+
) -> list[TextualMemoryItem]:
|
|
736
|
+
"""
|
|
737
|
+
Process tool trajectory memory items through LLM to generate fine mode memories.
|
|
738
|
+
"""
|
|
739
|
+
if not fast_memory_items:
|
|
740
|
+
return []
|
|
741
|
+
|
|
742
|
+
fine_memory_items = []
|
|
743
|
+
|
|
744
|
+
for fast_item in fast_memory_items:
|
|
745
|
+
# Extract memory text (string content)
|
|
746
|
+
mem_str = fast_item.memory or ""
|
|
747
|
+
if not mem_str.strip() or (
|
|
748
|
+
"tool:" not in mem_str
|
|
749
|
+
and "[tool_calls]:" not in mem_str
|
|
750
|
+
and not re.search(r"<tool_schema>.*?</tool_schema>", mem_str, re.DOTALL)
|
|
751
|
+
):
|
|
752
|
+
continue
|
|
753
|
+
try:
|
|
754
|
+
resp = self._get_llm_tool_trajectory_response(mem_str)
|
|
755
|
+
except Exception as e:
|
|
756
|
+
logger.error(f"[MultiModalFine] Error calling LLM for tool trajectory: {e}")
|
|
757
|
+
continue
|
|
758
|
+
for m in resp:
|
|
759
|
+
try:
|
|
760
|
+
# Normalize memory_type (same as simple_struct)
|
|
761
|
+
memory_type = "ToolTrajectoryMemory"
|
|
762
|
+
|
|
763
|
+
node = self._make_memory_item(
|
|
764
|
+
value=m.get("trajectory", ""),
|
|
765
|
+
info=info,
|
|
766
|
+
memory_type=memory_type,
|
|
767
|
+
correctness=m.get("correctness", ""),
|
|
768
|
+
experience=m.get("experience", ""),
|
|
769
|
+
tool_used_status=m.get("tool_used_status", []),
|
|
770
|
+
)
|
|
771
|
+
fine_memory_items.append(node)
|
|
772
|
+
except Exception as e:
|
|
773
|
+
logger.error(f"[MultiModalFine] parse error for tool trajectory: {e}")
|
|
774
|
+
|
|
775
|
+
return fine_memory_items
|
|
776
|
+
|
|
777
|
+
@timed
|
|
778
|
+
def _process_multi_modal_data(
|
|
779
|
+
self, scene_data_info: MessagesType, info, mode: str = "fine", **kwargs
|
|
780
|
+
) -> list[TextualMemoryItem]:
|
|
781
|
+
"""
|
|
782
|
+
Process multimodal data using MultiModalParser.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
scene_data_info: MessagesType input
|
|
786
|
+
info: Dictionary containing user_id and session_id
|
|
787
|
+
mode: mem-reader mode, fast for quick process while fine for
|
|
788
|
+
better understanding via calling llm
|
|
789
|
+
**kwargs: Additional parameters (mode, etc.)
|
|
790
|
+
"""
|
|
791
|
+
# Pop custom_tags from info (same as simple_struct.py)
|
|
792
|
+
# must pop here, avoid add to info, only used in sync fine mode
|
|
793
|
+
custom_tags = info.pop("custom_tags", None) if isinstance(info, dict) else None
|
|
794
|
+
|
|
795
|
+
# Use MultiModalParser to parse the scene data
|
|
796
|
+
# If it's a list, parse each item; otherwise parse as single message
|
|
797
|
+
if isinstance(scene_data_info, list):
|
|
798
|
+
# Parse each message in the list
|
|
799
|
+
all_memory_items = []
|
|
800
|
+
for msg in scene_data_info:
|
|
801
|
+
items = self.multi_modal_parser.parse(msg, info, mode="fast", **kwargs)
|
|
802
|
+
all_memory_items.extend(items)
|
|
803
|
+
else:
|
|
804
|
+
# Parse as single message
|
|
805
|
+
all_memory_items = self.multi_modal_parser.parse(
|
|
806
|
+
scene_data_info, info, mode="fast", **kwargs
|
|
807
|
+
)
|
|
808
|
+
fast_memory_items = self._concat_multi_modal_memories(all_memory_items)
|
|
809
|
+
if mode == "fast":
|
|
810
|
+
return fast_memory_items
|
|
811
|
+
else:
|
|
812
|
+
# Part A: call llm in parallel using thread pool
|
|
813
|
+
fine_memory_items = []
|
|
814
|
+
|
|
815
|
+
with ContextThreadPoolExecutor(max_workers=2) as executor:
|
|
816
|
+
future_string = executor.submit(
|
|
817
|
+
self._process_string_fine, fast_memory_items, info, custom_tags, **kwargs
|
|
818
|
+
)
|
|
819
|
+
future_tool = executor.submit(
|
|
820
|
+
self._process_tool_trajectory_fine, fast_memory_items, info, **kwargs
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
# Collect results
|
|
824
|
+
fine_memory_items_string_parser = future_string.result()
|
|
825
|
+
fine_memory_items_tool_trajectory_parser = future_tool.result()
|
|
826
|
+
|
|
827
|
+
fine_memory_items.extend(fine_memory_items_string_parser)
|
|
828
|
+
fine_memory_items.extend(fine_memory_items_tool_trajectory_parser)
|
|
829
|
+
|
|
830
|
+
# Part B: get fine multimodal items
|
|
831
|
+
for fast_item in fast_memory_items:
|
|
832
|
+
sources = fast_item.metadata.sources
|
|
833
|
+
for source in sources:
|
|
834
|
+
lang = getattr(source, "lang", "en")
|
|
835
|
+
items = self.multi_modal_parser.process_transfer(
|
|
836
|
+
source,
|
|
837
|
+
context_items=[fast_item],
|
|
838
|
+
custom_tags=custom_tags,
|
|
839
|
+
info=info,
|
|
840
|
+
lang=lang,
|
|
841
|
+
)
|
|
842
|
+
fine_memory_items.extend(items)
|
|
843
|
+
return fine_memory_items
|
|
844
|
+
|
|
845
|
+
@timed
|
|
846
|
+
def _process_transfer_multi_modal_data(
|
|
847
|
+
self, raw_node: TextualMemoryItem, custom_tags: list[str] | None = None, **kwargs
|
|
848
|
+
) -> list[TextualMemoryItem]:
|
|
849
|
+
"""
|
|
850
|
+
Process transfer for multimodal data.
|
|
851
|
+
|
|
852
|
+
Each source is processed independently by its corresponding parser,
|
|
853
|
+
which knows how to rebuild the original message and parse it in fine mode.
|
|
854
|
+
"""
|
|
855
|
+
sources = raw_node.metadata.sources or []
|
|
856
|
+
if not sources:
|
|
857
|
+
logger.warning("[MultiModalStruct] No sources found in raw_node")
|
|
858
|
+
return []
|
|
859
|
+
|
|
860
|
+
# Extract info from raw_node (same as simple_struct.py)
|
|
861
|
+
info = {
|
|
862
|
+
"user_id": raw_node.metadata.user_id,
|
|
863
|
+
"session_id": raw_node.metadata.session_id,
|
|
864
|
+
**(raw_node.metadata.info or {}),
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
fine_memory_items = []
|
|
868
|
+
# Part A: call llm in parallel using thread pool
|
|
869
|
+
with ContextThreadPoolExecutor(max_workers=2) as executor:
|
|
870
|
+
future_string = executor.submit(
|
|
871
|
+
self._process_string_fine, [raw_node], info, custom_tags, **kwargs
|
|
872
|
+
)
|
|
873
|
+
future_tool = executor.submit(
|
|
874
|
+
self._process_tool_trajectory_fine, [raw_node], info, **kwargs
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
# Collect results
|
|
878
|
+
fine_memory_items_string_parser = future_string.result()
|
|
879
|
+
fine_memory_items_tool_trajectory_parser = future_tool.result()
|
|
880
|
+
|
|
881
|
+
fine_memory_items.extend(fine_memory_items_string_parser)
|
|
882
|
+
fine_memory_items.extend(fine_memory_items_tool_trajectory_parser)
|
|
883
|
+
|
|
884
|
+
# Part B: get fine multimodal items
|
|
885
|
+
for source in sources:
|
|
886
|
+
lang = getattr(source, "lang", "en")
|
|
887
|
+
items = self.multi_modal_parser.process_transfer(
|
|
888
|
+
source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang
|
|
889
|
+
)
|
|
890
|
+
fine_memory_items.extend(items)
|
|
891
|
+
return fine_memory_items
|
|
892
|
+
|
|
893
|
+
def get_scene_data_info(self, scene_data: list, type: str) -> list[list[Any]]:
|
|
894
|
+
"""
|
|
895
|
+
Convert normalized MessagesType scenes into scene data info.
|
|
896
|
+
For MultiModalStructMemReader, this is a simplified version that returns the scenes as-is.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
scene_data: List of MessagesType scenes
|
|
900
|
+
type: Type of scene_data: ['doc', 'chat']
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
List of scene data info
|
|
904
|
+
"""
|
|
905
|
+
# TODO: split messages
|
|
906
|
+
return scene_data
|
|
907
|
+
|
|
908
|
+
def _read_memory(
|
|
909
|
+
self,
|
|
910
|
+
messages: list[MessagesType],
|
|
911
|
+
type: str,
|
|
912
|
+
info: dict[str, Any],
|
|
913
|
+
mode: str = "fine",
|
|
914
|
+
**kwargs,
|
|
915
|
+
) -> list[list[TextualMemoryItem]]:
|
|
916
|
+
list_scene_data_info = self.get_scene_data_info(messages, type)
|
|
917
|
+
|
|
918
|
+
memory_list = []
|
|
919
|
+
# Process Q&A pairs concurrently with context propagation
|
|
920
|
+
with ContextThreadPoolExecutor() as executor:
|
|
921
|
+
futures = [
|
|
922
|
+
executor.submit(
|
|
923
|
+
self._process_multi_modal_data, scene_data_info, info, mode=mode, **kwargs
|
|
924
|
+
)
|
|
925
|
+
for scene_data_info in list_scene_data_info
|
|
926
|
+
]
|
|
927
|
+
for future in concurrent.futures.as_completed(futures):
|
|
928
|
+
try:
|
|
929
|
+
res_memory = future.result()
|
|
930
|
+
if res_memory is not None:
|
|
931
|
+
memory_list.append(res_memory)
|
|
932
|
+
except Exception as e:
|
|
933
|
+
logger.error(f"Task failed with exception: {e}")
|
|
934
|
+
logger.error(traceback.format_exc())
|
|
935
|
+
return memory_list
|
|
936
|
+
|
|
937
|
+
def fine_transfer_simple_mem(
|
|
938
|
+
self,
|
|
939
|
+
input_memories: list[TextualMemoryItem],
|
|
940
|
+
type: str,
|
|
941
|
+
custom_tags: list[str] | None = None,
|
|
942
|
+
**kwargs,
|
|
943
|
+
) -> list[list[TextualMemoryItem]]:
|
|
944
|
+
if not input_memories:
|
|
945
|
+
return []
|
|
946
|
+
|
|
947
|
+
memory_list = []
|
|
948
|
+
|
|
949
|
+
# Process Q&A pairs concurrently with context propagation
|
|
950
|
+
with ContextThreadPoolExecutor() as executor:
|
|
951
|
+
futures = [
|
|
952
|
+
executor.submit(
|
|
953
|
+
self._process_transfer_multi_modal_data, scene_data_info, custom_tags, **kwargs
|
|
954
|
+
)
|
|
955
|
+
for scene_data_info in input_memories
|
|
956
|
+
]
|
|
957
|
+
for future in concurrent.futures.as_completed(futures):
|
|
958
|
+
try:
|
|
959
|
+
res_memory = future.result()
|
|
960
|
+
if res_memory is not None:
|
|
961
|
+
memory_list.append(res_memory)
|
|
962
|
+
except Exception as e:
|
|
963
|
+
logger.error(f"Task failed with exception: {e}")
|
|
964
|
+
logger.error(traceback.format_exc())
|
|
965
|
+
return memory_list
|