Skip to content

Commit 9686810

Browse files
CaralHsiCarltonXiangfancyboi999fridayLyuan.wang
authored
feat: Multi-Model Memory Reader with Modular Parser Architecture (#536)
* docs: update .env.example with comprehensive variables and comments * hotfix:hotfix * test: add routers api * feat: add multi-cube feature to chat * refactor: define ChatRequest and related backups * fix: func name in product models * feat: add 'task_id' in AddRequest(for get async add status later); refactor chatstream/chatcomplete function * feat: add add-mode in API AddRequest * add server router add api example * feat: update server router example * feat: tiny update for simple struct: support MessageType only for input(not tackle with different types yet) * feat: add _coerce_scene_data in simple memreader to transform scenedata to list[MessagesType] * feat: add multi-model reader * feat: init multi-model; update _coerce_scene_data * feat: add chat_time in coerce_scene_data * refactor: tiny adjust function name and remove useless func * feat: adjuct doc process in simple_struct mem-reader * refactor: rename _get_scene_data_info -> get_scene_data_info * feat: finish simple reader * format: update example reader: just better display * feat: update test coarse memory * feat: add MultiModelStruct MemReader * feat: update multi_model_struct, simplify and as a child from SimpleStructReader * feat: update multi_model_struct parser * fix: test bug * feat: add base parse * feat: add base fast parser * feat: update multi_model_struct * feat: modify sources * feat: fix some parameters in multi-model parser * fix: fine_memory_items bugs --------- Co-authored-by: HarveyXiang <[email protected]> Co-authored-by: fancy <[email protected]> Co-authored-by: fridayL <[email protected]> Co-authored-by: chunyu li <[email protected]> Co-authored-by: yuan.wang <[email protected]>
1 parent d2697ec commit 9686810

File tree

11 files changed

+633
-44
lines changed

11 files changed

+633
-44
lines changed

src/memos/mem_reader/multi_model_struct.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ def __init__(self, config: MultiModelStructMemReaderConfig):
3939
parser=None,
4040
)
4141

42+
def _concat_multi_model_memories(
43+
self, all_memory_items: list[TextualMemoryItem]
44+
) -> list[TextualMemoryItem]:
45+
# TODO: concat multi_model_memories
46+
return all_memory_items
47+
4248
@timed
43-
def _process_multi_model_data(self, scene_data_info: MessagesType, info, **kwargs):
49+
def _process_multi_model_data(
50+
self, scene_data_info: MessagesType, info, **kwargs
51+
) -> list[TextualMemoryItem]:
4452
"""
4553
Process multi-model data using MultiModelParser.
4654
@@ -50,23 +58,81 @@ def _process_multi_model_data(self, scene_data_info: MessagesType, info, **kwarg
5058
**kwargs: Additional parameters (mode, etc.)
5159
"""
5260
mode = kwargs.get("mode", "fine")
61+
# Pop custom_tags from info (same as simple_struct.py)
62+
# must pop here, avoid add to info, only used in sync fine mode
63+
custom_tags = info.pop("custom_tags", None) if isinstance(info, dict) else None
5364

5465
# Use MultiModelParser to parse the scene data
5566
# If it's a list, parse each item; otherwise parse as single message
5667
if isinstance(scene_data_info, list):
5768
# Parse each message in the list
5869
all_memory_items = []
5970
for msg in scene_data_info:
60-
items = self.multi_model_parser.parse(msg, info, mode=mode, **kwargs)
71+
items = self.multi_model_parser.parse(msg, info, mode="fast", **kwargs)
6172
all_memory_items.extend(items)
62-
return all_memory_items
73+
fast_memory_items = self._concat_multi_model_memories(all_memory_items)
74+
6375
else:
6476
# Parse as single message
65-
return self.multi_model_parser.parse(scene_data_info, info, mode=mode, **kwargs)
77+
fast_memory_items = self.multi_model_parser.parse(
78+
scene_data_info, info, mode="fast", **kwargs
79+
)
80+
81+
if mode == "fast":
82+
return fast_memory_items
83+
else:
84+
# TODO: parallel call llm and get fine multi model items
85+
# Part A: call llm
86+
fine_memory_items = []
87+
fine_memory_items_string_parser = []
88+
fine_memory_items.extend(fine_memory_items_string_parser)
89+
# Part B: get fine multi model items
90+
91+
for fast_item in fast_memory_items:
92+
sources = fast_item.metadata.sources
93+
for source in sources:
94+
items = self.multi_model_parser.process_transfer(
95+
source, context_items=[fast_item], custom_tags=custom_tags
96+
)
97+
fine_memory_items.extend(items)
98+
logger.warning("Not Implemented Now!")
99+
return fine_memory_items
66100

67101
@timed
68-
def _process_transfer_multi_model_data(self, raw_node: TextualMemoryItem):
69-
raise NotImplementedError
102+
def _process_transfer_multi_model_data(
103+
self,
104+
raw_node: TextualMemoryItem,
105+
custom_tags: list[str] | None = None,
106+
) -> list[TextualMemoryItem]:
107+
"""
108+
Process transfer for multi-model data.
109+
110+
Each source is processed independently by its corresponding parser,
111+
which knows how to rebuild the original message and parse it in fine mode.
112+
"""
113+
sources = raw_node.metadata.sources or []
114+
if not sources:
115+
logger.warning("[MultiModelStruct] No sources found in raw_node")
116+
return []
117+
118+
# Extract info from raw_node (same as simple_struct.py)
119+
info = {
120+
"user_id": raw_node.metadata.user_id,
121+
"session_id": raw_node.metadata.session_id,
122+
**(raw_node.metadata.info or {}),
123+
}
124+
125+
fine_memory_items = []
126+
# Part A: call llm
127+
fine_memory_items_string_parser = []
128+
fine_memory_items.extend(fine_memory_items_string_parser)
129+
# Part B: get fine multi model items
130+
for source in sources:
131+
items = self.multi_model_parser.process_transfer(
132+
source, context_items=[raw_node], info=info, custom_tags=custom_tags
133+
)
134+
fine_memory_items.extend(items)
135+
return fine_memory_items
70136

71137
def get_scene_data_info(self, scene_data: list, type: str) -> list[list[Any]]:
72138
"""
@@ -85,7 +151,7 @@ def get_scene_data_info(self, scene_data: list, type: str) -> list[list[Any]]:
85151

86152
def _read_memory(
87153
self, messages: list[MessagesType], type: str, info: dict[str, Any], mode: str = "fine"
88-
):
154+
) -> list[list[TextualMemoryItem]]:
89155
list_scene_data_info = self.get_scene_data_info(messages, type)
90156

91157
memory_list = []
@@ -106,7 +172,10 @@ def _read_memory(
106172
return memory_list
107173

108174
def fine_transfer_simple_mem(
109-
self, input_memories: list[TextualMemoryItem], type: str
175+
self,
176+
input_memories: list[TextualMemoryItem],
177+
type: str,
178+
custom_tags: list[str] | None = None,
110179
) -> list[list[TextualMemoryItem]]:
111180
if not input_memories:
112181
return []
@@ -116,7 +185,9 @@ def fine_transfer_simple_mem(
116185
# Process Q&A pairs concurrently with context propagation
117186
with ContextThreadPoolExecutor() as executor:
118187
futures = [
119-
executor.submit(self._process_transfer_multi_model_data, scene_data_info)
188+
executor.submit(
189+
self._process_transfer_multi_model_data, scene_data_info, custom_tags
190+
)
120191
for scene_data_info in input_memories
121192
]
122193
for future in concurrent.futures.as_completed(futures):

src/memos/mem_reader/read_multi_model/assistant_parser.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from memos.embedders.base import BaseEmbedder
66
from memos.llms.base import BaseLLM
77
from memos.log import get_logger
8-
from memos.memories.textual.item import TextualMemoryItem
8+
from memos.memories.textual.item import SourceMessage, TextualMemoryItem
99
from memos.types.openai_chat_completion_types import ChatCompletionAssistantMessageParam
1010

11-
from .base import BaseMessageParser
11+
from .base import BaseMessageParser, _extract_text_from_content
1212

1313

1414
logger = get_logger(__name__)
@@ -25,16 +25,45 @@ def __init__(self, embedder: BaseEmbedder, llm: BaseLLM | None = None):
2525
embedder: Embedder for generating embeddings
2626
llm: Optional LLM for fine mode processing
2727
"""
28-
self.embedder = embedder
29-
self.llm = llm
28+
super().__init__(embedder, llm)
29+
30+
def create_source(
31+
self,
32+
message: ChatCompletionAssistantMessageParam,
33+
info: dict[str, Any],
34+
) -> SourceMessage:
35+
"""Create SourceMessage from assistant message."""
36+
if not isinstance(message, dict):
37+
return SourceMessage(type="chat", role="assistant")
38+
39+
content = _extract_text_from_content(message.get("content", ""))
40+
return SourceMessage(
41+
type="chat",
42+
role="assistant",
43+
chat_time=message.get("chat_time"),
44+
message_id=message.get("message_id"),
45+
content=content,
46+
)
47+
48+
def rebuild_from_source(
49+
self,
50+
source: SourceMessage,
51+
) -> ChatCompletionAssistantMessageParam:
52+
"""Rebuild assistant message from SourceMessage."""
53+
return {
54+
"role": "assistant",
55+
"content": source.content or "",
56+
"chat_time": source.chat_time,
57+
"message_id": source.message_id,
58+
}
3059

3160
def parse_fast(
3261
self,
3362
message: ChatCompletionAssistantMessageParam,
3463
info: dict[str, Any],
3564
**kwargs,
3665
) -> list[TextualMemoryItem]:
37-
return []
66+
return super().parse_fast(message, info, **kwargs)
3867

3968
def parse_fine(
4069
self,

src/memos/mem_reader/read_multi_model/base.py

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,125 @@
44
in both fast and fine modes.
55
"""
66

7+
import re
8+
79
from abc import ABC, abstractmethod
810
from typing import Any
911

10-
from memos.memories.textual.item import TextualMemoryItem
12+
from memos import log
13+
from memos.memories.textual.item import (
14+
SourceMessage,
15+
TextualMemoryItem,
16+
TreeNodeTextualMemoryMetadata,
17+
)
18+
19+
20+
logger = log.get_logger(__name__)
21+
22+
23+
def _derive_key(text: str, max_len: int = 80) -> str:
24+
"""Default key when without LLM: first max_len words."""
25+
if not text:
26+
return ""
27+
sent = re.split(r"[。!?!?]\s*|\n", text.strip())[0]
28+
return (sent[:max_len]).strip()
29+
30+
31+
def _extract_text_from_content(content: Any) -> str:
32+
"""
33+
Extract text from message content.
34+
Handles str, list of parts, or None.
35+
"""
36+
if content is None:
37+
return ""
38+
if isinstance(content, str):
39+
return content
40+
if isinstance(content, list):
41+
texts = []
42+
for part in content:
43+
if isinstance(part, dict):
44+
part_type = part.get("type", "")
45+
if part_type == "text":
46+
texts.append(part.get("text", ""))
47+
elif part_type == "file":
48+
file_info = part.get("file", {})
49+
texts.append(file_info.get("file_data") or file_info.get("filename", "[file]"))
50+
else:
51+
texts.append(f"[{part_type}]")
52+
else:
53+
texts.append(str(part))
54+
return " ".join(texts)
55+
return str(content)
1156

1257

1358
class BaseMessageParser(ABC):
1459
"""Base interface for message type parsers."""
1560

61+
def __init__(self, embedder, llm=None):
62+
"""
63+
Initialize BaseMessageParser.
64+
65+
Args:
66+
embedder: Embedder for generating embeddings
67+
llm: Optional LLM for fine mode processing
68+
"""
69+
self.embedder = embedder
70+
self.llm = llm
71+
72+
@abstractmethod
73+
def create_source(
74+
self,
75+
message: Any,
76+
info: dict[str, Any],
77+
) -> SourceMessage | list[SourceMessage]:
78+
"""
79+
Create SourceMessage(s) from the message.
80+
81+
Each parser decides how to create sources:
82+
- Simple messages: return single SourceMessage
83+
- Multimodal messages: return list of SourceMessage (one per part)
84+
85+
Args:
86+
message: The message to create source from
87+
info: Dictionary containing user_id and session_id
88+
89+
Returns:
90+
SourceMessage or list of SourceMessage
91+
"""
92+
1693
@abstractmethod
94+
def rebuild_from_source(
95+
self,
96+
source: SourceMessage,
97+
) -> Any:
98+
"""
99+
Rebuild original message from SourceMessage.
100+
101+
Each parser knows how to reconstruct its own message type.
102+
103+
Args:
104+
source: SourceMessage to rebuild from
105+
106+
Returns:
107+
Rebuilt message in original format
108+
"""
109+
17110
def parse_fast(
18111
self,
19112
message: Any,
20113
info: dict[str, Any],
21114
**kwargs,
22115
) -> list[TextualMemoryItem]:
23116
"""
24-
Parse message in fast mode (no LLM calls, quick processing).
117+
Default parse_fast implementation (equivalent to simple_struct fast mode).
118+
119+
Fast mode logic:
120+
- Extract text content from message
121+
- Determine memory_type based on role (UserMemory for user, LongTermMemory otherwise)
122+
- Create TextualMemoryItem with tags=["mode:fast"]
123+
- No LLM calls, quick processing
124+
125+
Subclasses can override this method for custom behavior.
25126
26127
Args:
27128
message: The message to parse
@@ -31,6 +132,52 @@ def parse_fast(
31132
Returns:
32133
List of TextualMemoryItem objects
33134
"""
135+
if not isinstance(message, dict):
136+
logger.warning(f"[BaseParser] Expected dict, got {type(message)}")
137+
return []
138+
139+
# Extract text content
140+
content = _extract_text_from_content(message.get("content"))
141+
if not content:
142+
return []
143+
144+
# Determine memory_type based on role (equivalent to simple_struct logic)
145+
role = message.get("role", "").strip().lower()
146+
memory_type = "UserMemory" if role == "user" else "LongTermMemory"
147+
148+
# Create source(s) using parser's create_source method
149+
sources = self.create_source(message, info)
150+
if isinstance(sources, SourceMessage):
151+
sources = [sources]
152+
elif not sources:
153+
return []
154+
155+
# Extract info fields
156+
info_ = info.copy()
157+
user_id = info_.pop("user_id", "")
158+
session_id = info_.pop("session_id", "")
159+
160+
# Create memory item (equivalent to _make_memory_item)
161+
memory_item = TextualMemoryItem(
162+
memory=content,
163+
metadata=TreeNodeTextualMemoryMetadata(
164+
user_id=user_id,
165+
session_id=session_id,
166+
memory_type=memory_type,
167+
status="activated",
168+
tags=["mode:fast"],
169+
key=_derive_key(content),
170+
embedding=self.embedder.embed([content])[0],
171+
usage=[],
172+
sources=sources,
173+
background="",
174+
confidence=0.99,
175+
type="fact",
176+
info=info_,
177+
),
178+
)
179+
180+
return [memory_item]
34181

35182
@abstractmethod
36183
def parse_fine(

0 commit comments

Comments
 (0)