import json import logging import os import tempfile import uuid from hashlib import sha256 # Set up the directory path VECTOR_ID = str(uuid.uuid4()) home_dir = os.path.expanduser("~") mem0_dir = os.environ.get("MEM0_DIR") or os.path.join(home_dir, ".mem0") os.makedirs(mem0_dir, exist_ok=True) _logger = logging.getLogger(__name__) def _config_path(): return os.path.join(mem0_dir, "config.json") def _load_config(): """Load ~/.mem0/config.json, returning {} on missing/malformed file.""" path = _config_path() if not os.path.exists(path): return {} try: with open(path, "r") as f: data = json.load(f) return data if isinstance(data, dict) else {} except Exception as e: _logger.debug("Failed to load mem0 config %s: %s", path, e) return {} def _write_config(config): """Best-effort write of ~/.mem0/config.json. Never raises.""" path = _config_path() temp_path = None try: os.makedirs(os.path.dirname(path), exist_ok=True) with tempfile.NamedTemporaryFile("w", dir=os.path.dirname(path), delete=False) as f: temp_path = f.name json.dump(config, f, indent=4) f.flush() os.fsync(f.fileno()) os.replace(temp_path, path) except Exception as e: if temp_path: try: os.unlink(temp_path) except OSError: pass _logger.debug("Failed to write mem0 config %s: %s", path, e) def setup_config(): """Ensure ~/.mem0/config.json exists with a top-level user_id. Idempotent: backfills user_id for users whose config was written by the CLI (which writes telemetry.anonymous_id but no top-level user_id). Without this, OSS Python telemetry is silently dropped because get_user_id() returns None when user_id is missing. """ config = _load_config() if config.get("user_id"): return config["user_id"] = str(uuid.uuid4()) _write_config(config) def get_user_id(): config = _load_config() if not config: return "anonymous_user" return config.get("user_id") def read_anon_ids(): """Return anon IDs and alias markers from ~/.mem0/config.json. Returns a dict with keys "oss", "cli", "aliased_pairs" (IDs may be None). OSS Python writes top-level "user_id"; the CLI writes "telemetry.anonymous_id". They may coexist depending on which surface ran first. """ config = _load_config() telemetry = config.get("telemetry") if isinstance(config.get("telemetry"), dict) else {} aliased_pairs = telemetry.get("aliased_pairs") return { "oss": config.get("user_id"), "cli": telemetry.get("anonymous_id"), "aliased_pairs": aliased_pairs if isinstance(aliased_pairs, list) else [], } def _alias_pair_marker(anon_id, email): return sha256(f"{anon_id}\0{email}".encode("utf-8")).hexdigest() def is_aliased(anon_id, email): """Return whether anon_id -> email has already been identified.""" if not anon_id or not email: return False config = _load_config() telemetry = config.get("telemetry") if isinstance(config.get("telemetry"), dict) else {} aliased_pairs = telemetry.get("aliased_pairs") if not isinstance(aliased_pairs, list): return False return _alias_pair_marker(anon_id, email) in aliased_pairs def mark_aliased(anon_id, email): """Persist an anon_id -> email alias marker so $identify fires once per pair. The marker is hashed to avoid storing platform emails in the local config. """ if not anon_id or not email: return config = _load_config() telemetry = config.get("telemetry") if not isinstance(telemetry, dict): telemetry = {} aliased_pairs = telemetry.get("aliased_pairs") if not isinstance(aliased_pairs, list): aliased_pairs = [] marker = _alias_pair_marker(anon_id, email) if marker not in aliased_pairs: aliased_pairs.append(marker) telemetry["aliased_pairs"] = aliased_pairs config["telemetry"] = telemetry _write_config(config) def get_or_create_user_id(vector_store=None): """Store user_id in vector store and return it. If vector_store is None, simply returns the user_id from config. This ensures telemetry initialization never fails due to missing vector store. """ user_id = get_user_id() # If no vector store provided, just return the user_id if vector_store is None: return user_id # Try to get existing user_id from vector store try: existing = vector_store.get(vector_id=user_id) if existing and hasattr(existing, "payload") and existing.payload and "user_id" in existing.payload: stored_id = existing.payload["user_id"] # Ensure we never return None from vector store if stored_id is not None: return stored_id except Exception: pass # If we get here, we need to insert the user_id try: dims = getattr(vector_store, "embedding_model_dims", 1536) vector_store.insert( vectors=[[0.1] * dims], payloads=[{"user_id": user_id, "type": "user_identity"}], ids=[user_id] ) except Exception: pass return user_id