Skip to content

Commit 5531bd7

Browse files
authored
V2.0.16 (#1753)
## Description Please include a summary of the change, the problem it solves, the implementation approach, and relevant context. List any dependencies required for this change. Related Issue (Required): Fixes #issue_number ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (does not change functionality, e.g. code style improvements, linting) - [ ] Documentation update ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - [ ] Unit Test - [ ] Test Script Or Test Steps (please provide) - [ ] Pipeline Automated API Test (please provide) ## Checklist - [ ] I have performed a self-review of my own code | 我已自行检查了自己的代码 - [ ] I have commented my code in hard-to-understand areas | 我已在难以理解的地方对代码进行了注释 - [ ] I have added tests that prove my fix is effective or that my feature works | 我已添加测试以证明我的修复有效或功能正常 - [ ] I have created related documentation issue/PR in [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) (if applicable) | 我已在 [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) 中创建了相关的文档 issue/PR(如果适用) - [ ] I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用) - [ ] I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人 ## Reviewer Checklist - [ ] closes #xxxx (Replace xxxx with the GitHub issue number) - [ ] Made sure Checks passed - [ ] Tests have been provided
2 parents e0ef84d + b830d51 commit 5531bd7

15 files changed

Lines changed: 757 additions & 17 deletions

File tree

examples/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# MemOS Examples
2+
3+
This directory contains runnable examples for MemOS modules, memory types, API
4+
usage, and integrations. Run examples from the repository root so relative
5+
configuration and data paths resolve correctly.
6+
7+
## Running Examples
8+
9+
Install the project dependencies first, then run a script directly:
10+
11+
```bash
12+
python examples/mem_cube/load_cube.py
13+
```
14+
15+
Some examples require extra services or credentials, such as Neo4j, Redis,
16+
model provider API keys, or local model backends. Check the script and matching
17+
documentation before running those examples.
18+
19+
For guided walkthroughs, see the [examples guide](../docs/en/open_source/getting_started/examples.md)
20+
and the module documentation under [docs/en/open_source/modules](../docs/en/open_source/modules).
21+
22+
## Directory Overview
23+
24+
| Directory | Purpose |
25+
| --- | --- |
26+
| `api` | Server router and product API usage examples. |
27+
| `basic_modules` | Focused examples for embedders, LLMs, chunkers, rerankers, graph databases, and textual memory helpers. |
28+
| `core_memories` | Examples for core memory backends such as general, naive, preference, tree textual, KV cache, and vLLM KV cache memory. |
29+
| `data` | Shared sample configs, memory cube data, and input assets used by other examples. |
30+
| `dream` | End-to-end dream pipeline example. |
31+
| `extras` | Additional standalone demos that do not fit the main module categories. |
32+
| `mem_agent` | Agent-oriented examples, including deep search usage. |
33+
| `mem_chat` | Chat examples that combine generated cubes and explicit memory. |
34+
| `mem_cube` | MemCube load, dump, and legacy remote or lazy loading examples. |
35+
| `mem_feedback` | Examples for memory feedback workflows. |
36+
| `mem_mcp` | FastMCP server and client examples for MemOS integrations. |
37+
| `mem_reader` | MemReader parser, builder, sample, and runner demos for text, files, images, and messages. |
38+
| `mem_scheduler` | Scheduler examples for Redis-backed asynchronous memory workflows. |
39+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
##############################################################################
55

66
name = "MemoryOS"
7-
version = "2.0.15"
7+
version = "2.0.16"
88
description = "Intelligence Begins with Memory"
99
license = {text = "Apache-2.0"}
1010
readme = "README.md"

src/memos/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.0.15"
1+
__version__ = "2.0.16"
22

33
from memos.configs.mem_cube import GeneralMemCubeConfig
44
from memos.configs.mem_os import MOSConfig

src/memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib
12
import json
23
import re
34

@@ -95,6 +96,7 @@ def find_project_root(marker=".git"):
9596

9697
class StopwordManager:
9798
_stopwords = None
99+
_search_stopwords = None
98100

99101
@classmethod
100102
def _load_stopwords(cls):
@@ -108,6 +110,121 @@ def _load_stopwords(cls):
108110
cls._stopwords = stopwords
109111
return stopwords
110112

113+
@classmethod
114+
def _load_search_stopwords(cls):
115+
"""load stopwords used by search keyword extraction"""
116+
if cls._search_stopwords is not None:
117+
return cls._search_stopwords
118+
119+
search_query_stop_words = {
120+
"用户",
121+
"帮我",
122+
"请",
123+
"查询",
124+
"一下",
125+
"一点",
126+
"是否",
127+
"可以",
128+
"需要",
129+
"现在",
130+
"当前",
131+
"官方",
132+
"相关",
133+
"历史记录",
134+
"输出",
135+
"不要",
136+
"要求",
137+
"信息",
138+
"内容",
139+
"平台",
140+
"时间",
141+
"工作日",
142+
"发送时间",
143+
"上午",
144+
"下午",
145+
"晚上",
146+
"深夜",
147+
"周一",
148+
"周二",
149+
"周三",
150+
"周四",
151+
"周五",
152+
"周六",
153+
"周日",
154+
"图片",
155+
"私聊",
156+
"群聊",
157+
"群名",
158+
"今天",
159+
"谢谢",
160+
"好好",
161+
"正常",
162+
"检查一下",
163+
"不了",
164+
"发一",
165+
"上去",
166+
"有点",
167+
"问题",
168+
"事情",
169+
"东西",
170+
"情况",
171+
"习惯",
172+
"地点",
173+
"场面",
174+
"进行",
175+
"根据",
176+
"推荐",
177+
"提供",
178+
"帮助",
179+
"告诉",
180+
"觉得",
181+
"感觉",
182+
"比较",
183+
"相比",
184+
"调到",
185+
"调低",
186+
"选择",
187+
"选在",
188+
"面谈",
189+
"谈崩",
190+
"见证",
191+
"应该",
192+
"可能",
193+
"看看",
194+
"知道",
195+
"记得",
196+
"喜欢",
197+
"being",
198+
"doing",
199+
"if",
200+
"then",
201+
"else",
202+
"without",
203+
"from",
204+
"please",
205+
"tell",
206+
"about",
207+
"best",
208+
"way",
209+
"handle",
210+
"help",
211+
"what",
212+
"when",
213+
"where",
214+
"why",
215+
"how",
216+
"can",
217+
"could",
218+
"would",
219+
"should",
220+
"need",
221+
"hello",
222+
"hi",
223+
"hey",
224+
}
225+
cls._search_stopwords = cls._load_stopwords() | search_query_stop_words
226+
return cls._search_stopwords
227+
111228
@classmethod
112229
def _load_default_stopwords(cls):
113230
"""load stop words"""
@@ -358,6 +475,12 @@ def is_stopword(cls, word):
358475
cls._load_stopwords()
359476
return word in cls._stopwords
360477

478+
@classmethod
479+
def is_search_stopword(cls, word):
480+
if cls._search_stopwords is None:
481+
cls._load_search_stopwords()
482+
return word in cls._search_stopwords or word.lower() in cls._search_stopwords
483+
361484

362485
class FastTokenizer:
363486
def __init__(self, use_jieba=True, use_stopwords=True):
@@ -373,6 +496,10 @@ def tokenize_mixed(self, text, **kwargs):
373496
else:
374497
return self._tokenize_english(text)
375498

499+
def tokenize_english(self, text):
500+
"""Tokenize text as English without language auto-detection."""
501+
return self._tokenize_english(text)
502+
376503
def _is_chinese(self, text):
377504
"""check if chinese"""
378505
chinese_chars = sum(1 for char in text if "\u4e00" <= char <= "\u9fff")
@@ -385,7 +512,7 @@ def _is_chinese(self, text):
385512
)
386513
def _tokenize_chinese(self, text):
387514
"""split zh jieba"""
388-
import jieba
515+
jieba = importlib.import_module("jieba")
389516

390517
tokens = jieba.lcut(text) if self.use_jieba else list(text)
391518
tokens = [token.strip() for token in tokens if token.strip()]
@@ -395,7 +522,7 @@ def _tokenize_chinese(self, text):
395522
return tokens
396523

397524
def _tokenize_english(self, text):
398-
"""split zh regex"""
525+
"""split en regex"""
399526
tokens = re.findall(r"\b[a-zA-Z0-9]+\b", text.lower())
400527
if self.use_stopwords:
401528
return self.stopword_manager.filter_words(tokens)

src/memos/memories/textual/tree_text_memory/retrieve/searcher.py

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import importlib
23
import re
34
import traceback
45

@@ -13,6 +14,7 @@
1314
from memos.memories.textual.tree_text_memory.retrieve.bm25_util import EnhancedBM25
1415
from memos.memories.textual.tree_text_memory.retrieve.retrieve_utils import (
1516
FastTokenizer,
17+
StopwordManager,
1618
cosine_similarity_matrix,
1719
detect_lang,
1820
find_best_unrelated_subgroup,
@@ -33,7 +35,8 @@
3335

3436

3537
logger = get_logger(__name__)
36-
KEYWORD_EXTRACT_TOP_K = 12
38+
KEYWORD_EXTRACT_TOP_K = 3
39+
KEYWORD_ALLOW_POS = ("n", "nr", "nrt", "ns", "nt", "nz", "vn", "v", "t", "eng", "m")
3740
COT_DICT = {
3841
"fine": {"en": COT_PROMPT, "zh": COT_PROMPT_ZH},
3942
"fast": {"en": SIMPLE_COT_PROMPT, "zh": SIMPLE_COT_PROMPT_ZH},
@@ -516,27 +519,85 @@ def _require_keyword_user_name(user_name: str | None) -> str:
516519
)
517520
return normalized_user_name
518521

522+
@staticmethod
523+
def _is_keyword_stopword(term: str) -> bool:
524+
normalized = term.strip()
525+
return not normalized or StopwordManager.is_search_stopword(normalized)
526+
527+
@staticmethod
528+
def _normalize_keyword_term(term: str) -> str:
529+
normalized = str(term).strip()
530+
if re.fullmatch(r"[A-Za-z][A-Za-z0-9]*(?:[._+\-/][A-Za-z0-9]+)*", normalized):
531+
return normalized.lower()
532+
return normalized
533+
534+
@staticmethod
535+
def _keyword_extract_top_k(query: str, language: str) -> int:
536+
cleaned_query = query.strip()
537+
if not cleaned_query:
538+
return 0
539+
if len(cleaned_query) <= 12:
540+
return 1
541+
if language != "zh":
542+
token_count = len(re.findall(r"\b[a-zA-Z0-9]+\b", cleaned_query))
543+
return 2 if token_count <= 8 else KEYWORD_EXTRACT_TOP_K
544+
if len(cleaned_query) <= 120:
545+
return 2
546+
return KEYWORD_EXTRACT_TOP_K
547+
548+
@classmethod
549+
def _rank_english_keyword_terms(cls, terms: list[str]) -> list[str]:
550+
term_stats: dict[str, dict[str, int | str]] = {}
551+
for index, term in enumerate(terms):
552+
normalized_term = cls._normalize_keyword_term(term)
553+
if cls._is_keyword_stopword(normalized_term):
554+
continue
555+
key = normalized_term.lower()
556+
if key not in term_stats:
557+
term_stats[key] = {"term": normalized_term, "index": index, "count": 0}
558+
term_stats[key]["count"] = int(term_stats[key]["count"]) + 1
559+
560+
def score(item: tuple[str, dict[str, int | str]]) -> tuple[float, int]:
561+
_, data = item
562+
term = str(data["term"])
563+
count = int(data["count"])
564+
term_score = count * 3.0 + min(len(term), 16) * 0.1
565+
if any(ch.isdigit() for ch in term):
566+
term_score += 1.0
567+
if len(term) <= 2:
568+
term_score -= 0.5
569+
return (-term_score, int(data["index"]))
570+
571+
return [str(data["term"]) for _, data in sorted(term_stats.items(), key=score)]
572+
519573
def _extract_weighted_keyword_terms(self, query: str) -> list[str]:
520-
if detect_lang(query) == "zh":
521-
import jieba.analyse
574+
language = detect_lang(query)
575+
keyword_top_k = self._keyword_extract_top_k(query, language)
576+
if keyword_top_k <= 0:
577+
return []
578+
579+
if language == "zh":
580+
jieba_analyse = importlib.import_module("jieba.analyse")
522581

523-
weighted_terms = jieba.analyse.extract_tags(query, topK=KEYWORD_EXTRACT_TOP_K)
582+
weighted_terms = jieba_analyse.extract_tags(
583+
query,
584+
topK=keyword_top_k,
585+
allowPOS=KEYWORD_ALLOW_POS,
586+
)
524587
else:
525-
weighted_terms = []
526-
if self.tokenizer:
527-
weighted_terms = self.tokenizer.tokenize_mixed(query)
528-
else:
529-
weighted_terms = re.findall(r"\b[a-zA-Z0-9]+\b", query.lower())
588+
tokenizer = self.tokenizer or FastTokenizer()
589+
weighted_terms = self._rank_english_keyword_terms(tokenizer.tokenize_english(query))
530590

531591
query_words: list[str] = []
532592
seen_words: set[str] = set()
533593
for term in weighted_terms:
534-
normalized_term = str(term).strip()
535-
if not normalized_term or normalized_term in seen_words:
594+
normalized_term = self._normalize_keyword_term(term)
595+
dedupe_key = normalized_term.lower()
596+
if self._is_keyword_stopword(normalized_term) or dedupe_key in seen_words:
536597
continue
537-
seen_words.add(normalized_term)
598+
seen_words.add(dedupe_key)
538599
query_words.append(normalized_term)
539-
if len(query_words) >= KEYWORD_EXTRACT_TOP_K:
600+
if len(query_words) >= keyword_top_k:
540601
break
541602
return query_words
542603

tests/mem_feedback/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

tests/mem_feedback/test_base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from memos.mem_feedback.base import BaseMemFeedback
2+
from tests.utils import check_module_base_class
3+
4+
5+
def test_base_mem_feedback_class_contract():
6+
check_module_base_class(BaseMemFeedback)

0 commit comments

Comments
 (0)