Skip to content

Commit cadd397

Browse files
committed
Merge remote-tracking branch 'upstream/main' into agent/resolve-pr-1774-conflict
2 parents 3942ef3 + bed833f commit cadd397

38 files changed

Lines changed: 2920 additions & 157 deletions

.github/workflows/hermes-plugin-publish.yml

Lines changed: 0 additions & 134 deletions
This file was deleted.

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: 4 additions & 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"
@@ -62,6 +62,9 @@ issues = "https://github.com/MemTensor/MemOS/issues"
6262
[project.scripts]
6363
memos = "memos.cli:main"
6464

65+
[project.entry-points."memos.plugins"]
66+
dream = "memos.dream:CommunityDreamPlugin"
67+
6568
[project.optional-dependencies]
6669
# These are optional dependencies for various features of MemoryOS.
6770
# Developers install: `poetry install --extras <feature>`. e.g., `poetry install --extras general-mem`

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/api/handlers/search_handler.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77

88
import copy
99
import math
10+
import os
1011

12+
from contextlib import suppress
1113
from typing import Any
1214

1315
from memos.api.handlers.base_handler import BaseHandler, HandlerDependencies
1416
from memos.api.handlers.formatters_handler import rerank_knowledge_mem
1517
from memos.api.product_models import APISearchRequest, SearchResponse
18+
from memos.dream.contextualization import CONTEXT_MEMORY_TYPE
1619
from memos.log import get_logger
1720
from memos.memories.textual.tree_text_memory.retrieve.retrieve_utils import (
1821
cosine_similarity_matrix,
@@ -25,6 +28,20 @@
2528

2629
logger = get_logger(__name__)
2730

31+
_ENV_CONTEXT_RECALL = "MEMOS_DREAM_CONTEXT_RECALL"
32+
_ENV_CONTEXT_RECALL_TOP_K = "MEMOS_DREAM_CONTEXT_RECALL_TOP_K"
33+
_DEFAULT_CONTEXT_RECALL_TOP_K = 2
34+
35+
36+
def _env_enabled(name: str, default: str = "off") -> bool:
37+
return os.getenv(name, default).strip().lower() not in {"0", "false", "no", "off"}
38+
39+
40+
def _env_int(name: str, default: int) -> int:
41+
with suppress(TypeError, ValueError):
42+
return int(os.getenv(name, str(default)))
43+
return default
44+
2845

2946
class SearchHandler(BaseHandler):
3047
"""
@@ -71,6 +88,7 @@ def handle_search_memories(self, search_req: APISearchRequest) -> SearchResponse
7188
# Search and deduplicate
7289
cube_view = self._build_cube_view(search_req_local)
7390
results = cube_view.search_memories(search_req_local)
91+
self._merge_context_recall(results=results, search_req=search_req_local)
7492
if not search_req_local.relativity:
7593
search_req_local.relativity = 0
7694
self.logger.info(f"[SearchHandler] Relativity filter: {search_req_local.relativity}")
@@ -102,6 +120,105 @@ def handle_search_memories(self, search_req: APISearchRequest) -> SearchResponse
102120
data=results,
103121
)
104122

123+
def _merge_context_recall(
124+
self, *, results: dict[str, Any], search_req: APISearchRequest
125+
) -> None:
126+
if not _env_enabled(_ENV_CONTEXT_RECALL, "off"):
127+
return
128+
129+
top_k = max(0, _env_int(_ENV_CONTEXT_RECALL_TOP_K, _DEFAULT_CONTEXT_RECALL_TOP_K))
130+
if top_k <= 0:
131+
return
132+
133+
context_buckets = self._recall_context_buckets(search_req=search_req, top_k=top_k)
134+
if not context_buckets:
135+
return
136+
137+
results.setdefault("text_mem", []).extend(context_buckets)
138+
139+
def _recall_context_buckets(
140+
self, *, search_req: APISearchRequest, top_k: int
141+
) -> list[dict[str, Any]]:
142+
graph_db = self.graph_db or getattr(self.searcher, "graph_store", None)
143+
embedder = self.embedder or getattr(self.searcher, "embedder", None)
144+
if graph_db is None or embedder is None:
145+
self.logger.info(
146+
"[SearchHandler] Context recall skipped: graph_db or embedder unavailable."
147+
)
148+
return []
149+
150+
try:
151+
query_embedding = embedder.embed([search_req.query])[0]
152+
except Exception:
153+
self.logger.warning("[SearchHandler] Context recall embedding failed.", exc_info=True)
154+
return []
155+
156+
buckets: list[dict[str, Any]] = []
157+
for cube_id in self._resolve_cube_ids(search_req):
158+
try:
159+
hits = graph_db.search_by_embedding(
160+
query_embedding,
161+
top_k=top_k,
162+
scope=CONTEXT_MEMORY_TYPE,
163+
status="activated",
164+
user_name=cube_id,
165+
return_fields=[
166+
"memory",
167+
"key",
168+
"created_at",
169+
"updated_at",
170+
"source",
171+
"internal_info",
172+
],
173+
)
174+
except Exception:
175+
self.logger.warning(
176+
"[SearchHandler] Context recall search failed for cube=%s.",
177+
cube_id,
178+
exc_info=True,
179+
)
180+
continue
181+
182+
memories = [self._format_context_hit(hit) for hit in hits or [] if hit.get("memory")]
183+
if not memories:
184+
continue
185+
buckets.append(
186+
{
187+
"cube_id": cube_id,
188+
"memories": memories,
189+
"total_nodes": len(memories),
190+
}
191+
)
192+
return buckets
193+
194+
@staticmethod
195+
def _format_context_hit(hit: dict[str, Any]) -> dict[str, Any]:
196+
context_id = str(hit.get("id", ""))
197+
score = float(hit.get("score", 0.0) or 0.0)
198+
metadata = {
199+
"id": context_id,
200+
"memory": hit.get("memory", ""),
201+
"memory_type": CONTEXT_MEMORY_TYPE,
202+
"source": hit.get("source") or "dream",
203+
"key": hit.get("key", ""),
204+
"relativity": score,
205+
"score": score,
206+
"embedding": [],
207+
"sources": [],
208+
"usage": [],
209+
"ref_id": f"[{context_id.split('-')[0]}]" if context_id else "[context]",
210+
}
211+
for field in ("created_at", "updated_at", "internal_info"):
212+
if hit.get(field) is not None:
213+
metadata[field] = hit[field]
214+
215+
return {
216+
"id": context_id,
217+
"memory": hit.get("memory", ""),
218+
"metadata": metadata,
219+
"ref_id": metadata["ref_id"],
220+
}
221+
105222
@staticmethod
106223
def _apply_relativity_threshold(results: dict[str, Any], relativity: float) -> dict[str, Any]:
107224
if relativity <= 0:

0 commit comments

Comments
 (0)