Skip to content

Commit 6e219c4

Browse files
lijicodecaocuilong
andauthored
use nacos (#407)
* fix * fix:nacos * feat: fix config Exception * feat: format config * feat: format config --------- Co-authored-by: ccl <[email protected]>
1 parent fef40e9 commit 6e219c4

File tree

2 files changed

+240
-1
lines changed

2 files changed

+240
-1
lines changed

src/memos/api/config.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import base64
2+
import hashlib
3+
import hmac
14
import json
5+
import logging
26
import os
7+
import re
8+
import time
39

410
from typing import Any
511

12+
import requests
13+
614
from dotenv import load_dotenv
715

816
from memos.configs.mem_cube import GeneralMemCubeConfig
@@ -13,6 +21,238 @@
1321
# Load environment variables
1422
load_dotenv()
1523

24+
logger = logging.getLogger(__name__)
25+
26+
27+
def _update_env_from_dict(data: dict[str, Any]) -> None:
28+
"""Apply a dict to environment variables, with change logging."""
29+
30+
def _is_sensitive(name: str) -> bool:
31+
n = name.upper()
32+
return any(s in n for s in ["PASSWORD", "SECRET", "AK", "SK", "TOKEN", "KEY"])
33+
34+
for k, v in data.items():
35+
if isinstance(v, dict):
36+
new_val = json.dumps(v, ensure_ascii=False)
37+
elif isinstance(v, bool):
38+
new_val = "true" if v else "false"
39+
elif v is None:
40+
new_val = ""
41+
else:
42+
new_val = str(v)
43+
44+
old_val = os.environ.get(k)
45+
os.environ[k] = new_val
46+
47+
try:
48+
log_old = "***" if _is_sensitive(k) else (old_val if old_val is not None else "<unset>")
49+
log_new = "***" if _is_sensitive(k) else new_val
50+
if old_val != new_val:
51+
logger.info(f"Nacos config update: {k}={log_new} (was {log_old})")
52+
except Exception as e:
53+
# Avoid logging failures blocking config updates
54+
logger.debug(f"Skip logging change for {k}: {e}")
55+
56+
57+
def get_config_json(name: str, default: Any | None = None) -> Any:
58+
"""Read JSON object/array from env and parse. Returns default on missing/invalid."""
59+
raw = os.getenv(name)
60+
if not raw:
61+
return default
62+
try:
63+
return json.loads(raw)
64+
except Exception:
65+
logger.warning(f"Invalid JSON in env '{name}', returning default.")
66+
return default
67+
68+
69+
def get_config_value(path: str, default: Any | None = None) -> Any:
70+
"""Read value from env with optional dot-path for structured configs.
71+
72+
Examples:
73+
- get_config_value("MONGODB_CONFIG.base_uri")
74+
- get_config_value("MONGODB_BASE_URI")
75+
"""
76+
if "." not in path:
77+
val = os.getenv(path)
78+
return val if val is not None else default
79+
root, *subkeys = path.split(".")
80+
data = get_config_json(root, default=None)
81+
if not isinstance(data, dict):
82+
return default
83+
cur: Any = data
84+
for key in subkeys:
85+
if isinstance(cur, dict) and key in cur:
86+
cur = cur[key]
87+
else:
88+
return default
89+
return cur
90+
91+
92+
class NacosConfigManager:
93+
_client = None
94+
_data_id = None
95+
_group = None
96+
_enabled = False
97+
98+
# Pre-compile regex patterns for better performance
99+
_KEY_VALUE_PATTERN = re.compile(r"^([^=]+)=(.*)$")
100+
_INTEGER_PATTERN = re.compile(r"^[+-]?\d+$")
101+
_FLOAT_PATTERN = re.compile(r"^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$")
102+
103+
@classmethod
104+
def _sign(cls, secret_key: str, data: str) -> str:
105+
"""HMAC-SHA1 sgin"""
106+
signature = hmac.new(secret_key.encode("utf-8"), data.encode("utf-8"), hashlib.sha1)
107+
return base64.b64encode(signature.digest()).decode()
108+
109+
@staticmethod
110+
def _parse_value(value: str) -> Any:
111+
"""Parse string value to appropriate Python type.
112+
113+
Supports: bool, int, float, and string.
114+
"""
115+
if not value:
116+
return value
117+
118+
val_lower = value.lower()
119+
120+
# Boolean
121+
if val_lower in ("true", "false"):
122+
return val_lower == "true"
123+
124+
# Integer
125+
if NacosConfigManager._INTEGER_PATTERN.match(value):
126+
try:
127+
return int(value)
128+
except (ValueError, OverflowError):
129+
return value
130+
131+
# Float
132+
if NacosConfigManager._FLOAT_PATTERN.match(value):
133+
try:
134+
return float(value)
135+
except (ValueError, OverflowError):
136+
return value
137+
138+
# Default to string
139+
return value
140+
141+
@staticmethod
142+
def parse_properties(content: str) -> dict[str, Any]:
143+
"""Parse properties file content to dictionary with type inference.
144+
145+
Supports:
146+
- Comments (lines starting with #)
147+
- Key-value pairs (KEY=VALUE)
148+
- Type inference (bool, int, float, string)
149+
"""
150+
data: dict[str, Any] = {}
151+
152+
for line in content.splitlines():
153+
line = line.strip()
154+
155+
# Skip empty lines and comments
156+
if not line or line.startswith("#"):
157+
continue
158+
159+
# Parse key-value pair
160+
match = NacosConfigManager._KEY_VALUE_PATTERN.match(line)
161+
if match:
162+
key = match.group(1).strip()
163+
value = match.group(2).strip()
164+
data[key] = NacosConfigManager._parse_value(value)
165+
166+
return data
167+
168+
@classmethod
169+
def start_config_watch(cls):
170+
while True:
171+
cls.init()
172+
time.sleep(60)
173+
174+
@classmethod
175+
def start_watch_if_enabled(cls) -> None:
176+
enable = os.getenv("NACOS_ENABLE_WATCH", "false").lower() == "true"
177+
print("enable:", enable)
178+
if not enable:
179+
return
180+
interval = int(os.getenv("NACOS_WATCH_INTERVAL", "60"))
181+
import threading
182+
183+
def _loop() -> None:
184+
while True:
185+
try:
186+
cls.init()
187+
except Exception as e:
188+
logger.error(f"❌ Nacos watch loop error: {e}")
189+
time.sleep(interval)
190+
191+
threading.Thread(target=_loop, daemon=True).start()
192+
logger.info(f"Nacos watch thread started (interval={interval}s).")
193+
194+
@classmethod
195+
def init(cls) -> None:
196+
server_addr = os.getenv("NACOS_SERVER_ADDR")
197+
data_id = os.getenv("NACOS_DATA_ID")
198+
group = os.getenv("NACOS_GROUP", "DEFAULT_GROUP")
199+
namespace = os.getenv("NACOS_NAMESPACE", "")
200+
ak = os.getenv("AK")
201+
sk = os.getenv("SK")
202+
203+
if not (server_addr and data_id and ak and sk):
204+
logger.warning("❌ missing NACOS_SERVER_ADDR / AK / SK / DATA_ID")
205+
return
206+
207+
base_url = f"http://{server_addr}/nacos/v1/cs/configs"
208+
209+
def _auth_headers():
210+
ts = str(int(time.time() * 1000))
211+
212+
sign_data = namespace + "+" + group + "+" + ts if namespace else group + "+" + ts
213+
signature = cls._sign(sk, sign_data)
214+
return {
215+
"Spas-AccessKey": ak,
216+
"Spas-Signature": signature,
217+
"timeStamp": ts,
218+
}
219+
220+
try:
221+
params = {
222+
"dataId": data_id,
223+
"group": group,
224+
"tenant": namespace,
225+
}
226+
227+
headers = _auth_headers()
228+
resp = requests.get(base_url, headers=headers, params=params, timeout=10)
229+
230+
if resp.status_code != 200:
231+
logger.error(f"Nacos AK/SK fail: {resp.status_code} {resp.text}")
232+
return
233+
234+
content = resp.text.strip()
235+
if not content:
236+
logger.warning("⚠️ Nacos is empty")
237+
return
238+
try:
239+
data_props = cls.parse_properties(content)
240+
logger.info("nacos config:", data_props)
241+
_update_env_from_dict(data_props)
242+
logger.info("✅ parse Nacos setting is Properties ")
243+
except Exception as e:
244+
logger.error(f"⚠️ Nacos parse fail(not JSON/YAML/Properties): {e}")
245+
raise Exception(f"Nacos configuration parsing failed: {e}") from e
246+
247+
except Exception as e:
248+
logger.error(f"❌ Nacos AK/SK init fail: {e}")
249+
raise Exception(f"❌ Nacos AK/SK init fail: {e}") from e
250+
251+
252+
# init Nacos
253+
NacosConfigManager.init()
254+
NacosConfigManager.start_watch_if_enabled()
255+
16256

17257
class APIConfig:
18258
"""Centralized configuration management for MemOS APIs."""

src/memos/graph_dbs/polardb.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,6 @@ def get_nodes(
833833
# Parse embedding from JSONB if it exists
834834
if embedding_json is not None:
835835
try:
836-
print("embedding_json:", embedding_json)
837836
# remove embedding
838837
"""
839838
embedding = json.loads(embedding_json) if isinstance(embedding_json, str) else embedding_json

0 commit comments

Comments
 (0)