Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 18 additions & 12 deletions apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@
sys.path.insert(0, str(_PLUGIN_DIR))

from bridge_client import BridgeError, MemosBridgeClient, MemosHttpClient # noqa: E402
from daemon_manager import ensure_bridge_running, ensure_viewer_daemon, probe_viewer_status, kill_zombie_bridges, startup_lock_active # noqa: E402
from daemon_manager import ( # noqa: E402
ensure_bridge_running,
ensure_viewer_daemon,
kill_zombie_bridges,
probe_viewer_status,
startup_lock_active,
)


try: # pragma: no cover — host-provided base class, absent in unit tests
Expand Down Expand Up @@ -428,13 +434,12 @@ def initialize(self, session_id: str, **kwargs: Any) -> None: # type: ignore[ov
if startup_lock_active():
time.sleep(1.0)
viewer_status = probe_viewer_status()
if viewer_status == "running_memos":
if self._connect_http_bridge(session_id):
logger.info(
"MemOS: bridge ready (HTTP, late probe) session=%s platform=%s (episode deferred)",
self._session_id,
self._platform,
)
if viewer_status == "running_memos" and self._connect_http_bridge(session_id):
logger.info(
"MemOS: bridge ready (HTTP, late probe) session=%s platform=%s (episode deferred)",
self._session_id,
self._platform,
)

if self._bridge is None:
try:
Expand Down Expand Up @@ -1955,10 +1960,11 @@ def _reconnect_bridge(self, session_id: str = "", *, timeout: float = 30.0) -> N
ensure_bridge_running()
# Try HTTP first if daemon is running
viewer_status = probe_viewer_status()
if viewer_status == "running_memos":
if self._connect_http_bridge(session_id, timeout=timeout):
logger.info("MemOS: reconnected via HTTP")
return
if viewer_status == "running_memos" and self._connect_http_bridge(
session_id, timeout=timeout
):
logger.info("MemOS: reconnected via HTTP")
return

try:
ensure_viewer_daemon()
Expand Down
56 changes: 41 additions & 15 deletions src/memos/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,28 @@ def _auth_headers():
class APIConfig:
"""Centralized configuration management for MemOS APIs."""

@staticmethod
def _preference_extractor_extra_body(model_name: str) -> dict[str, Any] | None:
normalized_model = model_name.strip().lower()
if normalized_model.startswith(("qwen3.5", "qwen3.6")):
return {"enable_thinking": False}
return None

@staticmethod
def get_profile_memory_reserved_top_k() -> int:
"""Get profile-memory MMR reserve count.

This is intentionally read from environment/config at runtime so Nacos
property updates can take effect without changing request payloads.
"""
raw = os.getenv("PROFILE_MEMORY_RESERVED_TOP_K", "0")
try:
value = int(raw)
except (TypeError, ValueError):
logger.warning("Invalid PROFILE_MEMORY_RESERVED_TOP_K=%r, using 0.", raw)
return 0
return max(0, value)

@staticmethod
def get_openai_config() -> dict[str, Any]:
"""Get OpenAI configuration."""
Expand Down Expand Up @@ -469,23 +491,27 @@ def get_preference_extractor_llm_config() -> dict[str, Any]:
"""
pref_model = os.getenv("PREFERENCE_EXTRACTOR_MODEL")
if pref_model:
extra_body = APIConfig._preference_extractor_extra_body(pref_model)
config = {
"model_name_or_path": pref_model,
"temperature": 0.6,
"max_tokens": int(os.getenv("PREFERENCE_EXTRACTOR_MAX_TOKENS", "8000")),
"top_p": 0.95,
"top_k": 20,
"api_key": os.getenv(
"PREFERENCE_EXTRACTOR_API_KEY", os.getenv("OPENAI_API_KEY", "EMPTY")
),
"api_base": os.getenv(
"PREFERENCE_EXTRACTOR_API_BASE",
os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"),
),
"remove_think_prefix": True,
}
if extra_body is not None:
config["extra_body"] = extra_body
return {
"backend": os.getenv("PREFERENCE_EXTRACTOR_BACKEND", "openai"),
"config": {
"model_name_or_path": pref_model,
"temperature": 0.6,
"max_tokens": int(os.getenv("PREFERENCE_EXTRACTOR_MAX_TOKENS", "8000")),
"top_p": 0.95,
"top_k": 20,
"api_key": os.getenv(
"PREFERENCE_EXTRACTOR_API_KEY", os.getenv("OPENAI_API_KEY", "EMPTY")
),
"api_base": os.getenv(
"PREFERENCE_EXTRACTOR_API_BASE",
os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"),
),
"remove_think_prefix": True,
},
"config": config,
}
# Fallback to general_llm config (which itself falls back to OpenAI)
return APIConfig.get_memreader_general_llm_config()
Expand Down
25 changes: 24 additions & 1 deletion tests/mem_reader/test_preference_extractor_llm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
TextualMemoryItem,
TreeNodeTextualMemoryMetadata,
)
from memos.types.general_types import MemoryView


def test_product_default_config_wires_preference_extractor_model(monkeypatch):
Expand All @@ -28,6 +29,26 @@ def test_product_default_config_wires_preference_extractor_model(monkeypatch):
assert pref_config["config"]["api_key"] == "pref-key"


def test_preference_extractor_qwen35_disables_thinking(monkeypatch):
monkeypatch.setenv("PREFERENCE_EXTRACTOR_MODEL", "qwen3.5-flash")
monkeypatch.setenv("PREFERENCE_EXTRACTOR_API_BASE", "https://dashscope.example/v1")
monkeypatch.setenv("PREFERENCE_EXTRACTOR_API_KEY", "pref-key")

pref_config = APIConfig.get_preference_extractor_llm_config()

assert pref_config["config"]["extra_body"] == {"enable_thinking": False}


def test_preference_extractor_qwen36_disables_thinking(monkeypatch):
monkeypatch.setenv("PREFERENCE_EXTRACTOR_MODEL", "qwen3.6-flash")
monkeypatch.setenv("PREFERENCE_EXTRACTOR_API_BASE", "https://dashscope.example/v1")
monkeypatch.setenv("PREFERENCE_EXTRACTOR_API_KEY", "pref-key")

pref_config = APIConfig.get_preference_extractor_llm_config()

assert pref_config["config"]["extra_body"] == {"enable_thinking": False}


def test_product_default_config_leaves_preference_extractor_unset_without_model(monkeypatch):
monkeypatch.delenv("PREFERENCE_EXTRACTOR_MODEL", raising=False)
monkeypatch.delenv("PREFERENCE_EXTRACTOR_API_BASE", raising=False)
Expand Down Expand Up @@ -129,6 +150,8 @@ def test_multimodal_transfer_uses_preference_extractor_llm():
"memos.mem_reader.multi_modal_struct.process_preference_fine", return_value=[]
) as mock_process_pref,
):
reader._process_transfer_multi_modal_data([raw_node])
reader._process_transfer_multi_modal_data(
[raw_node], allow_memory_view=[MemoryView.PREFERENCE]
)

assert mock_process_pref.call_args.args[2] is reader.preference_extractor_llm
Loading