diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py index c47c28252..25546dfff 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py @@ -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 @@ -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: @@ -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() diff --git a/src/memos/api/config.py b/src/memos/api/config.py index d5c07dcbe..4d2029969 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -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.""" @@ -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() diff --git a/tests/mem_reader/test_preference_extractor_llm_config.py b/tests/mem_reader/test_preference_extractor_llm_config.py index a7c6507c4..dc23a704b 100644 --- a/tests/mem_reader/test_preference_extractor_llm_config.py +++ b/tests/mem_reader/test_preference_extractor_llm_config.py @@ -12,6 +12,7 @@ TextualMemoryItem, TreeNodeTextualMemoryMetadata, ) +from memos.types.general_types import MemoryView def test_product_default_config_wires_preference_extractor_model(monkeypatch): @@ -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) @@ -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