Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9fdbcf3
Dev (#496)
CarltonXiang Nov 21, 2025
82883a3
docs: update .env.example with comprehensive variables and comments
fancyboi999 Nov 21, 2025
439ed49
hotfix:hotfix
fridayL Nov 21, 2025
2b6dc7e
hotfix:hotfix (#513)
fridayL Nov 21, 2025
39a7b34
test: add routers api
CaralHsi Nov 22, 2025
11e3467
Merge branch 'main' into docs/update-env-example
CaralHsi Nov 24, 2025
5d434ea
docs: update .env.example with comprehensive variables and comments (…
CaralHsi Nov 24, 2025
93d3aa4
feat: add multi-cube feature to chat
CaralHsi Nov 24, 2025
23aeacb
refactor: define ChatRequest and related backups
CaralHsi Nov 24, 2025
9dac1ae
fix: func name in product models
CaralHsi Nov 24, 2025
dd02ba9
feat: add 'task_id' in AddRequest(for get async add status later); re…
CaralHsi Nov 24, 2025
39b25c7
feat: add add-mode in API AddRequest
CaralHsi Nov 24, 2025
047469a
add server router add api example
CaralHsi Nov 24, 2025
2278210
Merge branch 'dev' into feat/chat_router
CaralHsi Nov 24, 2025
294406a
feat: update server router example
CaralHsi Nov 25, 2025
8a68fce
feat: tiny update for simple struct: support MessageType only for inp…
CaralHsi Nov 25, 2025
40ec227
feat: add _coerce_scene_data in simple memreader to transform sceneda…
CaralHsi Nov 25, 2025
221df97
feat: add multi-model reader
CaralHsi Nov 25, 2025
a93170f
Merge branch 'dev' of github.com:MemTensor/MemOS into feat/chat_router
CaralHsi Nov 25, 2025
b6c0d17
Merge branch 'feat/chat_router' of github.com:CaralHsi/MemOSRealPubli…
CaralHsi Nov 25, 2025
088dc83
feat: init multi-model; update _coerce_scene_data
CaralHsi Nov 26, 2025
f427693
feat: add chat_time in coerce_scene_data
CaralHsi Nov 26, 2025
b73a00f
refactor: tiny adjust function name and remove useless func
CaralHsi Nov 26, 2025
f5349ec
feat: adjuct doc process in simple_struct mem-reader
CaralHsi Nov 26, 2025
f7c9e24
refactor: rename _get_scene_data_info -> get_scene_data_info
CaralHsi Nov 26, 2025
e79ba48
feat: finish simple reader
CaralHsi Nov 26, 2025
6f6561f
format: update example reader: just better display
CaralHsi Nov 26, 2025
245f151
feat: update test coarse memory
CaralHsi Nov 26, 2025
a16e82a
feat: add MultiModelStruct MemReader
CaralHsi Nov 26, 2025
642cf4f
feat: update multi_model_struct, simplify and as a child from SimpleS…
CaralHsi Nov 26, 2025
f0f6452
feat: update multi_model_struct parser
CaralHsi Nov 26, 2025
c299e59
merge dev solve confict
Nov 26, 2025
d807bf9
fix: test bug
CaralHsi Nov 26, 2025
754efaa
fix: conflict
CaralHsi Nov 26, 2025
91efb50
Merge branch 'feat/chat_router' of github.com:CaralHsi/MemOSRealPubli…
CaralHsi Nov 26, 2025
d01b87c
feat: add base parse
CaralHsi Nov 27, 2025
fceebbe
feat: add base fast parser
CaralHsi Nov 27, 2025
f06dfba
feat: update multi_model_struct
CaralHsi Nov 27, 2025
c1c7f4d
feat: modify sources
CaralHsi Nov 27, 2025
0701e63
feat: fix some parameters in multi-model parser
CaralHsi Nov 27, 2025
869533e
fix: conflict
CaralHsi Nov 27, 2025
3e9ac21
fix: conflict
CaralHsi Nov 27, 2025
2ddbd8c
fix: fine_memory_items bugs
CaralHsi Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 80 additions & 9 deletions src/memos/mem_reader/multi_model_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ def __init__(self, config: MultiModelStructMemReaderConfig):
parser=None,
)

def _concat_multi_model_memories(
self, all_memory_items: list[TextualMemoryItem]
) -> list[TextualMemoryItem]:
# TODO: concat multi_model_memories
return all_memory_items

@timed
def _process_multi_model_data(self, scene_data_info: MessagesType, info, **kwargs):
def _process_multi_model_data(
self, scene_data_info: MessagesType, info, **kwargs
) -> list[TextualMemoryItem]:
"""
Process multi-model data using MultiModelParser.

Expand All @@ -50,23 +58,81 @@ def _process_multi_model_data(self, scene_data_info: MessagesType, info, **kwarg
**kwargs: Additional parameters (mode, etc.)
"""
mode = kwargs.get("mode", "fine")
# Pop custom_tags from info (same as simple_struct.py)
# must pop here, avoid add to info, only used in sync fine mode
custom_tags = info.pop("custom_tags", None) if isinstance(info, dict) else None

# Use MultiModelParser to parse the scene data
# If it's a list, parse each item; otherwise parse as single message
if isinstance(scene_data_info, list):
# Parse each message in the list
all_memory_items = []
for msg in scene_data_info:
items = self.multi_model_parser.parse(msg, info, mode=mode, **kwargs)
items = self.multi_model_parser.parse(msg, info, mode="fast", **kwargs)
all_memory_items.extend(items)
return all_memory_items
fast_memory_items = self._concat_multi_model_memories(all_memory_items)

else:
# Parse as single message
return self.multi_model_parser.parse(scene_data_info, info, mode=mode, **kwargs)
fast_memory_items = self.multi_model_parser.parse(
scene_data_info, info, mode="fast", **kwargs
)

if mode == "fast":
return fast_memory_items
else:
# TODO: parallel call llm and get fine multi model items
# Part A: call llm
fine_memory_items = []
fine_memory_items_string_parser = []
fine_memory_items.extend(fine_memory_items_string_parser)
# Part B: get fine multi model items

for fast_item in fast_memory_items:
sources = fast_item.metadata.sources
for source in sources:
items = self.multi_model_parser.process_transfer(
source, context_items=[fast_item], custom_tags=custom_tags
)
fine_memory_items.extend(items)
logger.warning("Not Implemented Now!")
return fine_memory_items

@timed
def _process_transfer_multi_model_data(self, raw_node: TextualMemoryItem):
raise NotImplementedError
def _process_transfer_multi_model_data(
self,
raw_node: TextualMemoryItem,
custom_tags: list[str] | None = None,
) -> list[TextualMemoryItem]:
"""
Process transfer for multi-model data.

Each source is processed independently by its corresponding parser,
which knows how to rebuild the original message and parse it in fine mode.
"""
sources = raw_node.metadata.sources or []
if not sources:
logger.warning("[MultiModelStruct] No sources found in raw_node")
return []

# Extract info from raw_node (same as simple_struct.py)
info = {
"user_id": raw_node.metadata.user_id,
"session_id": raw_node.metadata.session_id,
**(raw_node.metadata.info or {}),
}

fine_memory_items = []
# Part A: call llm
fine_memory_items_string_parser = []
fine_memory_items.extend(fine_memory_items_string_parser)
# Part B: get fine multi model items
for source in sources:
items = self.multi_model_parser.process_transfer(
source, context_items=[raw_node], info=info, custom_tags=custom_tags
)
fine_memory_items.extend(items)
return fine_memory_items

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

def _read_memory(
self, messages: list[MessagesType], type: str, info: dict[str, Any], mode: str = "fine"
):
) -> list[list[TextualMemoryItem]]:
list_scene_data_info = self.get_scene_data_info(messages, type)

memory_list = []
Expand All @@ -106,7 +172,10 @@ def _read_memory(
return memory_list

def fine_transfer_simple_mem(
self, input_memories: list[TextualMemoryItem], type: str
self,
input_memories: list[TextualMemoryItem],
type: str,
custom_tags: list[str] | None = None,
) -> list[list[TextualMemoryItem]]:
if not input_memories:
return []
Expand All @@ -116,7 +185,9 @@ def fine_transfer_simple_mem(
# Process Q&A pairs concurrently with context propagation
with ContextThreadPoolExecutor() as executor:
futures = [
executor.submit(self._process_transfer_multi_model_data, scene_data_info)
executor.submit(
self._process_transfer_multi_model_data, scene_data_info, custom_tags
)
for scene_data_info in input_memories
]
for future in concurrent.futures.as_completed(futures):
Expand Down
39 changes: 34 additions & 5 deletions src/memos/mem_reader/read_multi_model/assistant_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from memos.embedders.base import BaseEmbedder
from memos.llms.base import BaseLLM
from memos.log import get_logger
from memos.memories.textual.item import TextualMemoryItem
from memos.memories.textual.item import SourceMessage, TextualMemoryItem
from memos.types.openai_chat_completion_types import ChatCompletionAssistantMessageParam

from .base import BaseMessageParser
from .base import BaseMessageParser, _extract_text_from_content


logger = get_logger(__name__)
Expand All @@ -25,16 +25,45 @@ def __init__(self, embedder: BaseEmbedder, llm: BaseLLM | None = None):
embedder: Embedder for generating embeddings
llm: Optional LLM for fine mode processing
"""
self.embedder = embedder
self.llm = llm
super().__init__(embedder, llm)

def create_source(
self,
message: ChatCompletionAssistantMessageParam,
info: dict[str, Any],
) -> SourceMessage:
"""Create SourceMessage from assistant message."""
if not isinstance(message, dict):
return SourceMessage(type="chat", role="assistant")

content = _extract_text_from_content(message.get("content", ""))
return SourceMessage(
type="chat",
role="assistant",
chat_time=message.get("chat_time"),
message_id=message.get("message_id"),
content=content,
)

def rebuild_from_source(
self,
source: SourceMessage,
) -> ChatCompletionAssistantMessageParam:
"""Rebuild assistant message from SourceMessage."""
return {
"role": "assistant",
"content": source.content or "",
"chat_time": source.chat_time,
"message_id": source.message_id,
}

def parse_fast(
self,
message: ChatCompletionAssistantMessageParam,
info: dict[str, Any],
**kwargs,
) -> list[TextualMemoryItem]:
return []
return super().parse_fast(message, info, **kwargs)

def parse_fine(
self,
Expand Down
151 changes: 149 additions & 2 deletions src/memos/mem_reader/read_multi_model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,125 @@
in both fast and fine modes.
"""

import re

from abc import ABC, abstractmethod
from typing import Any

from memos.memories.textual.item import TextualMemoryItem
from memos import log
from memos.memories.textual.item import (
SourceMessage,
TextualMemoryItem,
TreeNodeTextualMemoryMetadata,
)


logger = log.get_logger(__name__)


def _derive_key(text: str, max_len: int = 80) -> str:
"""Default key when without LLM: first max_len words."""
if not text:
return ""
sent = re.split(r"[。!?!?]\s*|\n", text.strip())[0]
return (sent[:max_len]).strip()


def _extract_text_from_content(content: Any) -> str:
"""
Extract text from message content.
Handles str, list of parts, or None.
"""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
texts = []
for part in content:
if isinstance(part, dict):
part_type = part.get("type", "")
if part_type == "text":
texts.append(part.get("text", ""))
elif part_type == "file":
file_info = part.get("file", {})
texts.append(file_info.get("file_data") or file_info.get("filename", "[file]"))
else:
texts.append(f"[{part_type}]")
else:
texts.append(str(part))
return " ".join(texts)
return str(content)


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

def __init__(self, embedder, llm=None):
"""
Initialize BaseMessageParser.

Args:
embedder: Embedder for generating embeddings
llm: Optional LLM for fine mode processing
"""
self.embedder = embedder
self.llm = llm

@abstractmethod
def create_source(
self,
message: Any,
info: dict[str, Any],
) -> SourceMessage | list[SourceMessage]:
"""
Create SourceMessage(s) from the message.

Each parser decides how to create sources:
- Simple messages: return single SourceMessage
- Multimodal messages: return list of SourceMessage (one per part)

Args:
message: The message to create source from
info: Dictionary containing user_id and session_id

Returns:
SourceMessage or list of SourceMessage
"""

@abstractmethod
def rebuild_from_source(
self,
source: SourceMessage,
) -> Any:
"""
Rebuild original message from SourceMessage.

Each parser knows how to reconstruct its own message type.

Args:
source: SourceMessage to rebuild from

Returns:
Rebuilt message in original format
"""

def parse_fast(
self,
message: Any,
info: dict[str, Any],
**kwargs,
) -> list[TextualMemoryItem]:
"""
Parse message in fast mode (no LLM calls, quick processing).
Default parse_fast implementation (equivalent to simple_struct fast mode).

Fast mode logic:
- Extract text content from message
- Determine memory_type based on role (UserMemory for user, LongTermMemory otherwise)
- Create TextualMemoryItem with tags=["mode:fast"]
- No LLM calls, quick processing

Subclasses can override this method for custom behavior.

Args:
message: The message to parse
Expand All @@ -31,6 +132,52 @@ def parse_fast(
Returns:
List of TextualMemoryItem objects
"""
if not isinstance(message, dict):
logger.warning(f"[BaseParser] Expected dict, got {type(message)}")
return []

# Extract text content
content = _extract_text_from_content(message.get("content"))
if not content:
return []

# Determine memory_type based on role (equivalent to simple_struct logic)
role = message.get("role", "").strip().lower()
memory_type = "UserMemory" if role == "user" else "LongTermMemory"

# Create source(s) using parser's create_source method
sources = self.create_source(message, info)
if isinstance(sources, SourceMessage):
sources = [sources]
elif not sources:
return []

# Extract info fields
info_ = info.copy()
user_id = info_.pop("user_id", "")
session_id = info_.pop("session_id", "")

# Create memory item (equivalent to _make_memory_item)
memory_item = TextualMemoryItem(
memory=content,
metadata=TreeNodeTextualMemoryMetadata(
user_id=user_id,
session_id=session_id,
memory_type=memory_type,
status="activated",
tags=["mode:fast"],
key=_derive_key(content),
embedding=self.embedder.embed([content])[0],
usage=[],
sources=sources,
background="",
confidence=0.99,
type="fact",
info=info_,
),
)

return [memory_item]

@abstractmethod
def parse_fine(
Expand Down
Loading
Loading