Skip to content

Commit 4a4746e

Browse files
author
黑布林
committed
Merge branch 'dev' of github.com:MemTensor/MemOS into dev
2 parents b4fbfde + aef6bcf commit 4a4746e

File tree

23 files changed

+873
-69
lines changed

23 files changed

+873
-69
lines changed

docker/.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ OLLAMA_API_BASE=http://localhost:11434 # required when backend=ollama
4747
MOS_RERANKER_BACKEND=http_bge # http_bge | http_bge_strategy | cosine_local
4848
MOS_RERANKER_URL=http://localhost:8001 # required when backend=http_bge*
4949
MOS_RERANKER_MODEL=bge-reranker-v2-m3 # siliconflow → use BAAI/bge-reranker-v2-m3
50-
MOS_RERANKER_HEADERS_EXTRA= # extra headers, JSON string
50+
MOS_RERANKER_HEADERS_EXTRA= # extra headers, JSON string, e.g. {"Authorization":"Bearer your_token"}
5151
MOS_RERANKER_STRATEGY=single_turn
5252
MOS_RERANK_SOURCE= # optional rerank scope, e.g., history/stream/custom
5353

@@ -93,6 +93,9 @@ NEO4J_DB_NAME=neo4j # required for shared-db mode
9393
MOS_NEO4J_SHARED_DB=false
9494
QDRANT_HOST=localhost
9595
QDRANT_PORT=6333
96+
# For Qdrant Cloud / remote endpoint (takes priority if set):
97+
QDRANT_URL=your_qdrant_url
98+
QDRANT_API_KEY=your_qdrant_key
9699
MILVUS_URI=http://localhost:19530 # required when ENABLE_PREFERENCE_MEMORY=true
97100
MILVUS_USER_NAME=root # same as above
98101
MILVUS_PASSWORD=12345678 # same as above

docs/product-api-tests.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
## Product API smoke tests (local 0.0.0.0:8001)
2+
3+
Source: https://github.com/MemTensor/MemOS/issues/518
4+
5+
### Prerequisites
6+
- Service is running: `python -m uvicorn memos.api.server_api:app --host 0.0.0.0 --port 8001`
7+
- `.env` is configured for Redis, embeddings, and the vector DB (current test setup: Redis reachable, Qdrant Cloud connected).
8+
9+
### 1) /product/add
10+
- Purpose: Write a memory (sync/async).
11+
- Example request (sync):
12+
13+
```bash
14+
curl -s -X POST http://127.0.0.1:8001/product/add \
15+
-H 'Content-Type: application/json' \
16+
-d '{
17+
"user_id": "tester",
18+
"mem_cube_id": "default_cube",
19+
"memory_content": "Apple is a fruit rich in fiber.",
20+
"async_mode": "sync"
21+
}'
22+
```
23+
24+
- Observed result: `200`, message: "Memory added successfully", returns the written `memory_id` and related info.
25+
26+
### 2) /product/get_all
27+
- Purpose: List all memories for the user/type to confirm writes.
28+
- Example request:
29+
30+
```bash
31+
curl -s -X POST http://127.0.0.1:8001/product/get_all \
32+
-H 'Content-Type: application/json' \
33+
-d '{
34+
"user_id": "tester",
35+
"memory_type": "text_mem",
36+
"mem_cube_ids": ["default_cube"]
37+
}'
38+
```
39+
40+
- Observed result: `200`, shows the recently written apple memories (WorkingMemory/LongTermMemory/UserMemory present, `vector_sync=success`).
41+
42+
### 3) /product/search
43+
- Purpose: Vector search memories.
44+
- Example request:
45+
46+
```bash
47+
curl -s -X POST http://127.0.0.1:8001/product/search \
48+
-H 'Content-Type: application/json' \
49+
-d '{
50+
"query": "What fruit is rich in fiber?",
51+
"user_id": "tester",
52+
"mem_cube_id": "default_cube",
53+
"top_k": 5,
54+
"pref_top_k": 3,
55+
"include_preference": false
56+
}'
57+
```
58+
59+
- Observed result: previously returned 400 because payload indexes (e.g., `vector_sync`) were missing in Qdrant. Index creation is now automatic during Qdrant initialization (memory_type/status/vector_sync/user_name).
60+
- If results are empty or errors persist, verify indexes exist (auto-created on restart) or recreate/clean the collection.
61+
62+
### Notes / Next steps
63+
- `/product/add` and `/product/get_all` are healthy.
64+
- `/product/search` still returns empty results even with vectors present; likely related to search filters or vector retrieval.
65+
- Suggested follow-ups: inspect `SearchHandler` flow, filter conditions (user_id/session/cube_name), and vector DB search calls; capture logs or compare with direct `VecDBFactory.search` calls.

src/memos/api/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,9 @@ def get_neo4j_community_config(user_id: str | None = None) -> dict[str, Any]:
500500
"distance_metric": "cosine",
501501
"host": os.getenv("QDRANT_HOST", "localhost"),
502502
"port": int(os.getenv("QDRANT_PORT", "6333")),
503+
"path": os.getenv("QDRANT_PATH"),
504+
"url": os.getenv("QDRANT_URL"),
505+
"api_key": os.getenv("QDRANT_API_KEY"),
503506
},
504507
},
505508
}

src/memos/configs/vec_db.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ class QdrantVecDBConfig(BaseVecDBConfig):
2727
host: str | None = Field(default=None, description="Host for Qdrant")
2828
port: int | None = Field(default=None, description="Port for Qdrant")
2929
path: str | None = Field(default=None, description="Path for Qdrant")
30+
url: str | None = Field(default=None, description="Qdrant Cloud/remote endpoint URL")
31+
api_key: str | None = Field(default=None, description="Qdrant Cloud API key")
3032

3133
@model_validator(mode="after")
3234
def set_default_path(self):
33-
if all(x is None for x in (self.host, self.port, self.path)):
35+
# Only fall back to embedded/local path when no remote host/port/path/url is provided.
36+
if all(x is None for x in (self.host, self.port, self.path, self.url)):
3437
logger.warning(
3538
"No host, port, or path provided for Qdrant. Defaulting to local path: %s",
3639
settings.MEMOS_DIR / "qdrant",

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,

0 commit comments

Comments
 (0)