Skip to content
Open
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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ tavily = [
"tavily-python (>=0.5.0,<1.0.0)",
]

# OceanBase / seekdb unified vector + graph provider
ob-mem = [
"pyseekdb (>=1.4.0,<1.5.0)", # Unified client for seekdb / OceanBase (Collection + raw SQL)
]

# All optional dependencies
# Allow users to install with `pip install MemoryOS[all]`
all = [
Expand Down Expand Up @@ -140,6 +145,7 @@ all = [
"rake-nltk (>=1.0.6,<1.1.0)",
"alibabacloud-oss-v2 (>=1.2.2,<1.2.3)",
"tavily-python (>=0.5.0,<1.0.0)",
"pyseekdb (>=1.4.0,<1.5.0)",

# Uncategorized dependencies
]
Expand Down
24 changes: 24 additions & 0 deletions src/memos/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,30 @@ def get_postgres_config(user_id: str | None = None) -> dict[str, Any]:
"maxconn": int(os.getenv("POSTGRES_MAX_CONN", "20")),
}

@staticmethod
def get_oceanbase_config(user_id: str | None = None) -> dict[str, Any]:
"""Get OceanBase / seekdb configuration for MemOS graph storage.

Single MySQL-compatible database with logical tenant isolation by
``user_name``; nodes/edges live in ``{table_prefix}_nodes`` / ``_edges``.
"""
user_name = os.getenv("MEMOS_USER_NAME", "default")
if user_id:
user_name = f"memos_{user_id.replace('-', '')}"

return {
"host": os.getenv("OCEANBASE_HOST", "localhost"),
"port": int(os.getenv("OCEANBASE_PORT", "2881")),
"user": os.getenv("OCEANBASE_USER", "root"),
"password": os.getenv("OCEANBASE_PASSWORD", ""),
"db_name": os.getenv("OCEANBASE_DB", "memos"),
"table_prefix": os.getenv("OCEANBASE_TABLE_PREFIX", "memos_graph"),
"user_name": user_name,
"use_multi_db": False,
"embedding_dimension": int(os.getenv("EMBEDDING_DIMENSION", "1024")),
"maxconn": int(os.getenv("OCEANBASE_MAX_CONN", "20")),
}

@staticmethod
def get_mysql_config() -> dict[str, Any]:
"""Get MySQL configuration."""
Expand Down
2 changes: 2 additions & 0 deletions src/memos/api/handlers/config_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def build_graph_db_config(user_id: str = "default") -> dict[str, Any]:
"neo4j": APIConfig.get_neo4j_config(user_id=user_id),
"polardb": APIConfig.get_polardb_config(user_id=user_id),
"postgres": APIConfig.get_postgres_config(user_id=user_id),
"oceanbase": APIConfig.get_oceanbase_config(user_id=user_id),
"seekdb": APIConfig.get_oceanbase_config(user_id=user_id),
}

# Support both GRAPH_DB_BACKEND and legacy NEO4J_BACKEND env vars
Expand Down
79 changes: 79 additions & 0 deletions src/memos/configs/graph_db.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import re

from typing import Any, ClassVar

from pydantic import BaseModel, Field, field_validator, model_validator

from memos.configs.base import BaseConfig
from memos.configs.vec_db import VectorDBConfigFactory
from memos.log import get_logger


logger = get_logger(__name__)


class BaseGraphDBConfig(BaseConfig):
Expand Down Expand Up @@ -241,6 +247,77 @@ def validate_config(self):
return self


class OceanBaseGraphDBConfig(BaseConfig):
"""
OceanBase / seekdb graph configuration for MemOS (via pyseekdb).

Stores graph nodes, edges, JSON properties and node embeddings in a single
MySQL-compatible database. Logical tenant isolation is done via ``user_name``.

Example:
---
host = "127.0.0.1"
port = 2881
user = "root"
password = "secret"
db_name = "memos"
user_name = "alice"
use_multi_db = False
"""

host: str = Field(..., description="Database host")
port: int = Field(default=2881, description="Database port")
user: str = Field(default="root", description="Database user")
password: str = Field(default="", description="Database password")
db_name: str = Field(..., description="Database name")
table_prefix: str = Field(default="memos_graph", description="Prefix for the node/edge tables")
user_name: str | None = Field(
default=None,
description="Logical user/tenant ID for data isolation",
)
use_multi_db: bool = Field(
default=False,
description="If False: use single database with logical isolation by user_name",
)
embedding_dimension: int = Field(default=768, description="Dimension of vector embedding")
maxconn: int = Field(
default=20,
description="Maximum number of connections in the connection pool",
)

@field_validator("table_prefix")
@classmethod
def validate_table_prefix(cls, value: str) -> str:
"""Reject prefixes that could break out of the identifier when built into DDL."""
# Longest derived identifier is idx_{prefix}_nodes_embedding (prefix + 20 chars);
# MySQL / OceanBase cap identifiers at 64 characters.
max_prefix_len = 44
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", value):
raise ValueError(
"`table_prefix` must match [A-Za-z_][A-Za-z0-9_]* (letters, digits, underscore)"
)
if len(value) > max_prefix_len:
raise ValueError(
f"`table_prefix` must be at most {max_prefix_len} characters to keep derived "
"index names within MySQL/OceanBase's 64-character identifier limit"
)
return value

@model_validator(mode="after")
def validate_config(self):
"""Validate config."""
if not self.db_name:
raise ValueError("`db_name` must be provided")
if not self.use_multi_db and not self.user_name:
raise ValueError("In single-database mode, `user_name` must be provided")
if not self.password:
logger.warning(
"OceanBase graph DB is configured with an empty password; "
"make sure this is intentional (e.g. a local dev instance)."
)
return self


class GraphDBConfigFactory(BaseModel):
backend: str = Field(..., description="Backend for graph database")
config: dict[str, Any] = Field(..., description="Configuration for the graph database backend")
Expand All @@ -250,6 +327,8 @@ class GraphDBConfigFactory(BaseModel):
"neo4j-community": Neo4jCommunityGraphDBConfig,
"polardb": PolarDBGraphDBConfig,
"postgres": PostgresGraphDBConfig,
"oceanbase": OceanBaseGraphDBConfig,
"seekdb": OceanBaseGraphDBConfig,
}

@field_validator("backend")
Expand Down
20 changes: 20 additions & 0 deletions src/memos/configs/vec_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ class MilvusVecDBConfig(BaseVecDBConfig):
password: str = Field(default="", description="Password for Milvus connection")


class OceanBaseVecDBConfig(BaseVecDBConfig):
"""Configuration for OceanBase / seekdb vector database (via pyseekdb)."""

host: str = Field(..., description="Host for the seekdb / OceanBase server")
port: int = Field(default=2881, description="Port for the seekdb / OceanBase server")
user: str = Field(default="root", description="Username for the connection")
password: str = Field(default="", description="Password for the connection")
database: str = Field(default="memos", description="Database name")

@field_validator("vector_dimension")
@classmethod
def validate_vector_dimension(cls, value: int | None) -> int:
"""OceanBase requires a concrete vector dimension to build the HNSW index."""
if value is None or isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise ValueError("`vector_dimension` must be a positive integer for OceanBase")
return value


class VectorDBConfigFactory(BaseConfig):
"""Factory class for creating vector database configurations."""

Expand All @@ -63,6 +81,8 @@ class VectorDBConfigFactory(BaseConfig):
backend_to_class: ClassVar[dict[str, Any]] = {
"qdrant": QdrantVecDBConfig,
"milvus": MilvusVecDBConfig,
"oceanbase": OceanBaseVecDBConfig,
"seekdb": OceanBaseVecDBConfig,
}

@field_validator("backend")
Expand Down
3 changes: 3 additions & 0 deletions src/memos/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class MemCubeError(MemOSError): ...
class VectorDBError(MemOSError): ...


class GraphDBError(MemOSError): ...


class LLMError(MemOSError): ...


Expand Down
3 changes: 3 additions & 0 deletions src/memos/graph_dbs/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from memos.graph_dbs.base import BaseGraphDB
from memos.graph_dbs.neo4j import Neo4jGraphDB
from memos.graph_dbs.neo4j_community import Neo4jCommunityGraphDB
from memos.graph_dbs.oceanbase import OceanBaseGraphDB
from memos.graph_dbs.polardb import PolarDBGraphDB
from memos.graph_dbs.postgres import PostgresGraphDB

Expand All @@ -16,6 +17,8 @@ class GraphStoreFactory(BaseGraphDB):
"neo4j-community": Neo4jCommunityGraphDB,
"polardb": PolarDBGraphDB,
"postgres": PostgresGraphDB,
"oceanbase": OceanBaseGraphDB,
"seekdb": OceanBaseGraphDB,
}

@classmethod
Expand Down
Loading