repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | """ |
| #2 | Memory client utilities for OpenMemory. |
| #3 | |
| #4 | This module provides functionality to initialize and manage the Mem0 memory client |
| #5 | with automatic configuration management and Docker environment support. |
| #6 | |
| #7 | Docker Ollama Configuration: |
| #8 | When running inside a Docker container and using Ollama as the LLM or embedder provider, |
| #9 | the system automatically detects the Docker environment and adjusts localhost URLs |
| #10 | to properly reach the host machine where Ollama is running. |
| #11 | |
| #12 | Supported Docker host resolution (in order of preference): |
| #13 | 1. OLLAMA_HOST environment variable (if set) |
| #14 | 2. host.docker.internal (Docker Desktop for Mac/Windows) |
| #15 | 3. Docker bridge gateway IP (typically 172.17.0.1 on Linux) |
| #16 | 4. Fallback to 172.17.0.1 |
| #17 | |
| #18 | Example configuration that will be automatically adjusted: |
| #19 | { |
| #20 | "llm": { |
| #21 | "provider": "ollama", |
| #22 | "config": { |
| #23 | "model": "llama3.1:latest", |
| #24 | "ollama_base_url": "http://localhost:11434" # Auto-adjusted in Docker |
| #25 | } |
| #26 | } |
| #27 | } |
| #28 | """ |
| #29 | |
| #30 | import hashlib |
| #31 | import json |
| #32 | import os |
| #33 | import socket |
| #34 | |
| #35 | from app.database import SessionLocal |
| #36 | from app.models import Config as ConfigModel |
| #37 | |
| #38 | from mem0 import Memory |
| #39 | |
| #40 | _memory_client = None |
| #41 | _config_hash = None |
| #42 | |
| #43 | |
| #44 | def _get_config_hash(config_dict): |
| #45 | """Generate a hash of the config to detect changes.""" |
| #46 | config_str = json.dumps(config_dict, sort_keys=True) |
| #47 | return hashlib.md5(config_str.encode()).hexdigest() |
| #48 | |
| #49 | |
| #50 | def _get_docker_host_url(): |
| #51 | """ |
| #52 | Determine the appropriate host URL to reach host machine from inside Docker container. |
| #53 | Returns the best available option for reaching the host from inside a container. |
| #54 | """ |
| #55 | # Check for custom environment variable first |
| #56 | custom_host = os.environ.get('OLLAMA_HOST') |
| #57 | if custom_host: |
| #58 | print(f"Using custom Ollama host from OLLAMA_HOST: {custom_host}") |
| #59 | return custom_host.replace('http://', '').replace('https://', '').split(':')[0] |
| #60 | |
| #61 | # Check if we're running inside Docker |
| #62 | if not os.path.exists('/.dockerenv'): |
| #63 | # Not in Docker, return localhost as-is |
| #64 | return "localhost" |
| #65 | |
| #66 | print("Detected Docker environment, adjusting host URL for Ollama...") |
| #67 | |
| #68 | # Try different host resolution strategies |
| #69 | host_candidates = [] |
| #70 | |
| #71 | # 1. host.docker.internal (works on Docker Desktop for Mac/Windows) |
| #72 | try: |
| #73 | socket.gethostbyname('host.docker.internal') |
| #74 | host_candidates.append('host.docker.internal') |
| #75 | print("Found host.docker.internal") |
| #76 | except socket.gaierror: |
| #77 | pass |
| #78 | |
| #79 | # 2. Docker bridge gateway (typically 172.17.0.1 on Linux) |
| #80 | try: |
| #81 | with open('/proc/net/route', 'r') as f: |
| #82 | for line in f: |
| #83 | fields = line.strip().split() |
| #84 | if fields[1] == '00000000': # Default route |
| #85 | gateway_hex = fields[2] |
| #86 | gateway_ip = socket.inet_ntoa(bytes.fromhex(gateway_hex)[::-1]) |
| #87 | host_candidates.append(gateway_ip) |
| #88 | print(f"Found Docker gateway: {gateway_ip}") |
| #89 | break |
| #90 | except (FileNotFoundError, IndexError, ValueError): |
| #91 | pass |
| #92 | |
| #93 | # 3. Fallback to common Docker bridge IP |
| #94 | if not host_candidates: |
| #95 | host_candidates.append('172.17.0.1') |
| #96 | print("Using fallback Docker bridge IP: 172.17.0.1") |
| #97 | |
| #98 | # Return the first available candidate |
| #99 | return host_candidates[0] |
| #100 | |
| #101 | |
| #102 | def _fix_ollama_urls(config_section): |
| #103 | """ |
| #104 | Fix Ollama URLs for Docker environment. |
| #105 | Replaces localhost URLs with appropriate Docker host URLs. |
| #106 | Sets default ollama_base_url if not provided. |
| #107 | """ |
| #108 | if not config_section or "config" not in config_section: |
| #109 | return config_section |
| #110 | |
| #111 | ollama_config = config_section["config"] |
| #112 | |
| #113 | # Set default ollama_base_url if not provided |
| #114 | if "ollama_base_url" not in ollama_config: |
| #115 | ollama_config["ollama_base_url"] = "http://host.docker.internal:11434" |
| #116 | else: |
| #117 | # Check for ollama_base_url and fix if it's localhost |
| #118 | url = ollama_config["ollama_base_url"] |
| #119 | if "localhost" in url or "127.0.0.1" in url: |
| #120 | docker_host = _get_docker_host_url() |
| #121 | if docker_host != "localhost": |
| #122 | new_url = url.replace("localhost", docker_host).replace("127.0.0.1", docker_host) |
| #123 | ollama_config["ollama_base_url"] = new_url |
| #124 | print(f"Adjusted Ollama URL from {url} to {new_url}") |
| #125 | |
| #126 | return config_section |
| #127 | |
| #128 | |
| #129 | def reset_memory_client(): |
| #130 | """Reset the global memory client to force reinitialization with new config.""" |
| #131 | global _memory_client, _config_hash |
| #132 | _memory_client = None |
| #133 | _config_hash = None |
| #134 | |
| #135 | |
| #136 | def get_default_memory_config(): |
| #137 | """Get default memory client configuration with sensible defaults.""" |
| #138 | # Detect vector store based on environment variables |
| #139 | vector_store_config = { |
| #140 | "collection_name": "openmemory", |
| #141 | "host": "mem0_store", |
| #142 | } |
| #143 | |
| #144 | # Check for different vector store configurations based on environment variables |
| #145 | if os.environ.get('CHROMA_HOST') and os.environ.get('CHROMA_PORT'): |
| #146 | vector_store_provider = "chroma" |
| #147 | vector_store_config.update({ |
| #148 | "host": os.environ.get('CHROMA_HOST'), |
| #149 | "port": int(os.environ.get('CHROMA_PORT')) |
| #150 | }) |
| #151 | elif os.environ.get('QDRANT_HOST') and os.environ.get('QDRANT_PORT'): |
| #152 | vector_store_provider = "qdrant" |
| #153 | vector_store_config.update({ |
| #154 | "host": os.environ.get('QDRANT_HOST'), |
| #155 | "port": int(os.environ.get('QDRANT_PORT')) |
| #156 | }) |
| #157 | elif os.environ.get('WEAVIATE_CLUSTER_URL') or (os.environ.get('WEAVIATE_HOST') and os.environ.get('WEAVIATE_PORT')): |
| #158 | vector_store_provider = "weaviate" |
| #159 | # Prefer an explicit cluster URL if provided; otherwise build from host/port |
| #160 | cluster_url = os.environ.get('WEAVIATE_CLUSTER_URL') |
| #161 | if not cluster_url: |
| #162 | weaviate_host = os.environ.get('WEAVIATE_HOST') |
| #163 | weaviate_port = int(os.environ.get('WEAVIATE_PORT')) |
| #164 | cluster_url = f"http://{weaviate_host}:{weaviate_port}" |
| #165 | vector_store_config = { |
| #166 | "collection_name": "openmemory", |
| #167 | "cluster_url": cluster_url |
| #168 | } |
| #169 | elif os.environ.get('REDIS_URL'): |
| #170 | vector_store_provider = "redis" |
| #171 | vector_store_config = { |
| #172 | "collection_name": "openmemory", |
| #173 | "redis_url": os.environ.get('REDIS_URL') |
| #174 | } |
| #175 | elif os.environ.get('PG_HOST') and os.environ.get('PG_PORT'): |
| #176 | vector_store_provider = "pgvector" |
| #177 | vector_store_config.update({ |
| #178 | "host": os.environ.get('PG_HOST'), |
| #179 | "port": int(os.environ.get('PG_PORT')), |
| #180 | "dbname": os.environ.get('PG_DB', 'mem0'), |
| #181 | "user": os.environ.get('PG_USER', 'mem0'), |
| #182 | "password": os.environ.get('PG_PASSWORD', 'mem0') |
| #183 | }) |
| #184 | elif os.environ.get('MILVUS_HOST') and os.environ.get('MILVUS_PORT'): |
| #185 | vector_store_provider = "milvus" |
| #186 | # Construct the full URL as expected by MilvusDBConfig |
| #187 | milvus_host = os.environ.get('MILVUS_HOST') |
| #188 | milvus_port = int(os.environ.get('MILVUS_PORT')) |
| #189 | milvus_url = f"http://{milvus_host}:{milvus_port}" |
| #190 | |
| #191 | vector_store_config = { |
| #192 | "collection_name": "openmemory", |
| #193 | "url": milvus_url, |
| #194 | "token": os.environ.get('MILVUS_TOKEN', ''), # Always include, empty string for local setup |
| #195 | "db_name": os.environ.get('MILVUS_DB_NAME', ''), |
| #196 | "embedding_model_dims": 1536, |
| #197 | "metric_type": "COSINE" # Using COSINE for better semantic similarity |
| #198 | } |
| #199 | elif os.environ.get('ELASTICSEARCH_HOST') and os.environ.get('ELASTICSEARCH_PORT'): |
| #200 | vector_store_provider = "elasticsearch" |
| #201 | # Construct the full URL with scheme since Elasticsearch client expects it |
| #202 | elasticsearch_host = os.environ.get('ELASTICSEARCH_HOST') |
| #203 | elasticsearch_port = int(os.environ.get('ELASTICSEARCH_PORT')) |
| #204 | # Use http:// scheme since we're not using SSL |
| #205 | full_host = f"http://{elasticsearch_host}" |
| #206 | |
| #207 | vector_store_config.update({ |
| #208 | "host": full_host, |
| #209 | "port": elasticsearch_port, |
| #210 | "user": os.environ.get('ELASTICSEARCH_USER', 'elastic'), |
| #211 | "password": os.environ.get('ELASTICSEARCH_PASSWORD', 'changeme'), |
| #212 | "verify_certs": False, |
| #213 | "use_ssl": False, |
| #214 | "embedding_model_dims": 1536 |
| #215 | }) |
| #216 | elif os.environ.get('OPENSEARCH_HOST') and os.environ.get('OPENSEARCH_PORT'): |
| #217 | vector_store_provider = "opensearch" |
| #218 | vector_store_config.update({ |
| #219 | "host": os.environ.get('OPENSEARCH_HOST'), |
| #220 | "port": int(os.environ.get('OPENSEARCH_PORT')) |
| #221 | }) |
| #222 | elif os.environ.get('FAISS_PATH'): |
| #223 | vector_store_provider = "faiss" |
| #224 | vector_store_config = { |
| #225 | "collection_name": "openmemory", |
| #226 | "path": os.environ.get('FAISS_PATH'), |
| #227 | "embedding_model_dims": 1536, |
| #228 | "distance_strategy": "cosine" |
| #229 | } |
| #230 | else: |
| #231 | # Default fallback to Qdrant |
| #232 | vector_store_provider = "qdrant" |
| #233 | vector_store_config.update({ |
| #234 | "port": 6333, |
| #235 | }) |
| #236 | |
| #237 | print(f"Auto-detected vector store: {vector_store_provider} with config: {vector_store_config}") |
| #238 | |
| #239 | return { |
| #240 | "vector_store": { |
| #241 | "provider": vector_store_provider, |
| #242 | "config": vector_store_config |
| #243 | }, |
| #244 | "llm": { |
| #245 | "provider": "openai", |
| #246 | "config": { |
| #247 | "model": "gpt-4o-mini", |
| #248 | "temperature": 0.1, |
| #249 | "max_tokens": 2000, |
| #250 | "api_key": "env:OPENAI_API_KEY" |
| #251 | } |
| #252 | }, |
| #253 | "embedder": { |
| #254 | "provider": "openai", |
| #255 | "config": { |
| #256 | "model": "text-embedding-3-small", |
| #257 | "api_key": "env:OPENAI_API_KEY" |
| #258 | } |
| #259 | }, |
| #260 | "version": "v1.1" |
| #261 | } |
| #262 | |
| #263 | |
| #264 | def _parse_environment_variables(config_dict): |
| #265 | """ |
| #266 | Parse environment variables in config values. |
| #267 | Converts 'env:VARIABLE_NAME' to actual environment variable values. |
| #268 | """ |
| #269 | if isinstance(config_dict, dict): |
| #270 | parsed_config = {} |
| #271 | for key, value in config_dict.items(): |
| #272 | if isinstance(value, str) and value.startswith("env:"): |
| #273 | env_var = value.split(":", 1)[1] |
| #274 | env_value = os.environ.get(env_var) |
| #275 | if env_value: |
| #276 | parsed_config[key] = env_value |
| #277 | print(f"Loaded {env_var} from environment for {key}") |
| #278 | else: |
| #279 | print(f"Warning: Environment variable {env_var} not found, keeping original value") |
| #280 | parsed_config[key] = value |
| #281 | elif isinstance(value, dict): |
| #282 | parsed_config[key] = _parse_environment_variables(value) |
| #283 | else: |
| #284 | parsed_config[key] = value |
| #285 | return parsed_config |
| #286 | return config_dict |
| #287 | |
| #288 | |
| #289 | def get_memory_client(custom_instructions: str = None): |
| #290 | """ |
| #291 | Get or initialize the Mem0 client. |
| #292 | |
| #293 | Args: |
| #294 | custom_instructions: Optional instructions for the memory project. |
| #295 | |
| #296 | Returns: |
| #297 | Initialized Mem0 client instance or None if initialization fails. |
| #298 | |
| #299 | Raises: |
| #300 | Exception: If required API keys are not set or critical configuration is missing. |
| #301 | """ |
| #302 | global _memory_client, _config_hash |
| #303 | |
| #304 | try: |
| #305 | # Start with default configuration |
| #306 | config = get_default_memory_config() |
| #307 | |
| #308 | # Variable to track custom instructions |
| #309 | db_custom_instructions = None |
| #310 | |
| #311 | # Load configuration from database |
| #312 | try: |
| #313 | db = SessionLocal() |
| #314 | db_config = db.query(ConfigModel).filter(ConfigModel.key == "main").first() |
| #315 | |
| #316 | if db_config: |
| #317 | json_config = db_config.value |
| #318 | |
| #319 | # Extract custom instructions from openmemory settings |
| #320 | if "openmemory" in json_config and "custom_instructions" in json_config["openmemory"]: |
| #321 | db_custom_instructions = json_config["openmemory"]["custom_instructions"] |
| #322 | |
| #323 | # Override defaults with configurations from the database |
| #324 | if "mem0" in json_config: |
| #325 | mem0_config = json_config["mem0"] |
| #326 | |
| #327 | # Update LLM configuration if available |
| #328 | if "llm" in mem0_config and mem0_config["llm"] is not None: |
| #329 | config["llm"] = mem0_config["llm"] |
| #330 | |
| #331 | # Fix Ollama URLs for Docker if needed |
| #332 | if config["llm"].get("provider") == "ollama": |
| #333 | config["llm"] = _fix_ollama_urls(config["llm"]) |
| #334 | |
| #335 | # Update Embedder configuration if available |
| #336 | if "embedder" in mem0_config and mem0_config["embedder"] is not None: |
| #337 | config["embedder"] = mem0_config["embedder"] |
| #338 | |
| #339 | # Fix Ollama URLs for Docker if needed |
| #340 | if config["embedder"].get("provider") == "ollama": |
| #341 | config["embedder"] = _fix_ollama_urls(config["embedder"]) |
| #342 | |
| #343 | if "vector_store" in mem0_config and mem0_config["vector_store"] is not None: |
| #344 | config["vector_store"] = mem0_config["vector_store"] |
| #345 | else: |
| #346 | print("No configuration found in database, using defaults") |
| #347 | |
| #348 | db.close() |
| #349 | |
| #350 | except Exception as e: |
| #351 | print(f"Warning: Error loading configuration from database: {e}") |
| #352 | print("Using default configuration") |
| #353 | # Continue with default configuration if database config can't be loaded |
| #354 | |
| #355 | # Use custom_instructions parameter first, then fall back to database value |
| #356 | instructions_to_use = custom_instructions or db_custom_instructions |
| #357 | if instructions_to_use: |
| #358 | config["custom_fact_extraction_prompt"] = instructions_to_use |
| #359 | |
| #360 | # ALWAYS parse environment variables in the final config |
| #361 | # This ensures that even default config values like "env:OPENAI_API_KEY" get parsed |
| #362 | print("Parsing environment variables in final config...") |
| #363 | config = _parse_environment_variables(config) |
| #364 | |
| #365 | # Check if config has changed by comparing hashes |
| #366 | current_config_hash = _get_config_hash(config) |
| #367 | |
| #368 | # Only reinitialize if config changed or client doesn't exist |
| #369 | if _memory_client is None or _config_hash != current_config_hash: |
| #370 | print(f"Initializing memory client with config hash: {current_config_hash}") |
| #371 | try: |
| #372 | _memory_client = Memory.from_config(config_dict=config) |
| #373 | _config_hash = current_config_hash |
| #374 | print("Memory client initialized successfully") |
| #375 | except Exception as init_error: |
| #376 | print(f"Warning: Failed to initialize memory client: {init_error}") |
| #377 | print("Server will continue running with limited memory functionality") |
| #378 | _memory_client = None |
| #379 | _config_hash = None |
| #380 | return None |
| #381 | |
| #382 | return _memory_client |
| #383 | |
| #384 | except Exception as e: |
| #385 | print(f"Warning: Exception occurred while initializing memory client: {e}") |
| #386 | print("Server will continue running with limited memory functionality") |
| #387 | return None |
| #388 | |
| #389 | |
| #390 | def get_default_user_id(): |
| #391 | return "default_user" |
| #392 |